Roger Leung‘s Epcot

vuePress-theme-reco Roger Leung ( z3rog )    2018 - 2021
Roger Leung‘s Epcot

Choose mode

  • dark
  • auto
  • light
Blog
Note
Github (opens new window)
author-avatar

Roger Leung ( z3rog )

18

Article

20

Tag

Blog
Note
Github (opens new window)
  • 首页
  • 框架与工具链

    • Vue 3
    • Vue 2
    • Webpack 4
  • 前端性能优化

    • 性能优化的必要性
    • 性能指标
    • 基本手段
    • 离线缓存
  • 浏览器机制

    • 架构
    • 导航
    • 渲染机制
    • 缓存机制
  • 网络协议

    • TCP 协议
    • HTTP 协议
    • HTTPS 协议
    • HTTP 2 协议
    • HTTP 3 协议
  • 其他

    • V8 中的快慢属性与快慢数组
    • V8 解析执行 JavaScript 流程简述
    • V8 的垃圾回收机制
    • 100 行代码写一个破产版 Vite
    • 浅谈微前端

Vue 3

vuePress-theme-reco Roger Leung ( z3rog )    2018 - 2021

Vue 3

Roger Leung ( z3rog ) 2020-06-20 VueVue 3

Vue 3 原理及源码初解读

# Vue 3 优势

Vue 3 在 Vue 2 的基础上做了很多方面的改进:

  • 使用 TypeScript 重写,更好的类型检查支持
  • 默认使用新的基于 Proxy 的响应式系统
  • 使用 AST 作模版解析,取代 Vue 2 正则匹配的解析手段
  • 对核心 API 做了解构并开放给开发者直接调用
  • 解构后对 Tree Shaking 的支持更加友好

# 基于 Proxy 的响应式系统及 API

还记得 Vue 2 是如何处理响应式对象与响应式数组的吗?

不记得了可以回顾一下这里

对于对象,遍历对象的属性,增加 getter 与 setter,若是深度监听且属性对应的值又为对象或数组(声明在 data 选项中的数据默认深度监听),继续递归遍历;对于数组,拦截与重写了数组原型的方法,实现数组变更只触发一次通知。

Vue 3 改用 Proxy 可以解决什么问题,又引入了什么需要解决的问题呢?

先看两个例子:

// case 1,proxy an nested obj
const obj = {
  foo: {
    bar: 1
  }
}

const proxy = new Proxy(obj, {
  get(target, key, receiver) {
    console.log('get value:', key)

    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log('set value:', key)

    return Reflect.set(target, key, value, receiver)
  },
})

proxy.foo.bar = 2
// get value: foo
// 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

对于嵌套的对象,对 proxy 实例对象的设置不能够触发深层属性的 setter。我们是否需要像 Vue 2 递归 defineReactive 一样递归 proxy 呢?

// case 2,proxy an array
const arr = [1, 2, 3]

const proxy = new Proxy(arr, {
  get(target, key, receiver) {
    console.log('get value:', key)

    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log('set value:', key)

    return Reflect.set(target, key, value, receiver)
  },
})

proxy.push(4)
// get value: push
// get value: length
// set value: 3
// set value: length
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

对于数组,调用 proxy.push 方法,相当于获取 proxy 对象下的 push 属性,又因 push 方法会增加数组元素,则需要获取原来的数组长度,设置新索引的元素,再设置新长度。一次操作触发了两次 getter 与 两次 setter。

以上两个问题 Vue 3 是如何解决的?带着疑问开始阅读源码。

注意

写作过程中发现尤大对 Vue 3 的 reactivity 有适当的重构,先按照旧的代码理顺逻辑,再看看这部分重构优化了什么。如果你急于知道结果,可以选择直接跳到这里,或者查看这次提交 (opens new window) 的信息及详细修改内容。

vue3-diff

# reactive

响应式相关的逻辑被放置于 packages/reactivity 目录下,其中有一些核心且高频的 API 在独立的 ts 文件中实现。首先讲讲:reactive。

