Vue3源码之reactivity

远子 •  2021年01月25日

注: 为了直观的看到 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 依旧会执行


如果你有问题欢迎留言和我交流

(完)