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

    正文概述 掘金(yingpengsha)   2021-03-23   556

    前言


    Vue3.0 和 Vue2.0 整体的响应式思路没有变化,但是实现细节发生了较大的变化。并且 Vue3.0 将响应式系统进行了解耦,从主体代码中抽离了出来,这意味着,我们可以将 Vue3.0 的响应式系统视作一个单独的库来使用,就像 RxJS。为了加深对 Vue3.0 的响应式原理的了解所以有了此篇文章。

    设计思路


    虽然 Vue3.0 和 Vue2.0 的响应式思路没有变,但为了方便回顾和讲解,我们还是重新理一下 Vue 的响应式系统设计思路

    什么是响应式


    对象 A 发生变化后,对象 B 也随之发生改变

    • 响应式编程:b 变量 或者 c 变量的值 发生变化,a 变量的值 也随之发生改变(a := b + c)
    • 响应式布局:视图窗口 发生变化,视图内元素布局 也随之发生改变
    • MVVM:Model 发生变化,View 也随之发生变化


    在 Vue 里的 对象 A 就是 数据对象 B 则是 视图的渲染函数 或者 watch 或者 computed

    Vue 的响应式系统需要做到什么

    1. 如何知道 数据 发生了变化
    2. 如何知道 响应对象 依赖了哪些 数据,建立依赖关系
    3. 如何在 数据 发生改变后通知依赖的 响应对象 做出响应


    我们再将上面的需求转换成我们或多或少都听过的专业术语

    1. 数据挟持
    2. 依赖收集
    3. 派发更新

    实现原理

    数据挟持

    概述


    如何知道数据发生了变化呢,这问题再进一步就是 如何知道数据被操作了呢,其中的操作包括(增、删、改、查)。

    Vue2.0 是利用 Object.defineProperty 可以挟持并自定义对象setter 操作和 getter 操作来实现的,但是这有一些问题:

    • 数组 这一数据类型的数据 defineProperty 无法直接挟持,需要使用比较 hack 的操作去完成
    • 操作在一些场景下无法捕获到,在无法挟持到的场景下,我们就必须使用 $set$delete 这些 Vue 封装的函数去代替 js 原生的值操作,增加了心智成本
    • 性能的浪费,因为无法知道具体哪些值是需要响应式的,Vue2.0 会不管三七二十一,只要是在 data 里能挟持的都会挟持一边,但实际上开发者数据更改的粒度往往不会这么细,所以这会导致一定程度上的性能浪费(用 Object.freeze() 等操作可以在一定程度上解决这个问题)


    Vue3.0 则是使用 Proxy 来实现数据更改的监听,Proxy 从定义上来说简直就是完美的为 **数据挟持 **这一目的而准备的,请看 MDN 介绍:

    从某种意义上来讲,Proxy 可以视为 Object.defineProperty 的强化,它拥有更丰富的可以挟持的内容,并且解决了上面描述使用 defineProperty 存在的问题:

    • 不再需要专门为 数组 进行特殊的挟持操作,Proxy 直接搞定
    • 操作 Proxy 也能直接挟持
    • 因为 Proxy 的一些特点,Proxy 可以实现惰性的挟持,而不需要深度的挟持所有值(后面再说怎么实现的)
    • 同时因为 Proxy 并不是对数据源进行更改,从而可以保证不会出现太多的副作用

    实现细节


    Vue3.0 对于数据的响应式挟持,统一使用 composition API 来实现,我们仅对最典型的 reactive() 来讲解一下

    reactive
    export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
    export function reactive(target: object) {
      // 如果目标数据已经被 readonly() 封装过了,则直接返回,不对其进行响应式处理
      if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
        return target
      }
      // 调用通用函数进行响应式处理
      return createReactiveObject(
        target, // 目标数据
        false, // 是否做只读操作
        mutableHandlers, // Object/Array 类型的代理处理器
        mutableCollectionHandlers // Map/Set/WeakMap/WeakSet 类型的代理处理器
      )
    }
    
    createReactiveObject
    function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) {
      // 如果不是对象,直接抛出错误并返回
      if (!isObject(target)) return target
      
      // 如果对象已经是响应式对象,则直接返回,但如果是 readonly() 一个已经是响应式的数据则不返回,继续执行
      if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) return target
      
      // 已经被代理的缓存(WeekMap),readonly 和 reactive 两种代理方式各有一个缓存
      const proxyMap = isReadonly ? readonlyMap : reactiveMap
      
      // 如果对象已经被代理过了,则直接从缓存中取出
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      
      // 判断目标对象是否是一些特殊的或者不需要劫持的对象,如果是则直接返回
      // 并获得其数据类型:Object/Array => TargeType.COMMON、Map/Set/WeakMap/WeakSet => TargeType.COLLECTION
      const targetType = getTargetType(target)
      if (targetType === TargetType.INVALID) {
        return target
      }
      
      // 创建 Proxy 代理,如果目标对象的类型是 Map/Set/WeakMap/WeakSet 则使用专门针对集合使用的代理处理器,反之用基本处理器
      const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
      )
      
      // 存入缓存
      proxyMap.set(target, proxy)
      // 返回
      return proxy
    }
    
    mutableHandlers

    Object/Array 类型的代理处理器

    export const mutableHandlers: ProxyHandler<object> = {
      get(target: Target, key: string | symbol, receiver: object) {
        // ...内部常量代理
        // ReactiveFlags.IS_REACTIVE = true
        // ReactiveFlags.IS_READONLY = false
        // ReactiveFlags.RAW = target
    		
        // 目标对象是否是数组
        const targetIsArray = isArray(target);
    
        // 调用一些特定的数组方法时的特殊处理
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
          return Reflect.get(arrayInstrumentations, key, receiver);
        }
    
        // 获取值
        const res = Reflect.get(target, key, receiver);
    
        // 如果是一些原生内置 Symbol,或者不需要跟踪的值的直接返回
        if (
          isSymbol(key)
            ? builtInSymbols.has(key as symbol)
            : isNonTrackableKeys(key)
        ) {
          return res;
        }
    
        // 依赖收集
        track(target, TrackOpTypes.GET, key);
    
        // 如果对应值已经 Ref() 过,则根据当前是不是通过正常 key 值访问一个数组来决定要不要返回原生 ref,还是其 value
        if (isRef(res)) {
          const shouldUnwrap = !targetIsArray || !isIntegerKey(key);
          return shouldUnwrap ? res.value : res;
        }
    
        // 如果是对象,则挟持该对象(惰性响应式的出处)
        if (isObject(res)) {
          return reactive(res);
        }
    
        // 返回结果
        return res;
      },
      set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
        // 旧值
        const oldValue = (target as any)[key];
        
        // 新值去除可能存在的响应式
        value = toRaw(value);
        
        // 如果旧值是 Ref 值,则传递给 Ref 处理
        if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
          oldValue.value = value;
          return true;
        }
    
        // 有没有对应 key 值
        const hadKey =
          isArray(target) && isIntegerKey(key)
            ? Number(key) < target.length
            : hasOwn(target, key);
        
        // 设置值
        const result = Reflect.set(target, key, value, receiver);
        
        // 派发更新
        if (target === toRaw(receiver)) {
          if (!hadKey) {
            trigger(target, TriggerOpTypes.ADD, key, value);
          } else if (hasChanged(value, oldValue)) {
            trigger(target, TriggerOpTypes.SET, key, value, oldValue);
          }
        }
        return result;
      },
      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;
      },
      has(target: object, key: string | symbol): boolean {
        const result = Reflect.has(target, key);
        // 如果该值不是原生内部的 Symbol 值,则进行依赖收集
        if (!isSymbol(key) || !builtInSymbols.has(key)) {
          track(target, TrackOpTypes.HAS, key);
        }
        return result;
      },
      ownKeys(target: object): (string | number | symbol)[] {
      	// 依赖收集
        track(
          target,
          TrackOpTypes.ITERATE,
          isArray(target) ? "length" : ITERATE_KEY
        );
        return Reflect.ownKeys(target);
      },
    };
    
    collectionHandlers

    Map/Set/WeakMap/WeakSet 类型的代理处理器
    由于上述四个类型修改值都是通过函数修改的,所以代理函数只拦截 get 方法,用于拦截响应对象调用了哪个操作函数,再进行具体的依赖收集或者派发更新

    export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
      get: (
        target: CollectionTypes,
        key: string | symbol,
        receiver: CollectionTypes
      ) => {
        // ...内部常量代理
        // ReactiveFlags.IS_REACTIVE = true
        // ReactiveFlags.IS_READONLY = false
        // ReactiveFlags.RAW = target
    		
        // 使用对应封装的函数,来进行处理
        return Reflect.get(
          hasOwn(mutableInstrumentations, key) && key in target
            ? mutableInstrumentations
            : target,
          key,
          receiver
        )
      }
    }
    
    // 函数代理
    const mutableInstrumentations: Record<string, Function> = {
      get(this: MapTypes, key: unknown) {
        return get(this, key)
      },
      get size() {
        return size((this as unknown) as IterableCollections)
      },
      has,
      add,
      set,
      delete: deleteEntry,
      clear,
      forEach: createForEach(false, false)
    }
    
    // 获取值
    function get(
      target: MapTypes,
      key: unknown,
      isReadonly = false,
      isShallow = false
    ) {
      // 原生对象
      target = (target as any)[ReactiveFlags.RAW]
      const rawTarget = toRaw(target)
      
      // 原生 key
      const rawKey = toRaw(key)
      
      // 如果是响应式的,则对响应式 key 进行依赖收集
      if (key !== rawKey) {
        track(rawTarget, TrackOpTypes.GET, key)
      }
        
      // 对原生 key 进行依赖收集
      track(rawTarget, TrackOpTypes.GET, rawKey)
        
      // 如果目标集合存在 has 函数,则再调用 has 进行依赖收集,因为 get()隐形依赖 has,并返回响应式 key
      const { has } = getProto(rawTarget)
      if (has.call(rawTarget, key)) {
        return toReactive(target.get(key))
      } else if (has.call(rawTarget, rawKey)) {
        return toReactive(target.get(rawKey))
      }
    }
    
    function size(target: IterableCollections, isReadonly = false) {
      // 依赖收集,key 值时 Vue 内部的 Symbol('iterate')
      target = (target as any)[ReactiveFlags.RAW]
      track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
      return Reflect.get(target, 'size', target)
    }
    
    function add(this: SetTypes, value: unknown) {
      value = toRaw(value)
      const target = toRaw(this)
      // 调用原生的 has
      const proto = getProto(target)
      const hadKey = proto.has.call(target, value)
      
      // 如果不存在,则添加,并派发更新
      if (!hadKey) {
        target.add(value)
        trigger(target, TriggerOpTypes.ADD, value, value)
      }
      return this
    }
    
    function set(this: MapTypes, key: unknown, value: unknown) {
      value = toRaw(value)
      const target = toRaw(this)
      const { has, get } = getProto(target)
    
      // 是否已经存在对应的 key,通过传入的 key 和 key 可能存在的真实 rawKey 去分别判断
      let hadKey = has.call(target, key)
      if (!hadKey) {
        key = toRaw(key)
        hadKey = has.call(target, key)
      }
    
      // 取出旧值,并设置
      const oldValue = get.call(target, key)
      target.set(key, value)
      
      // 如果是新增则触发新增的更新,反之触发设置的更新
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
      return this
    }
    
    function deleteEntry(this: CollectionTypes, key: unknown) {
      const target = toRaw(this)
      const { has, get } = getProto(target)
      
      // 同 set,是否已经存在对应的 key,通过传入的 key 和 key 可能存在的真实 rawKey 去分别判断
      let hadKey = has.call(target, key)
      if (!hadKey) {
        key = toRaw(key)
        hadKey = has.call(target, key)
      }
    
      // 取出旧值,并删除
      const oldValue = get ? get.call(target, key) : undefined
      const result = target.delete(key)
      
      // 触发删除的更新
      if (hadKey) {
        trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
      }
      return result
    }
    
    function clear(this: IterableCollections) {
      const target = toRaw(this)
      const hadItems = target.size !== 0
      const result = target.clear()
      // 触发清空的更新
      if (hadItems) {
        trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, undefined)
      }
      return result
    }
    
    function forEach(
      this: IterableCollections,
      callback: Function,
      thisArg?: unknown
    ) {
      const observed = this as any
      const target = observed[ReactiveFlags.RAW]
      const rawTarget = toRaw(target)
     	// 基于迭代器收集依赖
      track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)  
     	// 使其子集具备响应式
      return target.forEach((value: unknown, key: unknown) => {
        return callback.call(thisArg, toReactive(value), toReactive(key), observed)
      })
    }
    

    依赖收集

    概述


    如何知道 响应对象 依赖了哪些 数据,这个问题进一步就是 响应对象用了哪些数据。
    Vue 的大体思路是这样的,比如我一个函数 fnA,里使用了 data 里的 BC
    想要知道 fnA 使用了 BC,那我们干脆就直接运行一下 fnA,在 BC 里面等待 fnA 的获取,然后建立两者的依赖。

    Vue2.0 里有一些概念 Watcher,Dep,target。

    • Watcher 就是指 fnA
    • Dep 则是存在 Bsetter 里面的一个对象,用于存放 Watcher 集合
    • target 则是现在正在进行依赖收集的 Watcher

    简单粗暴来讲
    new Watcher(fnA) => **target 等于当前 Watcher并调用 fnA => fnA 获取 B 的值,B 会将 target 放到自己的 Dep 上 => B 更新了,通知自己 Dep 上的 Watcher 重新执行 fnA

    Vue3.0 思路差不多,但是实现上大有不同,因为 Vue3.0 不再随意对数据进行侵入式修改或者挟持,所以 Vue3.0 单独拎出来了一个静态变量存储依赖关系,这个变量叫做 targetMap
    同时引进了一个新概念 effect,它与 Vue2.0 的 **Watcher **差不多,但是概念有些转换,从 监听者 变成了 副作用,指的是 对应依赖 值改变后会发生的副作用

    数据类型

    数据类型大概如下:
    targetMapkey 指向的是 AB 所在的对象 Datavalue 指向的是
    KeyToDepMapkey 的是 'A''B' 的键值,value 则是存放 effect 里面的 Watcher 集合

    type Dep = Set<ReactiveEffect>
    type KeyToDepMap = Map<any, Dep>
    const targetMap = new WeakMap<any, KeyToDepMap>()
    

    Vue3.0 响应式原理漫谈

    实现细节

    Effect

    effect 实际上就是 Vue2.0 里面的 Watcher,只不过做的事情相对而言化繁为简了

    export function effect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions = EMPTY_OBJ
    ): ReactiveEffect<T> {
      // 如果传进来的函数已经是一个 effect 了,则取出其原生的函数进行处理
      if (isEffect(fn)) { fn = fn.raw }
      
      // 创建响应式副作用
      const effect = createReactiveEffect(fn, options)
      
      // 如果不是惰性的副作用,则直接运行并依赖收集,computed 就是惰性的
      if (!options.lazy) {
        effect()
      }
      return effect
    }
    
    createReactiveEffect
    function createReactiveEffect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions
    ): ReactiveEffect<T> {
      // 返回封装后的副作用函数
      const effect = function reactiveEffect(): unknown {
        // 副作用函数核心,稍后讲
      } as ReactiveEffect
      
      // 一些静态属性的定义
      effect.id = uid++
      effect.allowRecurse = !!options.allowRecurse
      effect._isEffect = true
      effect.active = true
      effect.raw = fn
      effect.deps = []
      effect.options = options
      return effect
    }
    
    reactiveEffect
    function reactiveEffect(): unknown {
      // 如果副作用已经被暂停,则优先执行其调度器,再运行函数本体
      if (!effect.active) {
        return options.scheduler ? undefined : fn()
      }
      // 如果当前副作用未在运行的时候才进入
      if (!effectStack.includes(effect)) {
        // 先清除旧的依赖关系
        cleanup(effect)
        try {
          // 开启依赖收集
          enableTracking()
          // 加入运行中的副作用堆栈
          effectStack.push(effect)
          // 确认当前副作用,Vue2.0 里的 target
          activeEffect = effect
          // 执行函数
          return fn()
        } finally {
          // 退出堆栈
          effectStack.pop()
          // 关闭依赖收集
          resetTracking()
          // 将当前副作用转交给上一个或者置空
          activeEffect = effectStack[effectStack.length - 1]
        }
      }
    }
    
    track

    会在数据挟持的 get / has / ownKeys 中调用

    export function track(target: object, type: TrackOpTypes, key: unknown) {
      // 如果当前没有再收集过程中,则退出
      if (!shouldTrack || activeEffect === undefined) {
        return
      }
      // 取出对象的 KeyToDepMap,如果没有则创建一个新的
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
      }
      
      // 取出对应 key 值的依赖集合,如果没有则创建一个新的
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, (dep = new Set()))
      }
      
      // 如果依赖中不存在当前的队列则进去,防止重复设置依赖
      if (!dep.has(activeEffect)) {
        // 双向依赖,保证新旧依赖的一致性
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
      }
    }
    

    派发更新

    概述


    派发更新可以说是三个大环节中最简单的一部分了,但 Vue3.0 对比 Vue2.0 实现了更多细节,只需要在 **值发生变动的时候从依赖关系中取出对应的副作用集合,触发副作用 **即可。
    我们可以在上面数据挟持中的 set / deleteProperty 发现派发更新的函数 trigger 的调用。

    实现细节

    trigger
    export function trigger(
      target: object,
      type: TriggerOpTypes,
      key?: unknown,
      newValue?: unknown,
      oldValue?: unknown,
      oldTarget?: Map<unknown, unknown> | Set<unknown>
    ) {
      // 响应值所在对象的 KeyToDepMap,如果没有则直接返回
      const depsMap = targetMap.get(target)
      if (!depsMap) {
        // never been tracked
        return
      }
    
      // 所需要触发的副作用集合
      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) {
      	// 如果对应的修改操作是,比如 new Set().clear()
        // 则将所有子值的副作用添加到副作用队列中
        depsMap.forEach(add)
      } else if (key === 'length' && isArray(target)) {
        // 如果修改的是数组的 length,意味新的长度后面的值都发生了变动,并将这些下标所对应的副作用加入到队列中
        depsMap.forEach((dep, key) => {
          if (key === 'length' || key >= (newValue as number)) {
            add(dep)
          }
        })
      } else {
        // 修改值,新增值,删除值
        // 将对应值的副作用添加至队列
        if (key !== void 0) {
          add(depsMap.get(key))
        }
    
        // 增删改对应需要触发的其他副作用(比如依赖于长度的副作用,依赖于迭代器的副作用)
        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)) {
              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:
            // 依赖于迭代器(比如调用了 new Map().forEach() 等于变相依赖了 set)的函数
            if (isMap(target)) {
              add(depsMap.get(ITERATE_KEY))
            }
            break
        }
      }
    
      // 优先调用调度器否则调用副作用自身
      const run = (effect: ReactiveEffect) => {
        if (effect.options.scheduler) {
          effect.options.scheduler(effect)
        } else {
          effect()
        }
      }
    
      effects.forEach(run)
    }
    

    起源地下载网 » Vue3.0 响应式原理漫谈

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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