packages / reactivity / src / reactive.ts

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 (readonlyToRaw.has(target)) {
    return target
  }
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

rawToReactive 及 reactiveToRaw 是全局范围的两个 WeakMap 映射表,分别存储「原始数据 -> 响应式数据」及「响应式数据 -> 原始数据」。

在调用 reactive 时,先判断该对象是只读数据,马上返回,随后进入真正创建响应式对象的函数:createReactiveObject。

function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target already has corresponding Proxy
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  if (!canObserve(target)) {
    return target
  }
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

前面对用户传进来的数据进行一些处理,若 target 已经有对应的响应式对象,或 target 本身已经是响应式对象,则无需处理直接返回;否则调用 30 行,创建一个新的代理对象,并写入 reactiveToRaw 和 rawToReactive 两个映射表中。new Proxy 传入的 handlers 可以参考 Vue 2 做的事情,最基础的无非是两件事:定义 getter 及 setter。getter 用于收集依赖,setter 用于消息分发。

我们暂以 baseHandlers 为例,看下 Vue 3 做了什么。

# createGetter

packages / reactivity / src / baseHandlers.ts


























 




 






function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    const targetIsArray = isArray(target)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    const res = Reflect.get(target, key, receiver)
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    if (shallow) {
      track(target, TrackOpTypes.GET, key)
      // TODO strict mode that returns a shallow-readonly version of the value
      return res
    }
    if (isRef(res)) {
      if (targetIsArray) {
        track(target, TrackOpTypes.GET, key)
        return res
      } else {
        // ref unwrapping, only for Objects, not for Arrays.
        return res.value
      }
    } else {
      track(target, TrackOpTypes.GET, key)
      return isObject(res)
        ? isReadonly
          ? // need to lazy access readonly and reactive here to avoid
            // circular dependency
            readonly(res)
          : reactive(res)
        : res
    }
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

如无意外,数组、对象仍然进行了区分。我们先跳过 4 - 6 行,看非数组下的处理。如果获取代理对象上对应 key 的值 res 不是数组,也不是经过 Vue 3 包装过的值(Ref),那么会再次遍历该值进行响应式处理(track,追踪,Vue 3 的依赖收集调用的方法之一);如果 res 仍然是对象,说明原来的 target 是嵌套对象数据,需要递归调用 reactive(res)。这一点与 Vue 2 很不同。

Vue 2 是在数据声明的时候,就对嵌套对象递归调用 defineReactive 处理成响应式(见此处),而 Vue 3,是在嵌套对象外层 getter 触发时,发现对应的值仍然时对象,才继续递归,真正做到了 按需监听、按需响应式。

举个例子

对于嵌套对象 const obj = { a: { b: { c: 1 }}}:

  • Vue 2:遍历obj,为 obj.a 定义 getter、setter;随后发现 obj.a 的值是对象,遍历 obj.a,为 obj.a.b 定义 getter、setter;又发现 obj.a.b 也是对象,继续遍历 obj.a.b,最终在所有对象( {c: 1}、{ b: { c: 1 }}、{ a: { b: { c: 1 }}} ) 上均定义了一个 observer。
  • Vue 3:遍历 obj,为 obj.a 定义 getter、setter,完成。随后若访问 obj.a.b (或更深层时),触发 obj.a 的 getter,发现 obj.a.b 是对象,递归为 obj.a.b 定义 getter、setter……若再也不访问,只有根属性被定义为响应式。

因此,使用 Vue 3 时再也不怕一个巨大的嵌套对象被 Vue 递归监听了,只要代码中没有访问该巨大嵌套对象的深层属性,Vue 3 只会监听根属性。

这样一来,解开了 前面例子 中的第一个问题。你可以尝试将上面的例子中的 getter 作如下修改,再查看结果。

get(target, key, receiver) {
  console.log('get value:', key)
+ const res = Reflect.get(target, key, receiver)
+ return typeof res === 'object' ? reactive(res) : res
- return Reflect.get(target, key, receiver)
},
1
2
3
4
5
6

