Vue3源码阅读——响应式是如何实现的(track + trigger篇)
前言
本文属于笔者Vue3源码阅读系列第五篇文章,往期精彩:
响应式部分源码已经产出两篇文章了,本文是最后一篇响应式相关的文章,主要内容为触发更新的过程分析,顺带看下track
函数(之前漏了)。
track
/**
* Tracks access to a reactive property.
*
* This will check which effect is running at the moment and record it as dep
* which records all effects that depend on the reactive property.
*
* @param target - Object holding the reactive property.
* @param type - Defines the type of access to the reactive property.
* @param key - Identifier of the reactive property to track.
*/
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
这个函数用于 track
对象中key 的依赖。有三个参数:
target
就是数据对象。TrackOpTypes
track
操作类型,有以下三种。export const enum TrackOpTypes { GET = 'get', // target.key HAS = 'has', // key in target ITERATE = 'iterate' // 遍历 }
key
对应数据对象的属性名,如果是数组,那就是索引。
track
中主要逻辑就是在确定dep
参数,确定了以后再调用trackEffects
,关于trackEffects
在上一篇文章中已经阅读过了,文章都是连起来的,不建议跳读哦!
大致逻辑如下:
- 判断
shouldTrack && activeEffect
,只有为true
才会track
; - 先从
targetMap
中获取这个target
对应的depsMap
,targetMap
是一个全局的WeakMap
对象,用于存对象对应的dep
。如果depsMap
为空,则初始化一个new Map()
进去。然后接着在depsMap
中根据key
找到这个key
对应的dep
,如果没有,就初始化为createDep()
,createDep
也在上一篇文章中分析过,他会返回dep = new Set(); dep.w = 0; dep.n = 0
;确定了dep
对象后就可以调用trackEffects
了。看下targetMap
的定义:
根据这个逻辑,我们能够得到// The main WeakMap that stores {target -> key -> dep} connections. // Conceptually, it's easier to think of a dependency as a Dep class // which maintains a Set of subscribers, but we simply store them as // raw Sets to reduce memory overhead. type KeyToDepMap = Map<any, Dep> const targetMap = new WeakMap<any, KeyToDepMap>()
targetMap
的一个大致结构,如下图所示:
targetMap
还是一个WeakMap
,顺便聊聊使用 WeakMap
的意义。
targetMap 使用 WeakMap 的意义
先说一下Map
,我们都知道Map
和Object
很类似,不同点在于Map
的key
可以是任何类型。而Object
的key
只能是字符串。
WeakMap
的特点如下:
key
只能是Object
,除了null
之外,typeof obj === 'object'
的都可以当做key
。WeakMap
没有遍历的方法。WeakMap
对key
是弱引用,即垃圾回收机制不将该引用考虑在内,举个例子:const e1 = document.getElementById('foo'); const e2 = document.getElementById('bar'); const arr = [ [e1, 'foo 元素'], [e2, 'bar 元素'], ]; // 不需要 e1 和 e2 的时候 // 必须手动删除引用 arr [0] = null; arr [1] = null; // 不需要处理引用 const wm = new WeakMap() wm.set(e1, 1) wm.set(e2, 2)
WeakMap
对value
还是正常的引用。
targetMap
使用 WeakMap
的意义就在于对垃圾回收更有利。接着分析下触发更新的过程。
trigger
/**
* Finds all deps associated with the target (or a specific property) and
* triggers the effects stored within.
*
* @param target - The reactive object.
* @param type - Defines the type of the operation that needs to trigger effects.
* @param key - Can be used to target a specific reactive property in the target object.
*/
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 还是从targetMap获取depsMap,如果获取不到
// 代表从来没有track过,直接return
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
// 用于存需要trigger的dep
let deps: (Dep | undefined)[] = []
// 如果是Map|Set 的clear操作
// depsMap中的所有dep都需要去trigger
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
}
// 如果key是length,并且target是Array
// 只需要 trigger length对应的dep 和 索引大于等于 newlength对应的dep
// const arr = reactive([1,2,3,4]),在template中使用到了arr
// 当执行 arr.length = 3 就会走到这里,deps就会有两项
// 一个是length对应的dep,一个是索引3对应的dep
else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
} else {
// 处理 SET | ADD | DELETE 这三种 TriggerOpTypes
// schedule runs for SET | ADD | DELETE
// key 不为 undefined,从 depsMap 获取一下这个 key 对应的 dep,并添加到deps
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
// ADD TriggerOpTypes
case TriggerOpTypes.ADD:
// target不是Array
if (!isArray(target)) {
// 添加depsMap.get(Symbol(__DEV__ ? 'iterate' : ''))
// Map|Set 的size、forEach、keys(only Set)、values、entries
// depsMap中depsMap.get(ITERATE_KEY),就有值
deps.push(depsMap.get(ITERATE_KEY))
// 如果是Map
if (isMap(target)) {
// 添加depsMap.get(Symbol(__DEV__ ? 'Map key iterate' : ''))
// 只有当用到了Map 的 keys
// depsMap中depsMap.get(MAP_KEY_ITERATE_KEY),才有值
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// 当key是正整数,数组中加了新的索引,那就直接trigger length的dep
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE
// DELETE TriggerOpTypes
// 此处的处理逻辑和 ADD TriggerOpTypes一致
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
// SET TriggerOpTypes
if (isMap(target)) {
// 如果是Map,添加depsMap.get(Symbol(__DEV__ ? 'iterate' : ''))
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
// 如果deps中只有一个dep,直接triggerEffects
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
// 有多个dep,先遍历去掉undefined的项
// 并将所有dep中的effect都添加到effects
// 然后调用createDep(effects),这将返回包含effect的dep实例
// 然后调用triggerEffects
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
当我们更改了target
中的属性,可能是新增、删除、清空等,就会触发trigger,从trigger
的逻辑不难看出来,它和track
很像,大部分的逻辑都是在确定dep
,与track
不同的是,trigger
时dep
可能有多个,确定了deps
再调用triggerEffects
。源码中我们已经添加了很多注释,接下来我们分析一下为啥要这么写:
- 从
targetMap
获取depsMap
,如果获取不到,代表从来没有track
过,直接return
。 - 如果是
Map|Set
的clear
操作,depsMap
中的所有dep
都需要去trigger
,这个很好理解,对象被清空了,所以任何用到对象中任何一项的dep
都需要被trigger
。 - 如果
key
是length
,并且target
是Array
,只需要trigger
length
对应的dep
和 索引大于等于newlength
对应的dep
。这个也很好理解,索引大于等于newlength
的被删了,length
变了。
const arr = reactive([1,2,3,4])
function onClick() {
arr.length = 3
}
// 在template中使用到了arr
// 当执行 arr.length = 3,deps 就会有两项
// 一个是length对应的dep,一个是索引3对应的dep
-
最后一个分支是处理
ADD|SET|DELETE
。先deps.push(depsMap.get(key))
,对于ADD
操作来说,这个肯定是undefined
,不过SET|DELETE
则可能有对应的dep
,然后针对ADD|SET|DELETE
分别处理。 -
处理
ADD
操作时,区分了数组、非数组:数组:当
key
是正整数,数组中索引增加了,那就直接trigger
length
的dep
```js const arr = reactive([1,2,3,4]) // 以下操作都会触发此处的ADD逻辑 function onClick() { arr.push(6) // arr.splice(arr.length, 0, 6) // arr[4] = 6 // ...... } ```
非数组:先将
key
为ITERATE_KEY
的dep
放进去(不管有没有),因为最后会把undefined
的过滤掉;如果target
是Map
,还要将key
为MAP_KEY_ITERATE_KEY
的dep
放进去。咱们来详细分析下为啥要这样处理。首先来看下什么情况能够track
到ITERATE_KEY
和MAP_KEY_ITERATE_KEY
,看下图:
如上图所示,如果在副作用函数中用到以上图中对象的一些属性或者方法,就会track
到key
为ITERATE_KEY
或MAP_KEY_ITERATE_KEY
的dep
。当trigger
ADD
操作时,可能是Map|WeakMap|Set|WeakSet
的ADD
,也可能是Object
的新增属性,但是不论是任何一种,都会影响对象的遍历结果,因此需要trigger
key
为ITERATE_KEY
或MAP_KEY_ITERATE_KEY
的dep
。不知道你们会不会想到一个问题——为啥map的keys()那么特殊,要单独用一个key? -
处理
DELETE
操作时,可能是Map|WeakMap|Set|WeakSet
的delete
,也可能是删除Object
的属性,但是不论是任何一种,都会影响对象的遍历结果,因此需要trigger
key
为ITERATE_KEY
或MAP_KEY_ITERATE_KEY
的dep
。与处理ADD
操作的非数组逻辑一致。 -
处理
SET
操作时,有两种情况会走到这里,一种是obj.xxx = 'newValue'
,这种情况在一开始已经处理了——deps.push(depsMap.get(key))
; 另一种是Map
的set
(Set
没有set
操作),Map
的set
操作影响的是map对象
除了keys()
之外的遍历操作(value、entries、forEach、Symbol.iterator
),因此只需要trigger
key
为ITERATE_KEY
的dep
。我相信看到这里解释了上面的问题,因为map
的set
不会改变keys()
的结果,如果map.keys()
也track
key
为ITERATE_KEY
的dep
,此时调用map.set
会造成不必要的trigger
。Vue
的源码永远细节满满👍🏻
我相信如果你看到这里,trigger
的逻辑肯定看明白了,接下来咱们就看 triggerEffects
:
triggerEffects
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
// 没看懂为什么这里要判断dep是不是数组,因为就不可能是数组,而且不是数组为啥要把dep里的effects放到数组,Set也可以for of的,可能是历史原因,还没改过来吧!
const effects = isArray(dep) ? dep : [...dep]
// 先 trigger computed的effect,先执行computed的副作用函数
// 因为其他的副作用函数可能依赖computed的value
for (const effect of effects) {
if (effect.computed的effect) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
// 再trigger别的effect
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
针对这一行多余的代码const effects = isArray(dep) ? dep : [...dep]
,我给vuejs
提了issue
:
如果被成功合并到主分支,那咱们的issue
还是有意义的:
接下来看下triggerEffect
的逻辑:
triggerEffect
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// 如果 effect 不等于 全局activeEffect 或者 effect.allowRecurse
if (effect !== activeEffect || effect.allowRecurse) {
// 开发模式,有effect.onTrigger的话,执行effect.onTrigger
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
// 调度执行器副作用函数
if (effect.scheduler) {
effect.scheduler()
} else {
// 直接调用run
effect.run()
}
}
}
triggerEffect
就是负责调用 effect.run
,不过调用的方式有所不同,如果有调度器,就会通过调度器来调用,没有就直接调用。当我触发一个变更,导致了组件的渲染函数重新执行,组件的渲染函数就是通过调度器执行的:
// create reactive effect for rendering
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
下图是调用堆栈:
从调用堆栈我们能够看到effect.run
会异步执行。这块咱们后续详细阅读吧。
Promise.then(effect.run)
最后,咱们通过一个简单的例子,画图来串一下track
和trigger
的完整过程。
结合例子串联响应式逻辑
结合一个例子来看可能会更好理解:
// Main.vue
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const visible = ref(true)
const count = ref(0)
function onClick() {
visible.value = !visible.value
}
</script>
<template>
<div>
<div v-if="visible">{{ count }}</div>
<button @click="onClick">click</button>
<Child :visible="visible"></Child>
</div>
</template>
// Child.vue
<template>
<div>{{ props.visible }}</div>
</template>
<script setup>
const props = defineProps(['visible'])
</script>
在上面的例子中,Main.vue
的渲染 mainEffect.run
会 track visible
和 count
对应的 dep
;Child.vue
的渲染 childEffect.run
会 track visible
对应的dep
,如下图所示:
当点击一次 click
按钮,visible.value = false;
此时将触发mainEffect.run
,由于 visible
为 false
,将不会 track count
对应的 dep
;childEffect
对象的 deps
则不会有什么影响,如下图所示:
以上图示就是 track
和 trigger
的完整过程。
总结
回顾一下本文主要内容:
track
。trigger
。triggerEffects
。triggerEffect
。track
和trigger
的完整过程分析。
这是笔者第五篇源码分析类的文章,如果掘友们有什么建议,或者文中有错误,还请评论指出,谢谢!
如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【赞
】都是我创作的最大动力 ^_^
。
转载自:https://juejin.cn/post/7252283213187432505