回顾Vue2 的响应式
上图来自官方文档,初始化时对状态数据做了劫持,在执行组件的render函数时,会访问一些状态数据,就会触发这些状态数据的getter,然后render函数对应的render watcher就会被这个状态收集为依赖,当状态变更触发setter,setter中通知render watcher执行 update,然后render函数重新执行以更新组件。 就这样完成了响应式的过程。
Vue3 的响应式从 Reactive 开始
上篇文章,从源码的最入口处开始,一步步寻找响应式的起源。
最终,在下面代码里, instance.data = reactive(data)
找到对 data 数据的响应式化处理。
export function applyOptions(instance: ComponentInternalInstance) {
// code...
const {
// state
data: dataOptions
} = options
// code...
if (dataOptions) {
const data = dataOptions.call(publicThis, publicThis)
if (!isObject(data)) {
__DEV__ && warn(`data() should return an object.`)
} else {
instance.data = reactive(data) // 这里!!!
}
}
}
Vue3使用Proxy
来实现数据的劫持,接下来我们进入源码(packages/reactivity/src/reactive.ts)
如果是只读,就这是返回 target
,不做任何处理。
createReactiveObject:
createReactiveObject
代码主要做了一些校验,然后最终 new Proxy(...)
并返回。
- 如果 target 不是 Object 就直接返回不做响应式处理;
- 如果目标对象已经是个 proxy 了就直接返回 target ;
主要逻辑在 new Proxy 的第二个参数里,也就是拦截方法里。如果tagetType 是集合类型(Map、Set、WeakMap、WeakSet这些),就用 collectionHandlers,否则常规的就是 baseHandlers。
接下来按照常规 baseHandlers 继续阅读下去。
baseHandlers:
baseHandlers
是作为createReactiveObject
的第三个参数传入的,找到 mutableHandlers
,
mutableHandlers(packages/reactivity/src/baseHandlers.ts):
继续找到 MutableReactiveHandler
这里面已经拦截了 ` set、deleteProperty、has、ownKeys`。
基类 BaseReactiveHandler里拦截了 get
:
下面按照顺序分别细看下具体的代理拦截逻辑。
Proxy.get
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
shallow = this._shallow
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) { // 是否是特定数组方法
debugger
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key) // 收集依赖
}
if (shallow) {
// 如果是浅响应式,只收集第一层依赖,后面的直接返回了
return res
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
// 如果是数组并且key 是整数就直接返回 res,否则返回 res.value
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
// 如果res是对象,那么就接着进行响应式处理,并返回代理对象递归处理,
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
target 是需要代理的数据源,key 是访问target 的 key。
- 首先处理了下特定的几个 key,ReactiveFlags.xxx
- if (targetIsArray && hasOwn(arrayInstrumentations, key)) 。如果是数组,并且 key是 arrayInstrumentations 的方法,就 return Reflect.get(arrayInstrumentations, key, receiver)。arrayInstrumentations的方法下面会列出。
- 继续往下执行,
const res = Reflect.get(target, key, receiver)
,拿到 res 的值,如果 key 是一个 Symbol类型的值并且 在内置builtInSymbols
里,或者isNonTrackableKeys(key) 为 true
,就直接返回 res,不需要进行接下来的依赖收集; - 如果不是只读, ` track(target, TrackOpTypes.GET, key)`,进行依赖收集。这里的 track 方法是进行依赖收集的,后面会列出。
- 如果是浅响应式,只收集第一层依赖,后面的直接返回了;
- 如果是数组并且key 是整数就直接返回 res,否则返回 res.value;
- 如果res是对象,那么就接着进行响应式处理,并返回代理对象递归处理;
- 最后返回 res。
至此,proxy.get 方法拦截结束,主要是根据 key 的类型进行特定返回,然后进行依赖收集,判断是否是shallow,是否是 ref ,是否是对象,最终返回 res 结果。
get 方法遗留有两个问题,一个是 track 方法的定义,这个后面一起讲,这里做个标记。
另一个就是 arrayInstrumentations。
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
debugger
const arr = toRaw(this) as any
// 为什么这里需要遍历下再 track?
// 因为这3 个数组方法,第二个参数 key是可以指定开始遍历的位置从 key 开始。
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
// 排除参数也是个响应式变量,toRaw 后继续执行一遍
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 为什么需要先暂停依赖收集,等执行完后再恢复收集?
// 解释例子:vue/examples/reactive/arrayGet.html
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
return instrumentations
}
createArrayInstrumentations方法,对 数组的一些方法进行了重写
- 对’includes’, ‘indexOf’, ‘lastIndexOf’ 这3 个方法的重写,首先对数组的所有项进行依赖收集,type 是 get,key 为 index;然后执行方法,返回 index,如果 找到,再将参数toRaw 后继续执行一遍,排除参数也是个响应式变量;
- 对’push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’这 5 个方法的重写,主要就是先暂停依赖收集,等结果执行完毕后再恢复依赖收集,为什么如此操作?可以看下面的这个例子:
// 看一个例子
const arr = ['h','e','l','l']
const proxyArr = new Proxy(arr, {
get(target,key) {
console.log('get', key)
return Reflect.get(arr, key)
},
set(target,key, value) {
console.log('set', key)
return Reflect.set(target, key, value)
}
})
proxyArr.push('o')
// 结果如下:
// get push
// get length
// set 4
// set length
// 我们能够看到,调用了一次push,会触发一次length的get,一次length的set,而且刚好被重写的这些API都是会改变数组length的API。
// 这样的话,假如我们没有对这些API重写,在effect中使用这些API会怎么样:
// const obj2 = reactive(arr)
// effect(() => {
// obj2.push('o')
// })
// effect(() => {
// obj2.push('!')
// })
/**
第1个effect:
收集key='length',触发track(target, ..., 'length')操作
相当于proxy[5]=o,触发key='5' 以及 key='length' 的 trigger 操作
第2个effect:
收集key='length',触发track(target, 'length')操作
相当于proxy[6]=!,触发key='6'以及key='length'的trigger操作
由于第1个effect收集了key='length',因此会触发第1个effect重新执行,再次收集key='length'和触发key='7'以及key='length'的trigger操作;
由于第2个effect收集了key='length',因此会触发第2个effect重新执行,再次收集key='length'和触发key='7=8'以及key='length'的trigger操作;
由于第1个effect收集了key='length',因此会触发第1个effect重新执行,再次收集key='length'和触发key='9'以及key='length'的trigger操作;
引起了死循环......
*/
Proxy.set
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!this._shallow) {
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
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 =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: 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
// 如果target是原型链上的东西,不触发更新
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
}
- 如果oldValue是只读的Ref,但是value(即将设置的值)不是Ref,直接return false。
target === toRaw(receiver)
表示target不是原型链上的东西,就是其本身。 为什么会判断下上面这个?举个例子:const obj = {a:1} proxy = new Proxy(obj, { get(target,key, receiver){ console.log(key, receiver === proxy) } }) obj.a // 这时候的打印结果为:a true 因为第 3 个参数 receiver 是Proxy 或者继承 Proxy 的对象 const obj2 = Object.create(proxy) obj2.a // 打印结果:a false。虽然通过原型链也能代理,但是这时候 receiver 已经指向obj2 了。
- 如果没有 hadKey 是 false,说明是新增操作,trigger 触发更新,type 是 ADD
- 如果 hadKey是 true,并且 oldValue 和 value 不相等,说明有修改,trigger 触发更新,type 是 SET
trigger 方法的定义,这个后面一起讲,这里做个标记。是触发页面更新。
Proxy.deleteProperty
// 用于拦截从target删除某个属性
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
}
剩下的几个就比较简单了,上述是拦截从 target 上删除某个属性,并触发更新 trigger,type 为 DELETE
Proxy.has
has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
// 不能是Symbol类型 或者 不能是特定的几个 symbols 类型 ,如果操作 has 就触发依赖收集
track(target, TrackOpTypes.HAS, key)
}
return result
}
用于拦截判断某个key是否存在于target的操作(xxx in target),先调用Reflect.has拿到结果,然后判断如果这个key不是Symbol类型,则调用track收集依赖;如果是Symbol类型,那就判断这个key在不在builtInSymbols中,不在也调用track收集依赖,type 为 HAS。
Proxy.ownKeys
ownKeys(target: object): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY
)
return Reflect.ownKeys(target)
}
for in… 或者 for of… 拦截,track收集依赖,type 为 ITERATE or ITERATE_KEY
总结下,到目前为止,Reactive 的模块结束了。 主要就是 new Proxy ,并做了一些拦截操作,在拦截里收集和触发依赖,收集依赖用 track 方法,依赖变化触发更新用 trigger 方法。
再看看 ref 的实现
目前为止,我们还不知道依赖到底是什么。
带着这个问题,看下 ref 的源码实现(packages/reactivity/src/ref.ts)
Ref 真正的实现,在 RefImpl 这个类里面。从源码里也可以看出,ref 的实现并没有用 proxy,而是采用 ES2015 类的方法进行实现,所以打包降级后还是通过 Object.defineProperty对value的访问进行拦截
RefImpl
class RefImpl<T> {
private _value: T // 存 value
private _rawValue: T // 存 原始 value
public dep?: Dep = undefined // 存依赖
public readonly __v_isRef = true // 是否是 Ref 对象
constructor(
value: T,
public readonly __v_isShallow: boolean
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this) // 收集依赖
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
RefImpl 类有几个构造属性,_value 和 _rawValue 是私有属性,分别存放 新的 value 值和之前的 value 值。
dep 存放依赖。劫持了 value 属性的获得和设置,并且同样也做了 get 依赖收集,set 触发更新。但是和 reactive 不同,不是用的 track/trigger 方法,而是 trackRefValue/triggerRefValue。
trackRefValue
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) { // activeEffect??
ref = toRaw(ref)
if (__DEV__) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep || (ref.dep = createDep())) // 创建一个空的依赖存放地,并触发收集
}
}
}
这里面有两个变量shouldTrack和activeEffect,记录下,后面应该会讲到。
trackEffects
方法,应该也是收集依赖的一个方法。可以也记录下。
ref.dep = createDep()
创建一个空的依赖存放地,并保存到 ref.dep 里。
triggerRefValue
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
if (__DEV__) {
triggerEffects(dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(dep)
}
}
}
拿到ref.dep,并且 触发更新,triggerEffects方法,参数是 dep 依赖。
上面两个方法,都用到了 dep。下面看下 dep 究竟是什么,从 createDep
方法开始看起。
createDep
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
dep.w = 0 // wasTracked
dep.n = 0 // newTracked
return dep
}
这方法很简单,如果不传参数,就是创建一个空的 Set 作为 dep,并且设置 dep.w和 dep.n,这两个属性,是为了做性能优化用的。做个标记,现在只需要知道dep 有这两个属性就行。
到目前为止,ref 的实现也没有了,遗留几个问题:
- shouldTrack和activeEffect是什么
- trackEffects和triggerEffects方法的定义
- dep.w 和 dep.n 是干什么用的
Effect.ts
下面从哪条分支开始看呢?还是继续createDep方法看能不能看出点啥。
目前为止,虽然我们知道 dep 是个 Set 集合,但也仅仅而已,还是不清楚究竟 dep 的类型长啥样。 ts 的优势体现出来了。
const dep = new Set<ReactiveEffect>(effects) as Dep
可以看到,这个 Set 类型是 ReactiveEffect
的集合。
从这个类型入手,可以认为ReactiveEffect
就是封装依赖的对象。
ReactiveEffect
ReactiveEffect
是个类,这个类有个 run 实例方法。联想到 vue2 的 watch 类,同样也有个 run 方法。
在 Vue2
中,有一个 Watcher
类,其种类包括 RenderWatcher
(渲染Watcer)、UserWatcher
(用户定义的watch选项)、ComputedWatcher
(用户定义的computed选项)。在对数据进行响应式处理时,数据会持有 Dep
实例,并且将这些 Watcher
实例添加到 subs
中;当依赖的数据变化,通过 dep.notify()
通知 subs
中的每个 Watcher
实例,Watcher
实例收到通知后再去做相应的处理。由此可见 Watcher
类的作用就是封装各种 Watcher 的处理逻辑。可以说在 Vue2 中 Watcher实例
就是依赖。
同样的 ReactiveEffect
的作用与 Watcher
差不多。
全局搜下这个,有哪些地方 new
了这个类。
感觉和 vue2 的 new Watcher
的使用地方也一致。
具体看下实现:
ReactiveEffect
类的构造函数有几个比较重要的属性,
- fn,这是第一个参数,后面执行 run 方法的时候会直接调用它
- scheduler,这是一个调度器,vue3 源码里面有大量的这种调度器,作为第二个构造参数。这里做个标记,后面会补充到。
run方法:
run() {
if (!this.active) {
// 如果是暂停状态,就只执行 fn 后直接返回,不收集依赖
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth // effectTrackDepth表示当前 run 执行深度
// 至于maxMarkerBits为什么是 30?
// 是因为trackOpBit的取值是 1 左移 effectTrackDepth位,js 采用的计数是 32 位的有符号二进制表示,第 1 位是符号位,
// 所以最多只能左移30 位。
// 该 优化方案是 3.2 以后的优化方案,3.2 版本之前的,统一走cleanupEffect方法
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this) // deps[i].w |= trackOpBit 给dep.w 做标记。 标记优化
} else {
cleanupEffect(this) // 删除所有依赖,清空deps。然后执行 fn 重新开始收集。这样重新收集所有依赖对性能要求较高
}
return this.fn() // 执行 fn,如果 fn 中有使用到响应式变量的 get,就会触发收集函数 track
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this) // 进一步处理和优化 debs 数组
}
// 还原操作
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
这个run
的作用就是用来执行副作用函数fn
,并且在执行过程中进行依赖收集:
在 renderder.ts 中,有这样一段代码(组件初始化的时候会执行到此):
组件初始化的时候,会手动执行 update
方法一次,目的是为了收集依赖。
而这个 update
方法执行的是 effect.run
方法,而 effect
又是 new ReactiveEffect
的。所以,再来看
ReactiveEffect
代码。
假设我们的组件结构是这样的。
<parent>
<child>
</child>
</parent>
-
如果当前effect实例是不工作状态,就仅仅执行一下fn,不需要收集依赖。
-
由于在一个effect.run的过程中可能会触发另外的effect.run, 暂存上一次的activeEffect、shouldTrack,目的是为了本次执行完以后把activeEffect、shouldTrack恢复回去。
-
找到最顶层 parent。
-
设置activeEffect shouldTrack,回答上面了一个标记(activeEffect是什么),activeEffect其实就是ReactiveEffect中的 this,也就是依赖本身。shouldTrack是一个全局标记,表示是否需要开始进行依赖收集。这里表示需要进行依赖收集。
-
trackOpBit = 1 « ++effectTrackDepth,effectTrackDepth是个全局变量,表示当前 调用 effect.run 的深度,trackOpBit 更新为 1 « effectTrackDepth。effectTrackDepth和trackOpBit的关系如下:
effectTrackDepth(effect.run 嵌套调用深度) trackOpBit 1 0001«1=0010=2 2 0001«2=0100=4 3 0001«3=1000=8 -
effectTrackDepth 是否小于全局的 maxMarkerBits = 30,如果是就调用 initDepMarkers 给 dep.w 做标记(优化方案),否则就把依赖都删除(简单方案)。
-
执行 fn()
-
finally中主要是做一些善后的工作了:移除多余依赖、恢复activeEffect、shouldTrack、调用–effectTrackDepth & trackOpBit更新。其中 finalizeDepMarkers方法也是优化方案的一部分,后面讲优化会讲到。
非优化方案cleanupEffect
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
如果 effectTrackDepth > maxMarkerBits,采用上述方案,每次采集依赖的时候,都会删除所有依赖,清空deps。然后执行 fn 重新开始收集。这样重新收集所有依赖很有可能有重复采集依赖,即之前采集过。3.2 版本之前的,统一走cleanupEffect方法。
为什么maxMarkerBits 是 30?
这是因为 js 采用的计数是 32 位的有符号二进制表示,第 1 位永远是符号位,层级加 1 左移1 位,最后一位不算。如果用二进制表示层级数的话,最多只能表示 30 位。
优化方案
initDepMarkers
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit // set was tracked 按位或运算 dep.w |= trackOpBit
}
}
}
遍历 deps,更新 deps[i].w ,与 trackOpBit 做 或 运算,其实相当于给每一层标记下当前 dep 有没有被收集过。
比如 effect.run 到第3层,trackOpBit = 4 ,用二进制表示就是 0100,首次初始化的时候第3层deps[i].w = 0000,执行 完fn 并且收集了依赖,此时 deps[i].n = 0100, deps[i].w = 0000,
finalizeDepMarkers
然后执行到 finally,执行finalizeDepMarkers 方法
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
// 第二次添加依赖的时候,并没有被标记未新依赖,所以需要删除他
dep.delete(effect)
} else {
deps[ptr++] = dep // 这里有个技巧。一次循环,做到删除数组和重新赋值操作。比如原数组是 ['new1', 'discard1', 'discard2','new2'],执行一次循环后, 最后执行deps.length = ptr,得到['new1','new2']
}
// clear bits
// 还原标记
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}
finalizeDepMarkers 方法其实就是做优化了,根据 dep 的 n 和 w 属性,来删除前一次收集到的冗余依赖。
wasTracked(dep) && !newTracked(dep)
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0 // dep.w >= trackOpBit
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0 // dep.n >= trackOpBit
这两个方法的定义,比较简单,就是拿 w 和 n 和trackOpBit做与运算,得到的结果如果 > 0,就表示在当前深度是有被标记位 1 的。
比如:
const switch = ref(true)
const foo = ref('foo')
effect( () = {
if(switch.value){
console.log(foo.value)
}else{
console.log('else condition')
}
})
switch.value = false
当 switch 为 true 时,triggerEffect,双向删除后,执行副作用函数,switch、foo 会重新收集到依赖 effect
当 switch 变成 false 后,triggerEffect,双向删除后,执行副作用函数,仅有 switch 能重新收集到依赖 effect
优化方案总结
-
组件首次初始化的时候会执行effect.run方法,如果有子组件的话,同样也会执行阶段执行子组件的 effect.run 方法,所以就会形成嵌套,用 effectTrackDepth表示当前 run 方法执行的深度,trackOpBit 表示用二进制来存储当前的深度,比如 effectTrackDepth为 2的时候,trackOpBit二进制表示 0100;
-
如果执行深度 <= 30 的话,就采用优化方案,否则采用暴力方案(3.2 版本之前的 vue 统一采用的后者方案);
-
暴力方法,就是每次 run 的时候都把之前的依赖全部清除掉,然后重新执行副作用函数 fn 进行依赖收集,缺点就是会产生很多无谓运算开销;
-
优化方案,每次初始化 dep 依赖的时候,都会有一个 w 和 n 属性,初始化都是 0。每次当前依赖收集进来的时候,就在当前深度上标识 1,同样也用二进制表示,比如当前trackOpBit为 0100,那么标记后的n 就是 0100,w 目前还是 0000。等到下次依赖发生更新需要重新采集的时候(发生在同一个 activeEffec里),就会根据 w 和 n 进行依赖的筛选和删除,然后重新赋值 w 属性,n 清空。
effect.ts 里面还有几个特别重要的方法定义,也是上面做了标记但是还没讲到的 track、trigger 方法
track
回顾下,前面在讲 Reactive的定义里,new Proxy()的 get 拦截就使用到这个方法进行依赖收集。
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
// targetMap 用于保存和当前响应式对象相关的依赖内容,本身是一个 WeakMap类型
// key是响应式对象 value是
let depsMap = targetMap.get(target)
if (!depsMap) {
// value是 depsMap
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep())) // dep是个 set 集合
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
这个函数,有3 个参数:
- target 就是数据对象。
- TrackOpTypes track操作类型,有以下三种。
export const enum TrackOpTypes { GET = 'get', // target.key HAS = 'has', // key in target ITERATE = 'iterate' // 遍历 }
- key 对应数据对象的属性名,如果是数组,那就是索引。
track方法里面定义的变量有点多,主要就是确定 dep,然后执行 trackEffects。而 ref 函数的定义,也是最终执行的是trackEffects 方法。
targetMap 是个全局的 WeakMap,key 是 target,value 是 depsMap,depsMap的 key 是 get 的 key,value 是 dep,如果dep 是空就创建一个 dep,dep 是一个 Set 集合,dep.w = 0; dep.n = 0。
trackEffects
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
debugger
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) { // newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
dep.n |= trackOpBit // set newly tracked dep.n = dep.n | trackOpBit
shouldTrack = !wasTracked(dep) // / 如果前一次收集的 dep 已经被收集过了,就可以跳过本次收集
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
extend(
{
effect: activeEffect!
},
debuggerEventExtraInfo!
)
)
}
}
}
- 优化方案给 dep 加 n属性,;
- 把当前的 activeEffect 加入到 dep 集合中;
- 然后 将 dep 加入到 activeEffect 的 deps中;
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
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
// void 0 === undefined
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
if (deps.length === 1) {
if (deps[0]) {
triggerEffects(deps[0])
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
triggerEffects(createDep(effects))
}
}
上述方法,主要是根据不同的类型,去筛选中 dep 来更新。
-
从targetMap获取depsMap,如果获取不到,代表从来没有track过,直接return。
-
如果是Map Set的clear操作,depsMap中的所有dep都需要去trigger,这个很好理解,对象被清空了,所以任何用到对象中任何一项的dep都需要被trigger。 -
如果key是length,并且target是Array,只需要 trigger length对应的dep 和 索引大于等于 newlength对应的dep。这个也很好理解,索引大于等于newlength的被删了,length变了。
-
最后一个分支是处理ADD SET DELETE。先 deps.push(depsMap.get(key)),对于ADD操作来说,这个肯定是undefined,不过SET DELETE则可能有对应的dep,然后针对ADD SET DELETE分别处理。 -
处理ADD操作时,如果target是数组并且key是正整数,直接trigger length 的 dep,如果是 非数组的情况,先将 key 为 ITERATE_KEY 的 dep 放进去(不管有没有),因为最后会把 undefined 的过滤掉;如果target 是 Map,还要将 key 为 MAP_KEY_ITERATE_KEY 的 dep 放进去。
- 最后一段逻辑,区分了下 deps 数组长度是 1 还是非 1 的情况,我没弄明白是什么意思。本着 vue3 严谨、极致优化的代码风格。我能想到的唯一的作用就是,如果deps 数组只有一项,就直接去执行它进行重渲染。如果非 1,需要把 debs 的每一项合并到一起,目的是为了用 set 做个去重
triggerEffects
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
// 先 trigger computed的effect,先执行computed的副作用函数
// 因为其他的副作用函数可能依赖computed的value
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
// 再trigger别的effect
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
先trigger computed的effect,再trigger别的effect,为了防止死循环,因为别的副作用函数可能依赖 computed 的value。然后执行 triggerEffect
triggerEffect
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
如果 effect.scheduler 存在,就执行它。如果没有,就执行 run方法。
这两个方法是啥呢?如果 vue2 的 watch 的 update,肯定就是更新逻辑了。
而 effect,就是 ReactiveEffect 的实例,scheduler是作为ReactiveEffect的第二个构造参数传进来的,run 方法内部执行的也是ReactiveEffect的第一个构造参数。
所以,这两个方法的执行逻辑,秘密就在哪里实例ReactiveEffect类。
renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
// ... 省略代码
}
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid
update()
}
这个方法,在mountComponent方法里面会调用它。也就是组件初始化的时候。
按照前面 trigger 源码的解释,对应的 run 方法就是第一个参数 componentUpdateFn,这个方法是更新页面渲染逻辑,打个标记后面会重点讲到这个方法,
而scheduler是个调度方法,对应的是 () => queueJob(update)
。update 方法的定义是 (instance.update = () => effect.run())
,执行的是 effect.run
方法。
queueJob
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
queue.push(job)
} else {
// 插队
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
这个方法,vue3 源码里很多地方都用到了。主要就是调度的作用,按照不同类型的队列,来决定怎么执行这个队列里面的方法。上面的例子就是 update 方法。还有个类型队列,可以插队。
总结
至此,响应式的整个流程差不多都走了一遍。
-
Vue 的 data 的响应式逻辑,是通过 reactive 实现的;
-
Reactive 通过 new Proxy 进行拦截的;
-
Ref 方法的实现是通过 Class 拦截 value 的 set 和 get 的;
-
Vue 的依赖收集和触发依赖更新的逻辑都在 effect.ts里,trackEffects 和 triggerEffect;
-
Vue 的依赖Dep 其实就是 ReactiveEffect 的实例,ReactiveEffect类有个 deps 属性,这里面放置的是 dep。组件会在初始化的时候,实例化一个ReactiveEffect实例,watch 和 computed 会分别 实例化一个新的ReactiveEffect。
-
每个 dep 依赖,如果发生改变,会triggerEffect触发更新,执行 update 方法去重新渲染页面,剩下的更新逻辑交给 renderder。