# track

packages / reactivity / src / effect.ts

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (dep === void 0) {
    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
      })
    }
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

如前文所说,track 是依赖收集的入口方法,由它来连接数据与数据变化后的响应函数。Vue 3 在全局范围内维护了一个名为 targetMap 的数据映射,这个映射的 key 为需要代理的原始数据 target,值为一个名为 depsMap 的 Set,depMap 的值为一系列的 effect 函数(后文详述,见effect)。

举个例子

const target = {
  name: 'z3rog'
}
const observed = reactive(target)
const render = () => {
  const name = observed.name
  console.log('name:', name)
}

effect(render)
1
2
3
4
5
6
7
8
9
10

上述代码调用了 reactive、effect,构建出的 targetMap 为

// targetMap is a WeakMap
// key of targetMap is value passed into reactive()
// value of targetMap is a Set collection which elements are what passed into effect()
{
  {name: 'z3rog'}: Set(1) {
    0: () => {
      const name = observed.name
      console.log('name:', name)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

Vue 3 使用 effect 代替了 Vue 2 中的 Watcher,当数据发生变化时,不再经由 dep 作消息分发找到对应的 watcher 响应变化,而是通过全局的 targetMap 直接找到对应的 depsMap,depsMap 中记录的就是当前数据发生变化后的响应函数集合。

逻辑理顺了,我们回过头继续看 baseHandlers 中对 setter 的处理

# createSetter

packages / reactivity / src / baseHandlers.ts
























 

 







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 = 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) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

忽略掉 createSetter 中对于深浅(shallow or not)的判断以及一些容错性代码,关注 19 行以后的内容。

Vue 会先判断传入的 target 是否与代理对象匹配,再判断当前传入的 key 是否是 target 自身的属性而非原型链上的。若前者为否直接被忽略,直接返回 20 行的 result,该值为一开始 createGetter 时创建的代理值;后者为否,说明为新增属性,需要增加对该新增属性的监听,若后者为是,则判断对应的 key 上的值是否发生变化(hasChanged(value, oldValue)),变化则触发一次 set。

这其实回答了 一开始 数组的例子中引入的疑问:对于数组的操作多次触发 setter,Vue 是怎么处理的呢?答案是:通过判断传入的 key 值是否是新增属性、key 对应的值是否发生变化,按需调用 trigger 方法,并为 trigger 方法传入对应的操作标识(TriggerOpTypes.ADD / TriggerOpTypes.SET),让 trigger 方法可以根据特定的数据变更的类型来响应特定的变化。

接下来需要看看,trigger 函数做了什么事情

# trigger

packages / reactivity / src / effect.ts

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>()
  const computedRunners = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    // ...
  }

  if (type === TriggerOpTypes.CLEAR) {
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    if (key !== void 0) {
      add(depsMap.get(key))
    }
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))
    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  const run = (effect: ReactiveEffect) => {
    // ...
  }

  computedRunners.forEach(run)
  effects.forEach(run)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

上面代码对中 run 及 add 方法做了省略,我们从第 9 行开始关注整个大的流程。

还记得 targetMap 与 depsMap 吗(不记得再回顾一下)?在数据发生变化时(不管是新增 key 还是修改原 key 上的值),trigger 方法首先从 targetMap 中拿到 target 的 depsMap,然后对操作类型(TriggerOpTypes)和 target 类型(是否是数组)做了一些判断,视情况将 depsMap 或 depsMap 的元素传递给 add 方法调用。最后 computedRunners 和 effects 分别遍历,并调用 run。

根据类型声明我们不看具体代码直接猜测

add 方法用于将 depsMap 集合中的 effect 元素分类至 computedRunners 或 effects 中这两个新的集合中,最后按「先遍历 computedRunners 后遍历 effectes」的顺序依次对两个集合的元素调用 run 方法,从而实现视图更新。

# effect

