美文网首页
通过源码深入了解 vue3 的 ref vs reactive

通过源码深入了解 vue3 的 ref vs reactive

作者: VioletJack | 来源:发表于2023-03-30 16:41 被阅读0次

    最近刚开始用 vue3,其中组合式 API 的 ref 和 reactive 两者让我有些困惑:

    • 它们都返回响应式的数据,那么它们两者的区别在哪?
    • 它们的原理是怎样的?

    于是就有了看源码的想法,源码是直接从 https://unpkg.com/vue@3/dist/vue.global.js 上保存下来的。后续就可以在源码上面调试学习啦。

    结论

    先说结论!(我知道很多朋友不喜欢看过程,只要结论。比如我自己 0,0)

    • 参数
      • ref() 函数的参数既可以是原始类型(string、number、boolean)也可以是对象类型(对象、数组、Set、Map)。
      • 如果将一个对象类型的数据赋值给 ref() 函数,这个对象将通过 reactive() 转为具有深层次响应式的对象。
      • reactive() 函数只有在接收对象类型是响应式的。它也可以接收 ref 函数返回的对象,不过如果需要解构就需要使用对象包裹。如 { a: refObj }
    • 返回值
      • ref() 接受一个内部值,并返回一个响应式的、可更改的 ref 对象。该对象通过内部值 .value 的 setter 和 getter 来获取和修改内部数据,如 count.value = 4
      • reactive() 函数返回一个对象的深层次响应式代理。

    他们最终的目的都是能响应式渲染模板(即数据变化后网页内容也随之变化)。

    ref

    源码

    先看下 ref 的源码,ref() 函数执行了 createRef() 函数,而 createRef() 中实例化了 RefImpl 类。

    function ref(value) {
      return createRef(value, false)
    }
    
    function createRef(rawValue, shallow) {
      // 如果已经是 ref 则直接返回
      if (isRef(rawValue)) {
        return rawValue
      }
      return new RefImpl(rawValue, shallow)
    }
    

    RefImpl 类中除了构造函数,只有一个 value 内部值的 setter 和 getter 函数。在构造函数中 _rawValue 是原始数据,而 _value 是响应数据(如果数据是对象类型则为 Proxy)。

    那么 _value 是如何来的?如果不是浅层响应式,则会调用 toReactive 函数。

    class RefImpl {
      constructor(value, __v_isShallow) {
        this.__v_isShallow = __v_isShallow
        this.dep = undefined
        this.__v_isRef = true
        this._rawValue = __v_isShallow ? value : toRaw(value)
        this._value = __v_isShallow ? value : toReactive(value)
      }
      get value() {
        trackRefValue(this)
        return this._value
      }
      set value(newVal) {
        const useDirectValue =
          this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
        newVal = useDirectValue ? newVal : toRaw(newVal)
        if (hasChanged(newVal, this._rawValue)) {
          this._rawValue = newVal
          this._value = useDirectValue ? newVal : toReactive(newVal)
          triggerRefValue(this, newVal)
        }
      }
    }
    

    toRaw() 函数中,递归获取数据的原始数据。(reactive() 函数返回的代理对象中带有 __v_raw 标签,它会让 getter 函数返回原始数据)

    function toRaw(observed) {
      const raw = observed && observed['__v_raw' /* ReactiveFlags.RAW */]
      return raw ? toRaw(raw) : observed
    }
    

    toReactive() 函数中,就可以看到已经使用 reactive() 函数的逻辑了。

    如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。

    const toReactive = (value) => (isObject(value) ? reactive(value) : value)
    

    顺便瞅一眼 isObject() 函数,对象类型的判定就是 typeof val === 'object'。不过由于 JavaScript 的缺陷,所以 typeof null 也是 object,需要排除掉。

    const isObject = (val) => val !== null && typeof val === 'object'
    

    小实验

    实验出真知

    const a = ref('123')
    a.value += '456'
    // '123456'
    const b = ref(6)
    b.value += 8
    // 14
    const c = ref(false)
    c.value = !c.value
    // true
    
    const r3 = ref(false)
    r3.value = true
    r3.value = 'oh li gei' // value 是不限定类型的
    // oh li gei
    
    const d = ref(null)
    // null
    const e = ref(undefined)
    // undefined
    const f = ref(Symbol())
    // Symbol()
    
    // 这里打赢 ref 返回的对象
    const g = ref({ a: [1, 2, 3] })
    // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Array(4), _value: Proxy}
    const h = ref([3, 4, 5, 6])
    // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Array(4), _value: Proxy}
    const i = ref(new Set())
    // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Set(1), _value: Proxy}
    
    // 验证 toRaw 函数的 __v_raw 属性
    const x = reactive({ a: 1, b: { c: { d: 3 } } })
    const y = ref(x)
    console.log('x', x.__v_raw) // { a: 1, b: { c: { d: 3 } } }
    console.log('y', y.__v_raw) // undefined
    
    • 对于字符串、数字、布尔类型来说,ref 函数可以让这些数据变成响应式的。
    • 对于 null、undefined、symbol 这类特殊数据,ref 函数返回值还是其本身,无意义。
    • 对于对象类型数据,正如源码所说,对数据使用了 reactive() 来进行深层响应式代理。从 ref 返回的对象可以看出,_rawValue 是原始数据,而 _value 是数据的代理。
    • reactive 函数返回的代理对象中带有 __v_raw 标签,会返回原始数据

    reactive

    源码

    reactive()

    reactive() 函数除了判断只读外就只是调用了 createReactiveObject() 函数。

    createReactiveObject() 函数中,排除了各种不需要代理的情况,并根据数据类型不同进行不同的代理逻辑处理。最后将代理结构记录到一个 Map 中。

    function reactive(target) {
      // 如果 target 是 Readonly 的代理,返回自身
      if (isReadonly(target)) {
        return target
      }
      return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers,
        reactiveMap,
      )
    }
    
    function createReactiveObject(
      target,
      isReadonly,
      baseHandlers,
      collectionHandlers,
      proxyMap,
    ) {
      // target 不是对象类型,返回自身
      if (!isObject(target)) {
        {
          console.warn(`value cannot be made reactive: ${String(target)}`)
        }
        return target
      }
      // target 已经是代理,返回自身
      if (
        target['__v_raw' /* ReactiveFlags.RAW */] &&
        !(isReadonly && target['__v_isReactive' /* ReactiveFlags.IS_REACTIVE */])
      ) {
        return target
      }
      // target 已经有响应的代理,返回代理
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      /**
       * 判断 target 数据类型
       * 0 无效,直接返回
       * 1 COMMON 类型,使用 baseHandlers 代理配置
       * 2 COLLECTION 类型,使用 collectionHandlers 代理配置
       */
      const targetType = getTargetType(target)
      if (targetType === 0 /* TargetType.INVALID */) {
        return target
      }
      const proxy = new Proxy(
        target,
        targetType === 2 /* TargetType.COLLECTION */
          ? collectionHandlers
          : baseHandlers,
      )
      // 记录代理关系
      proxyMap.set(target, proxy)
      return proxy
    }
    

    数据类型判断

    判断数据类型的代码如下,根据不同的数据分为:

    • 0 无效数据类型,不进行代理返回自身。
    • 1 普通对象类型
    • 2 收集器类型

    类型的获取是通过 Object.prototype.toString.call(target) 获取到 '[object Set]' 这类字符串,并截取 Set 这段有效字符串返回。

    function getTargetType(value) {
      return value['__v_skip' /* ReactiveFlags.SKIP */] ||
        !Object.isExtensible(value)
        ? 0 /* TargetType.INVALID */
        : targetTypeMap(toRawType(value))
    }
    
    function targetTypeMap(rawType) {
      switch (rawType) {
        case 'Object':
        case 'Array':
          return 1 /* TargetType.COMMON */
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
          return 2 /* TargetType.COLLECTION */
        default:
          return 0 /* TargetType.INVALID */
      }
    }
    

    普通类数据代理

    reactive() 函数中可以看到,代理配置分别使用的是 mutableHandlers 和 mutableCollectionHandlers。

    普通类型的代理配置 mutableHandlers 如下。这里代码量较大,暂时就只贴出 getter 和 settter 函数。

    const mutableHandlers = {
      get: get$1, // get 方法用于拦截某个属性的读取操作
      set: set$1, // set 方法用来拦截某个属性的赋值操作
      deleteProperty, // deleteProperty 方法用于拦截 delete 操作
      has: has$1, // has() 方法用来拦截HasProperty操作
      ownKeys, //  ownKeys() 方法用来拦截对象自身属性的读取操作
    }
    

    在 createGetter 中的处理逻辑如下:

    • 如果是标签 __v_raw 等则返回响应的值;
    • 如果是数组 API 的关键字 inclueds push 等就用 arrayInstrumentations 进行处理;(所以可以在 reactive 的返回值中直接使用数组 API arr.push()
    • 通过 Reflect.get() 获取到目标返回值。
    • 如果返回值是一个对象,且不是只读数据。那么就以递归的方式对这个子对象使用 reactive() 函数继续绑定响应式代理。(即深层响应式转换)
    • 返回最终结果。
    const get$1 = /*#__PURE__*/ createGetter()
    
    function createGetter(isReadonly = false, shallow = false) {
      return function get(target, key, receiver) {
        if (key === '__v_isReactive' /* ReactiveFlags.IS_REACTIVE */) {
          return !isReadonly
        } else if (key === '__v_isReadonly' /* ReactiveFlags.IS_READONLY */) {
          return isReadonly
        } else if (key === '__v_isShallow' /* ReactiveFlags.IS_SHALLOW */) {
          return shallow
        } else if (
          key === '__v_raw' /* ReactiveFlags.RAW */ &&
          receiver ===
            (isReadonly
              ? shallow
                ? shallowReadonlyMap
                : readonlyMap
              : shallow
              ? shallowReactiveMap
              : reactiveMap
            ).get(target)
        ) {
          return target
        }
        const targetIsArray = isArray(target)
        if (!isReadonly) {
          if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver)
          }
          if (key === 'hasOwnProperty') {
            return hasOwnProperty
          }
        }
        const res = Reflect.get(target, key, receiver) // Reflect.get 方法查找并返回target对象的name属性
        if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
          return res
        }
        if (!isReadonly) {
          track(target, 'get' /* TrackOpTypes.GET */, key)
        }
        if (shallow) {
          return res
        }
        if (isRef(res)) {
          // ref 解构取值
          return targetIsArray && isIntegerKey(key) ? res : res.value
        }
        if (isObject(res)) {
          // 递归生成响应式代理
          return isReadonly ? readonly(res) : reactive(res)
        }
        return res
      }
    }
    

    在 set 函数中

    • 首先是调用 toRaw() 函数将 value 和 oldValue 递归从代理变为原始数据。原理大致如 reactive({ a: 1 }).__v_raw // output: { a: 1}
    • 如果 oldValue 是 ref() 函数返回的,则进行解构赋值。
    • 通过 Reflect.set() 函数对代理目标 target 进行赋值。
    const set$1 = /*#__PURE__*/ createSetter()
    
    function createSetter(shallow = false) {
      return function set(target, key, value, receiver) {
        let oldValue = target[key]
        // 不需要更新的情况
        if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
          return false
        }
        if (!shallow) {
          if (!isShallow(value) && !isReadonly(value)) {
            oldValue = toRaw(oldValue) // 递归转为原始数据
            value = toRaw(value) // 递归转为原始数据
          }
          // ref 解构赋值
          if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
            oldValue.value = value
            return true
          }
        }
        const hadKey =
          isArray(target) && isIntegerKey(key)
            ? Number(key) < target.length
            : hasOwn(target, key)
        const result = Reflect.set(target, key, value, receiver) // Reflect.set 方法设置 target 对象的 name 属性等于value。
        // don't trigger if target is something up in the prototype chain of original
        if (target === toRaw(receiver)) {
          if (!hadKey) {
            trigger(target, 'add' /* TriggerOpTypes.ADD */, key, value)
          } else if (hasChanged(value, oldValue)) {
            trigger(target, 'set' /* TriggerOpTypes.SET */, key, value, oldValue)
          }
        }
        return result
      }
    }
    

    收集器类数据代理

    收集器类型的代理配置只有一个 getter 函数,它对收集器类型数据的 API 进行了定义。

    如果调用 set.add() map.get() 这类 API,就会去调用 instrumentations 对象中相应的函数。否则就返回代理目标 target 自身。

    const mutableCollectionHandlers = {
      get: /*#__PURE__*/ createInstrumentationGetter(false, false),
    }
    
    function createInstrumentationGetter(isReadonly, shallow) {
      const instrumentations = shallow
        ? isReadonly
          ? shallowReadonlyInstrumentations
          : shallowInstrumentations
        : isReadonly
        ? readonlyInstrumentations
        : mutableInstrumentations
      return (target, key, receiver) => {
        if (key === '__v_isReactive' /* ReactiveFlags.IS_REACTIVE */) {
          return !isReadonly
        } else if (key === '__v_isReadonly' /* ReactiveFlags.IS_READONLY */) {
          return isReadonly
        } else if (key === '__v_raw' /* ReactiveFlags.RAW */) {
          return target
        }
        return Reflect.get(
          hasOwn(instrumentations, key) && key in target
            ? instrumentations
            : target,
          key,
          receiver,
        )
      }
    }
    
    const mutableInstrumentations = {
      get(key) {
        return get(this, key)
      },
      get size() {
        return size(this)
      },
      has,
      add,
      set,
      delete: deleteEntry,
      clear,
      forEach: createForEach(false, false),
    }
    
    function add(value) {
      value = toRaw(value) // 去代理
      const target = toRaw(this) // 去代理
      const proto = getProto(target)
      const hadKey = proto.has.call(target, value)
      if (!hadKey) {
        target.add(value) // 执行原生函数
        trigger(target, 'add' /* TriggerOpTypes.ADD */, value, value)
      }
      return this
    }
    
    function set(key, value) {
      value = toRaw(value)
      const target = toRaw(this)
      const { has, get } = getProto(target)
      let hadKey = has.call(target, key)
      if (!hadKey) {
        key = toRaw(key)
        hadKey = has.call(target, key)
      } else {
        checkIdentityKeys(target, has, key)
      }
      const oldValue = get.call(target, key)
      target.set(key, value)
      if (!hadKey) {
        trigger(target, 'add' /* TriggerOpTypes.ADD */, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, 'set' /* TriggerOpTypes.SET */, key, value, oldValue)
      }
      return this
    }
    

    源码小结

    • reactive 函数只对 Object Array Map Set WeakMap WeakSet 类型的数据生效,且分为了两种处理方式。
    • reactive 函数会排除各种不符合条件的数据,返回数据本身。
    • reactive 函数是通过代理 Proxy 实现数据的存取的。
    • reactive 中的 __v_raw __v_isShallow 并不是属性值,而是判断标签。会根据标签返回相应结果。
    • reactive 对于 ref 对象的解构其实就是在 get 的时候取 .value 值,而在 set 的时候将值传给 .value
    • reactive 对于 Set、Map 这类数据,仅提供了 getter 方法。如果调用这类数据 API 函数,vue 在做了数据处理后会去调用它的原生函数。如果是获取数据内容,则直接返回数据本身。
    • 对象类型数据想要变成响应式的,就必须用 reactive 函数代理。
    • 上面代码中用到了 ES6 的 Reflect 和 Proxy ,关于它们的更多内容可以访问 Proxy - ECMAScript 6 入门Reflect - ECMAScript 6 入门 了解。

    小实验

    以下写法 reactive() 返回值是其自身,但不是响应式的。而且 vue 会发出警告:value cannot be made reactive: 123

    var a = reactive('123')
    // 123
    
    function add() {
      a += '456' // 变量 a 有变化,但是 HTML 无变化
    }
    
    var b = reactive(6)
    b += 8
    // 16
    
    const c = reactive(false)
    // false
    setTimeout(() => {
      c = true // c 变为 true,但是 HTML 无变化
    }, 1000)
    
    const d = reactive(null)
    // null
    
    const e = reactive(undefined)
    // undefined
    
    const f = reactive(Symbol())
    // Synbol()
    

    下面这些情况可以正常使用 reactive() 函数。

    const g = reactive({ a: 1, b: { c: 3 } })
    g.a++
    // Proxy: { a: 2, b: { c: 3 } }
    setInterval(() => {
      // 网页会每秒变化数据
      g.a++
      g.b.c += 2
    }, 1000)
    
    const h = reactive([3, 4, 5, 6])
    h.push(8)
    // Proxy: {0: 3, 1: 4, 2: 5, 3: 6, 4: 8}
    
    const i = reactive(new Set())
    i.add('2')
    i.add({ b: 3 })
    i.add(321)
    // Proxy: { "Set(4)": [ "2", { "b": 3 }, 321 ] }
    setTimeout(() => {
      i.add(null)
      // Proxy: { "Set(4)": [ "2", { "b": 3 }, 321, null ] }
    }, 1000)
    
    const j = reactive(new Map())
    j.set('yo', 'good')
    j.set('x', { b: 3 })
    setTimeout(() => {
      j.delete('x')
      // Proxy: { "Map(1)": { "yo =>": "good" } }
    }, 1000)
    

    既然 reactive 函数可以解构 ref,那么进行一些尝试。以下是官网的原话。

    值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

    但在实际实验下来发现这句话并不严谨。

    var a = ref(new Map())
    var b = reactive(a)
    a.value.set('a', 1)
    b.value.set('b', 2) // 需要加上 value
    // { "Map(2)": { "a =>": 1, "b =>": 2 } }
    
    console.log('a === b', a.value === b.value) // true
    
    var a = ref(new Set())
    var b = reactive({ a })
    a.value.add(1)
    b.a.add(2) // ! 被对象包裹的 Map 是可以被解构的
    // { "Map(2)": { "a =>": 1, "b =>": 2 } }
    console.log('a === b', a.value === b.a) // true
    

    尝试了 Object、Array、Set 后发现,被 ref 函数返回的对象如果直接传给 reactive 函数是不会被解构的,但如果 ref 对象被对象符号包裹 reactive({ ref: ref(new Set()) }) 的情况下是可以被解构的。

    最后

    本文我们先提出了 ref 和 reactive 的疑问,然后给出结果。再从源码层面逐步分析了 ref 和 reactive 函数。也算是基本掌握其原理了。

    关于 ref 和 reactive 的内容就这么多啦,希望对你有用。

    本文正在参加「金石计划」

    相关文章

      网友评论

          本文标题:通过源码深入了解 vue3 的 ref vs reactive

          本文链接:https://www.haomeiwen.com/subject/pklcddtx.html