Vue 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
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
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) 的信息及详细修改内容。
# 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
)
}
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
}
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
}
}
}
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)
},
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
})
}
}
}
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)
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)
}
}
}
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
}
}
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)
}
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 回调
}
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'
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
}
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>()
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
}
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
}
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: () => {
// ...
}
}
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)
2
3
4
5
6
因为 computed
延迟进行依赖收集的特性,trigger
函数调用时需要避免依赖 computed
属性的 effect
先于 computed
自身被调用,因此 trigger
函数内部才需要先将所有 effect
分类为 computed effect
还是普通的 effect
!
TO BE CONTINUED