前面对 effect 有作简单介绍。其实 effect 就是一些可能含有副作用的函数的封装,封装后通过在 effect 中挂载一些属性或方法来实现依赖收集、原始(raw)函数的调用、开发阶段为 track 和 trigger 增加回调用于调试等等功能,甚至如果你对 Vue 3 内部的调度过程非常熟悉,还可以在 options 中传入 scheduler 来自定义你的调度行为。

effect 接收一个函数 fn 和一个接口为 ReactiveEffectOptions 的 options 作为参数,返回一个接口为 ReactiveEffect 的对象,看具体代码前我们先看看这两个 interface:

// interface in effect.ts
export interface ReactiveEffect<T = any> {
  (...args: any[]): T
  _isEffect: true     // effect 的标记
  id: number          // effect id
  active: boolean     // 是否是处于 active 状态的 effect
  raw: () => T        // 最初传入的需要被处理为响应式的函数
  deps: Array<Dep>    // 依赖列表
  options: ReactiveEffectOptions // 调用 `effect(fn, options)` 传入的 options,options 可选参数见下
}

export interface ReactiveEffectOptions {
  lazy?: boolean      // 是否是 lazy 的 effect(类似于 computed 的延迟性 effect)
  computed?: boolean  // 是否是 computed
  scheduler?: (job: ReactiveEffect) => void  // 自定义的调度器,最终会传入一个 effect(此处被称作 job)作为参数
  onTrack?: (event: DebuggerEvent) => void   // 本地调试时可传入的 onTrack 回调,会在 Vue 调用 track 作依赖收集时触发
  onTrigger?: (event: DebuggerEvent) => void // 本地调试时可传入的 onTrigger 回调,会在 Vue 调用 trigger 作消息分发时触发
  onStop?: () => void // 若 effect 处于 active 状态时被 Vue 的调度器暂停,触发该 onStop 回调
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

我们还是用之前的例子做改造,向 effect 传入第二个参数 options:

const target = {
  name: 'z3rog'
}
const observed = reactive(target)
const render = () => {
  const name = observed.name
  console.log('name:', name)
}

effect(render, {
  onTrack: e => console.log(e),
  onTrigger: e => console.log(e)
})

target.name = 'Roger Leung'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

reactive 方法将 target 对象处理成响应式的 observed,这样当响应式对象 observed 中 name 属性的 getter 被调用时会触发 track 函数,开发环境中会调用 onTrack 回调。你可以回过头看 track 16 行。

同理,target.name = 'Roger Leung' 会触发 trigger 函数,trigger 函数在开发环境中调用 onTrigger 回调。前面的代码省略了这部分,读者可自行查阅源码。

接下来看一下 effect 的实现:

// core implementation in effect.ts
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    // 如果 fn 有 _isEffect 的标记,即传入 fn 是之前经过 effect() 返回的对象(更具体的话是一个函数),获取对象 raw 属性上挂在的原始函数
    fn = fn.raw
  }
  // 根据 fn、options 生成一个 effect 对象
  const effect = createReactiveEffect(fn, options)
  // 如果不是 “懒” effect,会执行一次 effect
  if (!options.lazy) {
    effect()
  }
  return effect
}

export function stop(effect: ReactiveEffect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false
  }
}

