您好,欢迎访问一九零五行业门户网

详解vue3中reactive和ref的区别(源码解析)

vue中reactive和ref的区别是什么?下面本篇文章带大家深入源码彻底搞清vue3中reactive和ref的区别,希望对大家有所帮助!
在vue3的日常开发中,我发现很多人都是基于自己的习惯reactive或ref一把梭,虽然这样都可以实现需求,既然这样那为什么已经有了reactive还需要再去设计一个ref呢?这两者的实际运用场景以及区别是什么呢?
并且关于ref的底层逻辑,有的人说ref的底层逻辑还是reactive。有的人说ref的底层是class,value只是这个class的一个属性,那这两种说法哪种正确呢?都有没有依据呢?
抱着这样的疑问我们本次就深入源码,彻底搞清vue3中reactive和ref的区别。(学习视频分享:vue视频教程)
不想看源码的童鞋,可以直接拉到后面看总结
reactive源码地址:packages/reactivity/reactive.ts
首先我们看一下vue3中用来标记目标对象target类型的reactiveflags
// 标记目标对象 target 类型的 reactiveflagsexport const enum reactiveflags {  skip = '__v_skip',  is_reactive = '__v_isreactive',  is_readonly = '__v_isreadonly',  raw = '__v_raw'}export interface target {  [reactiveflags.skip]?: boolean          // 不做响应式处理的数据  [reactiveflags.is_reactive]?: boolean   // target 是否是响应式  [reactiveflags.is_readonly]?: boolean   // target 是否是只读  [reactiveflags.raw]?: any               // 表示proxy 对应的源数据, target 已经是 proxy 对象时会有该属性}
reactive
export function reactive<t extends object>(target: t): unwrapnestedrefs<t>export function reactive(target: object) {  // if trying to observe a readonly proxy, return the readonly version.  // 如果目标对象是一个只读的响应数据,则直接返回目标对象  if (target && (target as target)[reactiveflags.is_readonly]) {    return target  }  // 创建 observe  return createreactiveobject(    target,    false,    mutablehandlers,    mutablecollectionhandlers,    reactivemap  )}
reactive函数接收一个target对象,如果target对象只读则直接返回该对象
若非只读则直接通过createreactiveobject创建observe对象
createreactiveobject
看着长不要怕,先贴createreactiveobject完整代码,我们分段阅读
/** *  * @param target 目标对象 * @param isreadonly 是否只读 * @param basehandlers 基本类型的 handlers * @param collectionhandlers 主要针对(set、map、weakset、weakmap)的 handlers * @param proxymap  weakmap数据结构 * @returns  */function createreactiveobject(  target: target,  isreadonly: boolean,  basehandlers: proxyhandler<any>,  collectionhandlers: proxyhandler<any>,  proxymap: weakmap<target, any>) {  // typeof 不是 object 类型的,在开发模式抛出警告,生产环境直接返回目标对象  if (!isobject(target)) {    if (__dev__) {      console.warn(`value cannot be made reactive: ${string(target)}`)    }    return target  }  // target is already a proxy, return it.  // exception: calling readonly() on a reactive object  // 已经是响应式的就直接返回(取reactiveflags.raw 属性会返回true,因为进行reactive的过程中会用weakmap进行保存,  // 通过target能判断出是否有reactiveflags.raw属性)  // 例外:对reactive对象进行readonly()  if (    target[reactiveflags.raw] &&    !(isreadonly && target[reactiveflags.is_reactive])  ) {    return target  }  // target already has corresponding proxy  // 对已经proxy的,则直接从weakmap数据结构中取出这个proxy对象  const existingproxy = proxymap.get(target)  if (existingproxy) {    return existingproxy  }  // only a whitelist of value types can be observed.  // 只对targettypemap类型白名单中的类型进行响应式处理  const targettype = gettargettype(target)  if (targettype === targettype.invalid) {    return target  }  // proxy 代理 target  // (set、map、weakset、weakmap) collectionhandlers  // (object、array) basehandlers  const proxy = new proxy(    target,    targettype === targettype.collection ? collectionhandlers : basehandlers  )  proxymap.set(target, proxy)  return proxy}
首先我们看到createreactiveobject接收了五个参数
  target: target,  isreadonly: boolean,  basehandlers: proxyhandler<any>,  collectionhandlers: proxyhandler<any>,  proxymap: weakmap<target, any>
target 目标对象
isreadonly 是否只读
basehandlers 基本类型的 handlers 处理数组,对象
collectionhandlers 处理 set、map、weakset、weakmap
proxymap weakmap数据结构存储副作用函数
这里主要是通过reactiveflags.raw和reactiveflags.is_reactive判断是否是响应式数据,若是则直接返回该对象
 if (    target[reactiveflags.raw] &&    !(isreadonly && target[reactiveflags.is_reactive])  ) {    return target  }
对于已经是proxy的,则直接从weakmap数据结构中取出这个proxy对象并返回
  const existingproxy = proxymap.get(target)  if (existingproxy) {    return existingproxy  }
这里则是校验了一下当前target的类型是不是object、array、map、set、weakmap、weakset,如果都不是则直接返回该对象,不做响应式处理
 // 只对targettypemap类型白名单中的类型进行响应式处理  const targettype = gettargettype(target)  if (targettype === targettype.invalid) {    return target  }
校验类型的逻辑
function gettargettype(value: target) {  return value[reactiveflags.skip] || !object.isextensible(value)    ? targettype.invalid    : targettypemap(torawtype(value))}function targettypemap(rawtype: string) {  switch (rawtype) {    case 'object':    case 'array':      return targettype.common    case 'map':    case 'set':    case 'weakmap':    case 'weakset':      return targettype.collection    default:      return targettype.invalid  }}
所有的前置校验完后,就可以使用proxy 代理target对象了
这里使用了一个三目运算符通过targettype.collection来执行不同的处理逻辑
(set、map、weakset、weakmap) 使用 collectionhandlers(object、array) 使用 basehandlers// proxy 代理 target  // (set、map、weakset、weakmap) collectionhandlers  // (object、array) basehandlers  const proxy = new proxy(    target,    targettype === targettype.collection ? collectionhandlers : basehandlers  )  proxymap.set(target, proxy)  return proxy
现在对createreactiveobject的执行逻辑是不是就很清晰了
到这里还没有结束,createreactiveobject中最后proxy是如何去代理target的呢?这里我们用basehandlers举例,深入basehandlers的内部去看看
basehandlers
源码地址:packages/reactivity/basehandlers.ts
在reactive.ts中我们可以看到一共引入了四种 handler
import {  mutablehandlers,  readonlyhandlers,  shallowreactivehandlers,  shallowreadonlyhandlers} from './basehandlers'
mutablehandlers 可变处理readonlyhandlers 只读处理shallowreactivehandlers 浅观察处理(只观察目标对象的第一层属性)shallowreadonlyhandlers 浅观察 && 只读我们以mutablehandlers为例
// 可变处理// const get = /*#__pure__*/ creategetter()// const set = /*#__pure__*/ createsetter()// get、has、ownkeys 会触发依赖收集 track()// set、deleteproperty 会触发更新 trigger()export const mutablehandlers: proxyhandler<object> = {  get,                  // 用于拦截对象的读取属性操作  set,                  // 用于拦截对象的设置属性操作  deleteproperty,       // 用于拦截对象的删除属性操作  has,                  // 检查一个对象是否拥有某个属性  ownkeys               // 针对 getownpropertynames,  getownpropertysymbols, keys 的代理方法}
这里的get和set分别对应着creategetter()、createsetter()
creategetter()
先上完整版代码
/** * 用于拦截对象的读取属性操作 * @param isreadonly 是否只读 * @param shallow 是否浅观察 * @returns  */function creategetter(isreadonly = false, shallow = false) {  /**   * @param target 目标对象   * @param key 需要获取的值的键值   * @param receiver 如果遇到 setter,receiver 则为setter调用时的this值   */  return function get(target: target, key: string | symbol, receiver: object) {    // reactiveflags 是在reactive中声明的枚举值,如果key是枚举值则直接返回对应的布尔值    if (key === reactiveflags.is_reactive) {      return !isreadonly    } else if (key === reactiveflags.is_readonly) {      return isreadonly    } else if (      // 如果key是raw  receiver 指向调用者,则直接返回目标对象。      // 这里判断是为了保证触发拦截 handle 的是 proxy 本身而不是 proxy 的继承者      // 触发拦的两种方式:一是访问 proxy 对象本身的属性,二是访问对象原型链上有 proxy 对象的对象的属性,因为查询会沿着原型链向下找      key === reactiveflags.raw &&      receiver ===        (isreadonly          ? shallow            ? shallowreadonlymap            : readonlymap          : shallow          ? shallowreactivemap          : reactivemap        ).get(target)    ) {      return target    }    const targetisarray = isarray(target)    // 如果目标对象 不为只读、是数组、key属于arrayinstrumentations:['includes', 'indexof', 'lastindexof']方法之一,即触发了这三个方法之一    if (!isreadonly && targetisarray && hasown(arrayinstrumentations, key)) {      // 通过 proxy 调用,arrayinstrumentations[key]的this一定指向 proxy      return reflect.get(arrayinstrumentations, key, receiver)    }    const res = reflect.get(target, key, receiver)    // 如果 key 是 symbol 内置方法,或者访问的是原型对象__proto__,直接返回结果,不收集依赖    if (issymbol(key) ? builtinsymbols.has(key) : isnontrackablekeys(key)) {      return res    }    // 不是只读类型的 target 就收集依赖。因为只读类型不会变化,无法触发 setter,也就会触发更新    if (!isreadonly) {      track(target, trackoptypes.get, key)    }    // 如果是浅观察,不做递归转化,就是说对象有属性值还是对象的话不递归调用 reactive()    if (shallow) {      return res    }    // 如果get的结果是ref    if (isref(res)) {      // ref unwrapping - does not apply for array + integer key.      // 返回 ref.value,数组除外      const shouldunwrap = !targetisarray || !isintegerkey(key)      return shouldunwrap ? res.value : res    }    // 由于 proxy 只能代理一层,如果子元素是对象,需要递归继续代理    if (isobject(res)) {      // convert returned value into a proxy as well. we do the isobject check      // here to avoid invalid value warning. also need to lazy access readonly      // and reactive here to avoid circular dependency.      return isreadonly ? readonly(res) : reactive(res)    }    return res  }}
看着长,最终就是track()依赖收集
track()依赖收集内容过多,和trigger()触发更新一起,单开一篇文章
createsetter()
/** * 拦截对象的设置属性操作 * @param shallow 是否是浅观察 * @returns  */function createsetter(shallow = false) {  /**   * @param target 目标对象   * @param key 设置的属性名称   * @param value 要改变的属性值   * @param receiver 如果遇到setter,receiver则为setter调用时的this值   */  return function set(    target: object,    key: string | symbol,    value: unknown,    receiver: object  ): boolean {    let oldvalue = (target as any)[key]    // 如果模式不是浅观察模式    if (!shallow) {      // 拿新值和老值的原始值,因为新传入的值可能是响应式数据,如果直接和 target 上原始值比较是没有意义的      value = toraw(value)      oldvalue = toraw(oldvalue)      // 目标对象不是数组,旧值是ref,新值不是ref,则直接赋值,这里提到ref      if (!isarray(target) && isref(oldvalue) && !isref(value)) {        oldvalue.value = value        return true      }    } else {      // in shallow mode, objects are set as-is regardless of reactive or not    }    // 检查对象是否有这个属性    const hadkey =      isarray(target) && isintegerkey(key)        ? number(key)   isobject(value) ? reactive(value) : value
trackrefvalue
ref的依赖收集方法
export function trackrefvalue(ref: refbase<any>) {  if (istracking()) {    ref = toraw(ref)    if (!ref.dep) {      ref.dep = createdep()    }    if (__dev__) {      trackeffects(ref.dep, {        target: ref,        type: trackoptypes.get,        key: 'value'      })    } else {      trackeffects(ref.dep)    }  }}
triggerrefvalue
ref的派发更新方法
export function triggerrefvalue(ref: refbase<any>, newval?: any) {  ref = toraw(ref)  if (ref.dep) {    if (__dev__) {      triggereffects(ref.dep, {        target: ref,        type: triggeroptypes.set,        key: 'value',        newvalue: newval      })    } else {      triggereffects(ref.dep)    }  }}
总结看完reactive和ref源码,相信对本文一开始的几个问题也都有了答案,这里也总结了几个问题:
问:ref的底层逻辑是什么,具体是如何实现的答:ref底层会通过 new refimpl()来创造ref数据,在new refimpl()会首先给数据添加__v_isref只读属性用来标识ref数据。而后判断传入的值是否是对象,如果是对象则使用toreactive()处理成reactive,并将值赋给refimpl()的value属性上。在访问和设置ref数据的value时会分别触发依赖收集和派发更新流程。
问:ref底层是否会使用reactive处理数据答:refimpl中非浅观察会调用toreactive()方法处理数据,toreactive()中会先判断传入的值是不是一个对象,如果是对象则使用reactive进行处理,不是则直接返回值本身。
问:为什么已经有了reactive还需要在设计一个ref呢?答: 因为vue3响应式方案使用的是proxy,而proxy的代理目标必须是非原始值,没有任何方式能去拦截对原始值的操作,所以就需要一层对象作为包裹,间接实现原始值的响应式方案。
问:为什么ref数据必须要有个value属性,访问ref数据必须要通过.value的方式呢?答:这是因为要解决响应式丢失的问题,举个例子:
// obj是响应式数据const obj = reactive({ foo: 1, bar: 2 })// newobj 对象下具有与 obj对象同名的属性,并且每个属性值都是一个对象// 该对象具有一个访问器属性 value,当读取 value的值时,其实读取的是 obj 对象下相应的属性值 const newobj = {    foo: {        get value() {            return obj.foo        }    },    bar: {        get value() {            return obj.bar        }    }}effect(() => {    // 在副作用函数内通过新对象 newobj 读取 foo 的属性值    console.log(newobj.foo)})// 正常触发响应obj.foo = 100
可以看到,在现在的newobj对象下,具有与obj对象同名的属性,而且每个属性的值都是一个对象,例如foo 属性的值是:
{    get value() {        return obj.foo    }}
该对象有一个访问器属性value,当读取value的值时,最终读取的是响应式数据obj下的同名属性值。也就是说,当在副作用函数内读取newobj.foo时,等价于间接读取了obj.foo的值。这样响应式数据就能够与副作用函数建立响应联系
(学习视频分享:web前端开发、编程基础视频)
以上就是详解vue3中reactive和ref的区别(源码解析)的详细内容。
其它类似信息

推荐信息