最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 深入理解 Vue3 的响应式

    正文概述 掘金(CUndH)   2020-12-05   306

    深入理解 Vue3 的响应式

    0 前言

    随着 Vue3 的登台,各大博客论坛铺天盖地的涌来各种文章。我们组也是率先把 Vue3 应用到了工作中,刮起了一波学习热潮。上周组内一位大佬的分享也让我受益良多,同时也打开了我的好奇心,那么就抱着问到底的精神来看看让我疑惑的问题。

    1 ref 和 reactive 的关系

    Vue3 的文档中主要提供了两种比较常用的方法来把你的数据转换成响应式,refreactive。这两种方法的区别就在于 refreactive 的基础上对数据进行了二次分装,需要以 .value的形式获取值,可以从源码中看到:

    class RefImpl<T> {
      private _value: T
    
      public readonly __v_isRef = true
    
      constructor(private _rawValue: T, public readonly _shallow = false) {
        this._value = _shallow ? _rawValue : convert(_rawValue)
      }
    
      get value() {
        track(toRaw(this), TrackOpTypes.GET, 'value')
        return this._value
      }
    
      set value(newVal) {
        if (hasChanged(toRaw(newVal), this._rawValue)) {
          this._rawValue = newVal
          this._value = this._shallow ? newVal : convert(newVal)
          trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
        }
      }
    }
    
    const convert = <T extends unknown>(val: T): T =>
      isObject(val) ? reactive(val) : val
    

    重点在于 convert 方法,能看出最后还是会通过 reactive 方法来转换成响应式。也知道了因为 Proxy 不能监听简单数据类型所以 Vue 在基础数据类型上又加了一层对象来实现响应式。

    2. reactive 做了什么

    既然 ref 只是 reactive的一层伪装,那我们就直接看看 reactive 做了什么吧:

    export const reactiveMap = new WeakMap<Target, any>()
    export const readonlyMap = new WeakMap<Target, any>()
    
    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
      }
      return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers
      )
    }
    
    
    function createReactiveObject(
      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<any>
    ) {
      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
      if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
      ) {
        return target
      }
      // target already has corresponding Proxy
      const proxyMap = isReadonly ? readonlyMap : reactiveMap
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      // only a whitelist of value types can be observed.
      const targetType = getTargetType(target)
      if (targetType === TargetType.INVALID) {
        return target
      }
      const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
      )
      proxyMap.set(target, proxy)
      return proxy
    }
    

    Vue 首先判断了一下如果 target 是只读对象那么直接返回。

    然后就进入了 createReactiveObject 方法,代码很清晰,先判断了是否已经绑定过,没有就绑定。最后,通过判断目标的类型,如果是普通的对象 Object 或 Array,处理器对象就使用 baseHandlers,如果是 Set, Map, WeakMap, WeakSet 中的一个,就使用 collectionHandlers

    这里有一个小知识点,为什么 Vue 存储使用的是 WeakMap ?WeakMap 的键值必须是对象,而且 WeakMap 的键值是不可枚举的,是弱引用。方便垃圾回收。

    弱引用对应的当然就是强引用了,强引用有一种计数机制。举个例子当一个对象被创建时,计数为1,每有一个变量引用该对象,计数都会加1,只有当计数为0时,对象才会被垃圾回收。所以一旦造成循环引用等问题,就会造成内存泄漏。而 WeakMap 当它的键所指对象没有被其他地方引用时,就会被垃圾回收了。

    3. baseHandlers

    baseHandlers 针对不同类型的响应式做了不同的处理,我们重点看一下完全响应式是如何处理的:

    export const mutableHandlers: ProxyHandler<object> = {
     get,
     set,
     deleteProperty,
     has,
     ownKeys
    }
    

    从 Vue2 中我们知道,如果 Vue 想要在数据更新时通知界面渲染那肯定要收集依赖,而在数据被修改后触发依赖,那么我们就知道:get、has、ownKeys 是收集依赖,set、deleteProperty 是触发依赖。

    function createGetter(isReadonly = false, shallow = false) {
      return function get(target: Target, key: string | symbol, receiver: object) {
        if (key === ReactiveFlags.IS_REACTIVE) {
          return !isReadonly
        } else if (key === ReactiveFlags.IS_READONLY) {
          return isReadonly
        } else if (
          key === ReactiveFlags.RAW &&
          receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
        ) {
          return target
        }
    
        const targetIsArray = isArray(target)
    
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
          return Reflect.get(arrayInstrumentations, key, receiver)
        }
    
        const res = Reflect.get(target, key, receiver)
    
        if (
          isSymbol(key)
            ? builtInSymbols.has(key as symbol)
            : key === `__proto__` || key === `__v_isRef`
        ) {
          return res
        }
    
        if (!isReadonly) {
          // 只读不需要收集
          track(target, TrackOpTypes.GET, key)
        }
    
        if (shallow) {
          // 浅层响应,直接返回
          return res
        }
    
        if (isRef(res)) {
          // ref unwrapping - does not apply for Array + integer key.
          const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
          return shouldUnwrap ? res.value : res
        }
    
        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
      }
    }
    

    代码并不难理解,加了点注释。其中 arrayInstrumentations 是用来收集数组的依赖的,我们来看看具体怎么做的:

    const arrayInstrumentations: Record<string, Function> = {}
    // instrument identity-sensitive Array methods to account for possible reactive
    // values
    ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
      const method = Array.prototype[key] as any
      arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
        const arr = toRaw(this)
        for (let i = 0, l = this.length; i < l; i++) {
          track(arr, TrackOpTypes.GET, i + '')
        }
        // we run the method using the original args first (which may be reactive)
        const res = method.apply(arr, args)
        if (res === -1 || res === false) {
          // if that didn't work, run it again using raw values.
          return method.apply(arr, args.map(toRaw))
        } else {
          return res
        }
      }
    })
    

    在这里 Vue 通过封装了数组方法来达到收集依赖的目的。

    再回到之前的代码,代码中用到了 Reflect.get。Reflect 是一个内置对象,它与 Proxy 几乎共用方法。那为什么 Vue 要多此一举用 Reflect 呢? 我们来看个例子:

    const data = new Proxy([1,2,3], {
        get(target, key) {
            console.log('get', key)
            return target[key];
        },
        set(target, key, value) {
            console.log('set', key, value)
            return true;
        }
    })
    data.push(4)
    

    这是一个普通写法,set 方法为什么要返回一个 true,如果不返回 true 则会抛出一个 typeError。看上去是不是不太优雅,那么我们用 Reflect 改造一下。

    const data = new Proxy([1, 2, 3], {
      get(target, key, receiver) {
        console.log('get', key);
        return Reflect.get(target, key, receiver);
      },
      set(target, key, value, receiver) {
        console.log('set', key, value);
        return Reflect.set(target, key, value, receiver);
      },
    });
    data.push(4);
    

    改造完后,我们来看看打印结果,打印出来是这样的:

    get push
    get length
    set 3 4
    set length 4
    

    这样达到的效果是一样的,不过还是有点问题。你会发现打印了两次 get 和 set,这是因为在 push 后,不仅改变了数组内的元素,同时操作了数组的 length 产生了两次 get 和 set,那么 Vue 是如何解决这种重复问题的?

    export const hasOwn = (
      val: object,
      key: string | symbol
    ): key is keyof typeof val => hasOwnProperty.call(val, key)
    ...
    function createSetter(shallow = false) {
      return function set(
        target: object,
        key: string | symbol,
        value: unknown,
        receiver: object
      ): boolean {
        const oldValue = (target as any)[key]
        if (!shallow) {
          value = toRaw(value)
          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) < target.length
            : hasOwn(target, key)
        const result = Reflect.set(target, key, value, receiver)
        // don't trigger if target is something up in the prototype chain of original
        if (target === toRaw(receiver)) {
          if (!hadKey) {
          	// key 不存在说明是新增操作,触发依赖
            trigger(target, TriggerOpTypes.ADD, key, value)
          } else if (hasChanged(value, oldValue)) {
          	// 新旧值不相等,触发依赖
            // 同时也防止了刚才提到的 push 后触发了两次依赖的问题,新旧值相同则不触发依赖
            trigger(target, TriggerOpTypes.SET, key, value, oldValue)
          }
        }
        return result
      }
    }
    

    Vue 通过判断 key 是否为 target 自身属性,以及设置 val 是否跟 target[key] 相等来阻止重复的响应。

    继续看 set 和 get 方法中,用到了 trigger 和 track 方法。看一下源码中他们都来自于一个叫 effect 的文件,那么 effect 是做什么用的?

    export function effect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions = EMPTY_OBJ
    ): ReactiveEffect<T> {
      if (isEffect(fn)) {
        fn = fn.raw
      }
      const effect = createReactiveEffect(fn, options)
      if (!options.lazy) {
        effect()
      }
      return effect
    }
    

    其中创建 effect 的其实是 createReactiveEffect 方法,我们来看看这个方法:

    function createReactiveEffect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions
    ): ReactiveEffect<T> {
      const effect = function reactiveEffect(): unknown {
        if (!effect.active) {
          return options.scheduler ? undefined : fn()
        }
        if (!effectStack.includes(effect)) {
          // 清除依赖
          cleanup(effect)
          try {
            enableTracking()
            effectStack.push(effect)
            // 
            activeEffect = effect
            return fn()
          } finally {
            effectStack.pop()
            resetTracking()
            activeEffect = effectStack[effectStack.length - 1]
          }
        }
      } as ReactiveEffect
      effect.id = uid++
      effect.allowRecurse = !!options.allowRecurse
      effect._isEffect = true
      effect.active = true	// 判断当前 effect 是否活跃
      effect.raw = fn
      effect.deps = []
      effect.options = options
      return effect
    }
    

    所以真正的依赖函数是 activeEffect,那我们继续来看看 trigger 和 track 做了什么

    export function track(target: object, type: TrackOpTypes, key: unknown) {
      // activeEffect 为空代表没有依赖,直接返回
      if (!shouldTrack || activeEffect === undefined) {
        return
      }
      // targetMap 是一个 WeakMap,用于收集依赖
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
      }
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, (dep = new Set()))
      }
      if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
        if (__DEV__ && activeEffect.options.onTrack) {
          activeEffect.options.onTrack({
            effect: activeEffect,
            target,
            type,
            key
          })
        }
      }
    }
    
    export function trigger(
      target: object,
      type: TriggerOpTypes,
      key?: unknown,
      newValue?: unknown,
      oldValue?: unknown,
      oldTarget?: Map<unknown, unknown> | Set<unknown>
    ) {
      const depsMap = targetMap.get(target)
      // 没有收集依赖的话直接返回
      if (!depsMap) {
        // never been tracked
        return
      }
    
      // 依赖队列,这里的依赖队列是 Set 类型避免重复
      const effects = new Set<ReactiveEffect>()
      const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
        if (effectsToAdd) {
          effectsToAdd.forEach(effect => {
            if (effect !== activeEffect || effect.allowRecurse) {
              effects.add(effect)
            }
          })
        }
      }
    
      if (type === TriggerOpTypes.CLEAR) {
        // collection being cleared
        // trigger all effects for target
        depsMap.forEach(add)
      } else if (key === 'length' && isArray(target)) {
      	// 这里就能监听到数组的 length 变化了
        depsMap.forEach((dep, key) => {
          if (key === 'length' || key >= (newValue as number)) {
            add(dep)
          }
        })
      } else {
        // schedule runs for SET | ADD | DELETE
        if (key !== void 0) {
          add(depsMap.get(key))
        }
    
        // also run for iteration key on ADD | DELETE | Map.SET
        switch (type) {
          case TriggerOpTypes.ADD:
            if (!isArray(target)) {
              add(depsMap.get(ITERATE_KEY))
              if (isMap(target)) {
                add(depsMap.get(MAP_KEY_ITERATE_KEY))
              }
            } else if (isIntegerKey(key)) {
              // new index added to array -> length changes
              add(depsMap.get('length'))
            }
            break
          case TriggerOpTypes.DELETE:
            if (!isArray(target)) {
              add(depsMap.get(ITERATE_KEY))
              if (isMap(target)) {
                add(depsMap.get(MAP_KEY_ITERATE_KEY))
              }
            }
            break
          case TriggerOpTypes.SET:
            if (isMap(target)) {
              add(depsMap.get(ITERATE_KEY))
            }
            break
        }
      }
    
      const run = (effect: ReactiveEffect) => {
        if (__DEV__ && effect.options.onTrigger) {
          effect.options.onTrigger({
            effect,
            target,
            key,
            type,
            newValue,
            oldValue,
            oldTarget
          })
        }
        if (effect.options.scheduler) {
          effect.options.scheduler(effect)
        } else {
          effect()
        }
      }
    
      // 触发依赖
      effects.forEach(run)
    }
    

    其实已经有 Vue2 内味儿了,其实就是一个改良版的发布订阅模式。get 时通过 track 收集依赖,而 set 时通过 trigger 触发了依赖,而 effect 收集了这些依赖并进行追踪,在响应后去触发相应的依赖。effect 也正是 Vue3 响应式的核心。

    了解了这些后,那么剩下的三个方法就很明朗了,一看就能明白:

    function deleteProperty(target: object, key: string | symbol): boolean {
      const hadKey = hasOwn(target, key)
      const oldValue = (target as any)[key]
      const result = Reflect.deleteProperty(target, key)
      if (result && hadKey) {
        trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
      }
      return result
    }
    
    function has(target: object, key: string | symbol): boolean {
      const result = Reflect.has(target, key)
      if (!isSymbol(key) || !builtInSymbols.has(key)) {
        track(target, TrackOpTypes.HAS, key)
      }
      return result
    }
    
    function ownKeys(target: object): (string | number | symbol)[] {
      track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
      return Reflect.ownKeys(target)
    }
    

    那么 Vue3 的响应式相关的比较核心的源码基本就是这些了,至于 collectionHandlers 方法,基本是换汤不换药,核心也是通过 trigger 和 track 实现,有兴趣的大佬可以自己看看。

    4 参考

    • MDN Rlect
    • Vue3 中文文档
    • Vue3 源码

    起源地下载网 » 深入理解 Vue3 的响应式

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元