let uid = 0

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    if (!effect.active) {
      // 若非 active 的 effect 传入了自定义的调度函数 scheduler,返回 undefined,否则调用一下 raw,目的是触发依赖收集
      return options.scheduler ? undefined : fn(...args)
    }
    // 若 effect 栈中不含有该 effect
    if (!effectStack.includes(effect)) {
      // cleanup 函数用于将 effect.deps 置空
      // 不记得 effect.deps 是什么吗?回顾一下 track 函数吧!
      cleanup(effect)
      try {
        // 开启 tracking
        enableTracking()
        // 当前 effect 加入 effect 栈中
        effectStack.push(effect)
        // 将当前 effect 设置为 active
        activeEffect = effect
        // 调用一下 raw 方法并返回结果
        return fn(...args)
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  // 为 effect 对象挂在一些属性
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

注意

在讲解 effect 之前,已经对 createGetter、track 等函数做了不少介绍,但此处明确一下,只有在真正调用 effect 才开始依赖收集,reactive 中 createGetter 只是提前对「如何收集」做了声明。注意到 effect 函数中调用 fn() 了吗(在前面的例子中,fn 就是 render 函数)?在这个函数被调用时,就会触发函数中各个响应式数据的 getter,真正进入响应式的世界……

# 映射查找优化

在早前的 Vue 版本中,使用了四个 WeakMap 来存储原始值与对应的响应式数据之间的映射:

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
1
2
3
4
5

正如 reactive 所使用的一样。这四个 WeakMap 表的大小会随着数据量的增大而线性增加(尽管会自动被 GC),每一次「原始值与响应式值」、「原始值与只读值」之间的相互查找都是 O(N) 级别的时间复杂度,并不高效。因此尤大在 这次提交 (opens new window) 中进行了重构。原来的代码本质上是想判断当前用户传入的数据是什么类型:是否是响应式数据?是否是只读属性?…… 因此这四个 WeakMap 可以使用内部定义的只读属性来作为标示,查找的时间复杂度将为 O(1):

// 定义一些响应式系统中的只读标识
export const enum ReactiveFlags {
  skip = '__v_skip',
  isReactive = '__v_isReactive',
  isReadonly = '__v_isReadonly',
  raw = '__v_raw',
  reactive = '__v_reactive',
  readonly = '__v_readonly'
}

// 通过往原始数据中添加上述的标识,可以降低「判断这个值是何种类型并找到映射值」的复杂度:
// 1. __v_skip, __v_isReactive, __v_isReadonly 三个布尔指针
// 2. __v_raw, __v_readonly, __v_reactive 三个指针直接指向原始值、只读值、响应式值
interface Target {
  __v_skip?: boolean
  __v_isReactive?: boolean
  __v_isReadonly?: boolean
  __v_raw?: any
  __v_reactive?: any
  __v_readonly?: any
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这样就可以在全局范围内省去四个巨大的映射表。

# computed

packages / reactivity / src / computed.ts

还记得 Vue 2 的 computed 是怎么实现的吗?如果你对 Vue 2 的实现方式有所了解,基本上已经理解了一大半 Vue 3 computed 的逻辑。Vue 2 中,computed 的本质是 lazy watcher,Vue 3 下用 effect 替代了 watcher 这个概念,computed 成为一个 lazy reactive effect。

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    // TODO: getter = ..., setter = ...
  } else {
    // TODO: getter = ..., setter = ...
  }

  let dirty = true
  let value: T
  let computed: ComputedRef<T>

  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      // ...
    }
  })
  computed = {
    __v_isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        // ...
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

computed 在调用 effect 时,往 effect 的 options 传入了这样一个对象:

{
  lazy: true,
  // mark effect as computed so that it gets priority during trigger
  computed: true,
  scheduler: () => {
    // ...
  }
}
1
2
3
4
5
6
7
8

可以看到显式地声明了 lazy: true 及 computed: true。我们前面说了,computed 的本质是 lazy effect,并且 effect 中的代码很明显只判断了 options.lazy 这种情况:如果是 lazy 则不会马上执行 effect(),而只是返回该 effect,因此computed 会延迟进行依赖收集的过程。既然如此,为什么还要单独增加 computed 这个标识呢?先看看第三行中源码的注释,然后我们回过头来看看之前说的 trigger。

const run = (effect: ReactiveEffect) => {
    // ...
  }

computedRunners.forEach(run)
effects.forEach(run)
1
2
3
4
5
6

因为 computed 延迟进行依赖收集的特性,trigger 函数调用时需要避免依赖 computed 属性的 effect 先于 computed 自身被调用,因此 trigger 函数内部才需要先将所有 effect 分类为 computed effect 还是普通的 effect!

TO BE CONTINUED