当前位置 博文首页 > rmlzy:Vue3 源码之 reactivity

    rmlzy:Vue3 源码之 reactivity

    作者:rmlzy 时间:2021-01-29 11:30

    注: 为了直观的看到 Vue3 的实现逻辑, 本文移除了边缘情况处理、兼容处理、DEV环境的特殊逻辑等, 只保留了核心逻辑

    vue-next/reactivity 实现了 Vue3 的响应性, reactivity 提供了以下接口:

    export {
      ref, // 代理基本类型
      shallowRef, // ref 的浅代理模式
      isRef, // 判断一个值是否是 ref
      toRef, // 把响应式对象的某个 key 转为 ref
      toRefs, // 把响应式对象的所有 key 转为 ref
      unref, // 返回 ref.value 属性
      proxyRefs,
      customRef, // 自行实现 ref						
      triggerRef, // 触发 customRef
      Ref, // 类型声明
      ToRefs, // 类型声明
      UnwrapRef, // 类型声明
      ShallowUnwrapRef, // 类型声明
      RefUnwrapBailTypes // 类型声明
    } from './ref'
    export {
      reactive, // 生成响应式对象
      readonly, // 生成只读对象
      isReactive, // 判断值是否是响应式对象
      isReadonly, // 判断值是否是只读对象
      isProxy, // 判断值是否是 proxy
      shallowReactive, // 生成浅响应式对象
      shallowReadonly, // 生成浅只读对象
      markRaw, // 让数据不可被代理
      toRaw, // 获取代理对象的原始对象
      ReactiveFlags, // 类型声明
      DeepReadonly // 类型声明
    } from './reactive'
    export {
      computed, // 计算属性
      ComputedRef, // 类型声明
      WritableComputedRef, // 类型声明
      WritableComputedOptions, // 类型声明
      ComputedGetter, // 类型声明
      ComputedSetter // 类型声明
    } from './computed'
    export {
      effect, // 定义副作用函数, 返回 effect 本身, 称为 runner
      stop, // 停止 runner
      track, // 收集 effect 到 Vue3 内部的 targetMap 变量
      trigger, // 执行 targetMap 变量存储的 effects
      enableTracking, // 开始依赖收集
      pauseTracking, // 停止依赖收集
      resetTracking, // 重置依赖收集状态
      ITERATE_KEY, // 固定参数
      ReactiveEffect, // 类型声明
      ReactiveEffectOptions, // 类型声明
      DebuggerEvent // 类型声明
    } from './effect'
    export {
      TrackOpTypes, // track 方法的 type 参数的枚举值
      TriggerOpTypes // trigger 方法的 type 参数的枚举值
    } from './operations'
    

    一、名词解释

    • target: 普通的 JS 对象

    • reactive: @vue/reactivity 提供的函数, 接收一个对象, 并返回一个 代理对象, 即响应式对象

    • shallowReactive: @vue/reactivity 提供的函数, 用来定义浅响应对象

    • readonly:@vue/reactivity 提供的函数, 用来定义只读对象

    • shallowReadonly: @vue/reactivity 提供的函数, 用来定义浅只读对象

    • handlers: Proxy 对象暴露的钩子函数, 有 get()set()deleteProperty()ownKeys() 等, 可以参考MDN

    • targetMap: @vue/reactivity 内部变量, 存储了所有依赖

    • effect: @vue/reactivit 提供的函数, 用于定义副作用, effect(fn, options) 的参数就是副作用函数

    • watchEffect: @vue/runtime-core 提供的函数, 基于 effect 实现

    • track: @vue/reactivity 内部函数, 用于收集依赖

    • trigger: @vue/reactivity 内部函数, 用于消费依赖

    • scheduler: effect 的调度器, 允许用户自行实现

    二、Vue3 实现响应式的思路

    先看下边的流程简图, 图中 Vue 代码的功能是: 每隔一秒在 idBoxdiv 中输出当前时间

    在开始梳理 Vue3 实现响应式的步骤之前, 要先简单理解 effect, effect 是响应式系统的核心, 而响应式系统又是 Vue3 的核心

    上图中从 tracktargetMap 的黄色箭头, 和从 targetMaptrigger 的白色箭头, 就是 effect 函数要处理的环节

    effect 函数的语法为:

    effect(fn, options)
    

    effect 接收两个参数, 第一个必填参数 fn 是副作用函数

    第二个选填 options 的参数定义如下:

    export interface ReactiveEffectOptions {
      lazy?: boolean                              // 是否延迟触发 effect
      scheduler?: (job: ReactiveEffect) => void   // 调度函数
      onTrack?: (event: DebuggerEvent) => void    // 追踪时触发
      onTrigger?: (event: DebuggerEvent) => void  // 触发回调时触发
      onStop?: () => void                         // 停止监听时触发
      allowRecurse?: boolean                      // 是否允许递归
    }
    

    下边从流程图中左上角的 Vue 代码开始

    第 1 步

    通过 reactive 方法将 target 对象转为响应式对象, reactive 方法的实现方法如下:

    import { mutableHandlers } from './baseHandlers'
    import { mutableCollectionHandlers } from './collectionHandlers'
    
    const reactiveMap = new WeakMap<Target, any>()
    const readonlyMap = new WeakMap<Target, any>()
    
    export function reactive(target: object) {
      return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers
      )
    }
    
    function createReactiveObject(
      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<any>
    ) {
      const proxyMap = isReadonly ? readonlyMap : reactiveMap
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      const targetType = getTargetType(target) // 先忽略, 上边例子中, targetType 的值为: 1
      const proxy = new Proxy(
        target,
        targetType === 2 ? collectionHandlers : baseHandlers
      )
      proxyMap.set(target, proxy)
      return proxy
    }
    

    reactive 方法携带 target 对象和 mutableHandlersmutableCollectionHandlers 调用 createReactiveObject 方法, 这两个 handers 先忽略

    createReactiveObject 方法通过 reactiveMap 变量缓存了一份响应式对象, reactiveMapreadonlyMap 变量是文件内部的变量, 相当于文件级别的闭包变量

    其中 targetType 有三种枚举值: 0 代表不合法, 1 代表普通对象, 2 代表集合, 图中例子中, targetType 的值为 1, 对于 { text: '' } 这个普通对象传进 reactive() 方法时, 使用 baseHandlers 提供的 mutableHandlers

    最后调用 Proxy 方法将 target 转为响应式对象, 其中 "响应" 体现在 handers 里, 可以这样理解: reactive = Proxy (target, handlers)

    第 2 步

    mutableHandlers 负责挂载 getsetdeletePropertyhasownKeys 这五个方法到响应式对象上

    其中 gethasownKeys 负责收集依赖, setdeleteProperty 负责消费依赖

    响应式对象的 gethasownKeys 方法被触发时, 会调用 createGetter 方法, createGetter 的实现如下:

    function createGetter(isReadonly = false, shallow = false) {
      return function get(target: Target, key: string | symbol, receiver: object) {
        const res = Reflect.get(target, key, receiver)
        if (!isReadonly) {
          track(target, TrackOpTypes.GET, key)
        }
        if (isObject(res)) {
          return isReadonly ? readonly(res) : reactive(res)
        }
        return res
      }
    }
    

    { text: '' } 这个普通JS对象传到 createGetter 时, key 的值为: text, res 的值为: String 类型, 如果 res 的值为 Object 类型则会递归调用, 将 res 转为响应式对象

    createGetter 方法的目的是触发 track 方法, 对应本文的第 3 步

    响应式对象的 setdeleteProperty 方法被触发时, 会调用 createSetter 方法, createSetter 的实现如下:

    function createSetter(shallow = false) {
      return function set(
        target: object,
        key: string | symbol,
        value: unknown,
        receiver: object
      ): boolean {
        const oldValue = (target as any)[key]
        const result = Reflect.set(target, key, value, receiver)
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
        return result
      }
    }
    

    createSetter 方法的目的是触发 trigger 方法, 对应本文的第 4 步

    第 3 步

    这一步是整个响应式系统最关键的一步, 即我们常说的依赖收集, 依赖收集的概念很简单, 就是把 响应式数据副作用函数 建立联系

    文章一开始流程图的例子中, 就是把 target 对象和 document.getElementById("Box").innerText = date.text; 这个副作用函数建立关联, 这个 "关联" 指的就是上边提到的 targetMap 变量, 后边会详细描述一下 targetMap 对象的结构

    第 2 步介绍了 createGetter 方法的核心是调用 track 方法, track 方法由 @/vue/reativity/src/effect.ts 提供, 下面看一下 track 的实现:

    const targetMap = new WeakMap<any, KeyToDepMap>()
    
    // target: { text: '' }
    // type: get
    // key: text
    export function track(target: object, type: TrackOpTypes, key: unknown) {
      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)
      }
    }
    

    track 方法我们能看到 targetMap 这个闭包变量上储存了所有的 effect, 换句话说是把能影响到 target 的副作用函数收集到 targetMap 变量中

    targetMap 是个 WeakMap, WeakMap 和 Map 的区别在于 WeakMap 的键只能是对象, 用 WeakMap 而不用 Map 是因为 Proxy 对象不能代理普通数据类型

    targetMap 的结构:

    const targetMap = {
    	[target]: {
    		[key1]: [effect1, effect2, effect3, ...],
    		[key2]: [effect1, effect2, effect3, ...]
    	}
    }
    

    { text: '' } 这个target 传进来时, targetMap 的结构是:

    // 上边例子中用来在 id 为 Box 的 div 中输出当前时间的副作用函数
    const effect = () => {
    	document.getElementById("Box").innerText = date.text;
    };
    
    const target = {
    	"{ text: '' }": {
    		"text": [effect]
    	}
    }
    

    举三个例子, 来分析一下 targetMap 的结构, 第一个例子是多个 target 情况:

    <script>
    import { effect, reactive } from "@vue/reactivity";
    
    const target1 = { language: "JavaScript"};
    const target2 = { language: "Go"};
    const target3 = { language: "Python"};
    const r1 = reactive(target1);
    const r2 = reactive(target2);
    const r3 = reactive(target3);
    
    // effect1
    effect(() => {
      console.log(r1.language);
    });
    
    // effect2
    effect(() => {
      console.log(r2.language);
    });
    
    // effect3
    effect(() => {
      console.log(r3.language);
    });
    
    // effect4
    effect(() => {
      console.log(r1.language);
      console.log(r2.language);
      console.log(r3.language);
    });
    </script>
    

    这种情况下 targetMap 的构成是:

    const effect1 = () => {
      console.log(r1.language);
    };
    const effect2 = () => {
      console.log(r2.language);
    };
    const effect3 = () => {
      console.log(r3.language);
    };
    const effect4 = () => {
      console.log(r1.language);
      console.log(r2.language);
      console.log(r3.language);
    };
    
    const targetMap = {
    	'{"language":"JavaScript"}': {
    		"language": [effect1, effect4]
    	},
      '{"language":"Go"}': {
        "language": [effect2, effect4]
      },
      '{"language":"Python"}': {
        "language": [effect3, effect4]
      }
    }
    

    第二个例子是单个 target 多个属性时:

    import { effect, reactive } from "@vue/reactivity";
    const target = { name: "rmlzy", age: "27", email: "rmlzy@outlook.com"};
    const user = reactive(target);
    
    effect(() => {
      console.log(user.name);
      console.log(user.age);
      console.log(user.email);
    });
    

    这种情况下 targetMap 的构成是:

    const effect = () => {
      console.log(user.name);
      console.log(user.age);
      console.log(user.email);
    };
    
    const targetMap = {
      '{"name":"rmlzy","age":"27","email":"rmlzy@outlook.com"}': {
        "name": [effect],
        "age": [effect],
        "email": [effect]
      }
    }
    

    第三个例子是多维对象时:

    import { effect, reactive } from "@vue/reactivity";
    const target = {
      name: "rmlzy",
      skills: {
        frontend: ["JS", "TS"],
        backend: ["Node", "Python", "Go"]
      }
    };
    const user = reactive(target);
    
    // effect1
    effect(() => {
      console.log(user.name);
    });
    
    // effect2
    effect(() => {
      console.log(user.skills);
    });
    
    // effect3
    effect(() => {
      console.log(user.skills.frontend);
    });
    
    // effect4
    effect(() => {
      console.log(user.skills.frontend[0]);
    });
    

    这种情况下 targetMap 的构成是:

    const effect1 = () => {
      console.log(user.name);
    };
    const effect2 = () => {
      console.log(user.skills);
    };
    const effect3 = () => {
      console.log(user.skills.frontend);
    };
    const effect4 = () => {
      console.log(user.skills.frontend[0]);
    };
    
    const targetMap = {
      '{"name":"rmlzy","skills":{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}}': {
        "name": [effect1],
        "skills": [effect2, effect3, effect4]
      },
      '{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}': {
        "frontend": [effect3, effect4]
      }
    }
    

    第 4 步

    第 3 步的目的是收集依赖, 这一步的目的是消费依赖

    这里要注意, 只有当 target 代理对象的 get 方法被触发时, 才会真正执行 track, 换句话说, 没有地方需要 get target 对象时, target 没有依赖, 也就没有收集依赖一说

    下边的例子中只是把 target 转换为了响应式对象, 并没有触发依赖收集, targetMap 是空的

    const target = {"text": ""};
    const date = reactive(target);
    effect(() => {
      date.text = new Date().toString();
    });
    

    第 2 步介绍了 createSetter 方法的核心是调用 trigger 方法, trigger 方法由 @/vue/reativity/src/effect.ts 提供, 下面看一下 trigger 的实现:

    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
      }
      const effects = new Set<ReactiveEffect>()
    	if (isMap(target)) {
        effects.add(depsMap.get(ITERATE_KEY))
    	}
      const run = (effect: ReactiveEffect) => {
        if (effect.options.scheduler) {
          effect.options.scheduler(effect)
        } else {
          effect()
        }
      }
      effects.forEach(run)
    }
    

    trigger 的实现很简单, 先把 target 相关的 effect 汇总到 effects 数组中, 然后调用 effects.forEach(run) 执行所有的副作用函数

    再回顾一下 effect 方法的定义: effect(fn, options), 其中 options 有个可选属性叫 scheduler, 从上边 run 函数也可以看到 scheduler 的作用是让用户自定义如何执行副作用函数

    第 5 步

    又回到了本文最开始讲的 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 方法

    可以看到 options.lazy 默认为 false 会直接执行 effect, 当设置为 true 时, 会返回 effect 由用户手动触发

    createReactiveEffect 函数的实现如下:

    const effectStack: ReactiveEffect[] = []
    let activeEffect: ReactiveEffect | undefined
    
    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.raw = fn
      effect.deps = []
      effect.options = options
      return effect
    }
    

    首先定义了 effect 是个普通的 function, 先看后边 effect 函数挂载的属性:

    effect.id = uid++ // 自增ID, 每个 effect 唯一的ID
    effect.allowRecurse = !!options.allowRecurse // 是否允许递归
    effect._isEffect = true // 特殊标记
    effect.active = true // 激活状态
    effect.deps = [] // 依赖数组
    effect.raw = fn // 缓存一份用户传入的副作用函数
    effect.options = options // 缓存一份用户传入的配置
    

    isEffect 函数用来判断值是否是 effect, 就是根据上边 _isEffect 变量判断的, isEffect 函数实现如下:

    function isEffect(fn) {
      return fn && fn._isEffect === true;
    }
    

    再来看 effect 的核心逻辑:

    cleanup(effect)
    try {
      enableTracking()
      effectStack.push(effect)
      activeEffect = effect
      return fn()
    } finally {
      effectStack.pop()
      resetTracking()
      activeEffect = effectStack[effectStack.length - 1]
    }
    

    effectStack 用数组实现栈, activeEffect 是当前生效的 effect

    先执行 cleanup(effect):

    function cleanup(effect: ReactiveEffect) {
      const { deps } = effect
      if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
          deps[i].delete(effect)
        }
        deps.length = 0
      }
    }
    

    cleanup 的目的是清空 effect.deps, deps 是持有该 effect 的依赖数组, deps 的结构如下

    清除完依赖后, 开始重新收集依赖, 把当前 effect 追加到 effectStack, 将 activeEffect 设置为当前的 effect, 然后调用 fn 并且返回 fn() 的结果

    第 4 步提过到: "只有当 target 代理对象的 get 方法被触发时, 才会真正执行 track", 至此才是真正的触发了 target代理对象的 get 方法, 执行了track 方法然后收集到了依赖

    等到 fn 执行结束, finally 阶段, 把当前的 effect 弹出, 恢复 effectStack 和 activeEffect, Vue3 整个响应式的流程到此结束

    三、知识点

    activeEffect 的作用

    我的理解是为了暴露给 onTrack 方法, 来整体看一下 activeEffect 出现的地方:

    let activeEffect;
    
    function effect(fn, options = EMPTY_OBJ) {
      const effect = createReactiveEffect(fn, options);
      return effect;
    }
    
    function createReactiveEffect(fn, options) {
      const effect = function reactiveEffect() {
      	// 省略部分代码 ...
        try {
          activeEffect = effect;
          return fn();
        }
        finally {
          activeEffect = effectStack[effectStack.length - 1];
        }
      };
      // 省略部分代码 ...
      return effect;
    }
    
    function track(target, type, key) {
      if (activeEffect === undefined) {
        return;
      }
      let dep = targetMap.get(target).get(key); // dep 是存储 effect 的 Set 数组
      if (!dep.has(activeEffect)) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
        if (activeEffect.options.onTrack) {
          activeEffect.options.onTrack({
            effect: activeEffect,
            target,
            type,
            key
          });
        }
      }
    }
    
    1. fn 执行前, activeEffect 被赋值为当前 effect

    2. fn 执行时的依赖收集阶段, 获取 targetMap 中的 dep (存储 effect 的 Set 数组), 并暴露给 options.onTrack 接口

    effect 和 stop

    @vue/reactivity 提供了 stop 函数, effect 可以被 stop 函数终止

    const obj = reactive({ foo: 0 });
    
    const runner = effect(() => {
      console.log(obj.foo);
    });
    
    // effect 被执行一次, 输出 0
    
    // obj.foo 被赋值一次, effect 被执行一次, 输出 1
    obj.foo ++;
    
    // 停止 effect
    stop(runner);
    
    // effect 不会被触发, 无输出
    obj.foo ++;
    

    watchEffect 和 effect

    1. watchEffect 来自 @vue/runtime-core, effect 来自 @vue/reactivity
    2. watchEffect 基于 effect 实现
    3. watchEffect 会维护与组件实例的关系, 如果组件被卸载, watchEffect 会被 stop, 而 effect 不会被 stop

    watchEffect 和 invalidate

    watchEffect 接收的副作用函数, 会携带一个 onInvalidate 的回调函数作为参数, 这个回调函数会在副作用无效时执行

    watchEffect(async (onInvalidate) => {
      let valid = true;
      onInvalidate(() => {
        valid = false;
      });
      const data = await fetch(obj.foo);
      if (valid) {
        // 获取到 data
      } else {
        // 丢弃
      }
    });
    

    ref

    JS数据类型:

    • 基本类型: String、Number、Boolean、Null、Undefined、Symbol
    • 引用数据类型: Object、Array、Function

    因为 Proxy 只能代理对象, reactive 函数的核心又是 Proxy, 所以 reactive 不能代理基本类型

    对于基本类型需要用 ref 函数将基本类型转为对象:

    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)
        }
      }
    }
    

    其中 __v_isRef 参数用来标志当前值是 ref 类型, isRef 的实现如下:

    export function isRef(r: any): r is Ref {
      return Boolean(r && r.__v_isRef === true)
    }
    

    这样做有个缺点, 需要多取一层 .value:

    const myRef = ref(0);
    effect(() => {
      console.log(myRef.value);
    });
    myRef.value = 1;
    

    这也是 Vue ref 语法糖提案的原因, 可以参考 如何评价 Vue 的 ref 语法糖提案?

    reactive 和 shallowReactive

    shallowReactive 用来定义浅响应数据, 深层次的对象值是非响应式的:

    const target = {
      foo: {
        bar: 1
      }
    };
    const obj = shallowReactive(target);
    
    effect(() => {
      console.log(obj.foo.bar);
    });
    
    obj.foo.bar = 2; // 无效, reactive 则有效
    obj.foo = { bar: 2 }; // 有效
    

    readonly 和 shallowReadonly

    类似 shallowReactive, 深层次的对象值是可以被修改的

    markRaw 和 toRaw

    markRaw 的作用是让数据不可被代理, 所有携带 __v_skip 属性, 并且值为 true 的数据都会被跳过:

    export function markRaw<T extends object>(value: T): T {
      def(value, ReactiveFlags.SKIP, true)
      return value
    }
    

    toRaw 的作用是获取代理对象的原始对象:

    const obj = {};
    const reactiveProxy = reactive(obj);
    console.log(toRaw(reactiveProxy) === obj); // true
    

    computed

    const myRef = ref(0);
    const myRefComputed = computed(() => {
      return myRef.value * 2;
    });
    effect(() => {
      console.log(myRef.value * 2);
    });
    

    myRef 值变化时, computed 会执行一次, effect 会执行一次

    myRef 值未变化时, computed 不会执行, effect 依旧会执行


    如果你有问题欢迎留言和我交流, 阅读原文