一、前言
本文是# 深入源码彻底搞清vue3中reactive和ref的区别的衍生篇,我们继续从源码入手,去解读vue3中的track()
依赖收集以及trigger()
派发更新。
在阅读本文前不知道你是否已经明白依赖收集
以及派发更新
的具体作用。当然不明白也没关系,本文会先语义话的讲一讲这两者的概念,这样能有一个基本的理解。接着我们在深入源码去看一看这两者的具体实现,废话不多说,进入正文(u‿ฺu✿ฺ)
二、track()
依赖收集
在我们日常开发中,当我们在template
中使用响应式变量,并且改变这些值时,vue总能及时的监听到这些变化并重新渲染相关的组件,那这是怎么做到的呢?
其实我们大家都知道在vue3中若要实现响应式数据
需要通过reactive
或ref
去定义我们的值(reactive和ref详情可点击)。而在reactive
中则是通过get
去拦截我们对数据的读取操作,在这个拦截读取的过程中我们会先将数据通过effect
包裹一层然后给它收集起来,这个过程就是依赖收集
。
至于为什么要收集起来,是不是只有收集起来了,到时候改这个数据的时候vue是不是才能知道你改的是哪个数据,然后才能去重新渲染相对于的组件。
接着我们看看在vue3中是如何去实现这个过程的吧
effect() 副作用函数
关于effect
的具体作用,大家可以看看# 基于Proxy从0到1实现响应式数据这篇文章。
源码地址:packages/reactivity/src/effect.ts
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
// 如果已经是effect副作用函数,则返回副作用的原始函数
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
const _effect = new ReactiveEffect(fn)
// 创建`effect`
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
// 若不是延迟执行,则立即执行一次副作用函数(计算属性lazy为true)
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
// 返回副作用函数
return runner
}
这里可以看到通过new ReactiveEffect
创建了一个_effect
实例
const _effect = new ReactiveEffect(fn)
通过bind
函数返回一个新的副作用函数runner
,这个新函数的this被指定为_effect
,并将_effect
添加到这个新副作用函数的effect
属性上,最后返回这个新副作用函数。
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
// 返回副作用函数
return runner
ReactiveEffect
我们可以看到在effect
副作用函数内最终返回的runner
指向ReactiveEffect
的run
方法
// 临时存储响应式函数
const effectStack: ReactiveEffect[] = []
// 依赖收集栈
const trackStack: boolean[] = []
// 最大嵌套深度
const maxMarkerBits = 30
代码语言:javascript复制export class ReactiveEffect<T = any> {
run() {
// 如果effect非激活状态,则返回原始副作用函数执行后的结果(fn就是被effect包裹的原始函数)
if (!this.active) {
return this.fn()
}
// 如effect不在effectStack栈中
if (!effectStack.includes(this)) {
try {
// 当调用 effect 注册副作用函数时, 将副作用函数赋值给 activeEffect
effectStack.push((activeEffect = this))
// 在调用副作用函数之前将当前副作用函数压入栈中
enableTracking()
// 记录递归深度位数
trackOpBit = 1 << effectTrackDepth
// 如果 effect 嵌套层数没有超过 30 层
if (effectTrackDepth <= maxMarkerBits) {
// 给依赖打标记,for循环遍历 _effect 实例中的 deps 属性,
// 给每个 deps 的 w 属性标记为 trackOpBit 的值
// deps就是用来存储所有与该副作用函数相关联的依赖集合
initDepMarkers(this)
} else {
// 清除当前 effect 相关依赖
cleanupEffect(this)
}
return this.fn()
} finally {
// 完成依赖标记
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
// 恢复到上一级
trackOpBit = 1 << --effectTrackDepth
// 重置依赖收集状态
resetTracking()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,
// 并把 activeEffect 还原为之前的值(始终让activeEffect 指向栈顶的副作用函数)
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}
}
run
方法内在每次effrct
执行时会将activeEffect
设置为当前激活的副作用函数并且入栈,待副作用函数执行完毕后会将其从栈中弹出,并始终让activeEffect
指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现相互影响的情况。
track()
track
就是依赖收集器,负责把依赖收集起来统一放到一个依赖管理中心
具体则是以 key 为维度,将每一个 key 关联的副作用函数收集起来,存放在一个 Set 数据结构中,并以键值对的形式存储在 depsMap 的 Map 结构中
代码语言:javascript复制/**
* @description:
* @param {target} 目标对象
* @param {type} 收集的类型(GET = 'get', HAS = 'has', ITERATE = 'iterate')
* @param {key} 触发 track 的 object 的 key
*/
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 不启用依赖收集 则直接return
if (!isTracking()) {
return
}
// targetMap 依赖管理中心,用于收集依赖和触发依赖
// 在 targetMap 中获取对应的 target 的依赖集合
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果 target 不在 targetMap 中,则新建一个,并初始化 value 为 new Map()
targetMap.set(target, (depsMap = new Map()))
}
// deps 来收集依赖函数,当监听的 key 值发生变化时,触发 dep 中的依赖函数
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
这里我们已经成功将依赖收集了起来,接下来就是看当我们修改响应式数据时,vue是如何知晓并更新的
三、trigger() 派发更新
trigger
是 track
收集的依赖对应的触发器,也就是负责根据映射关系,获取响应式函数,再派发通知 triggerEffects
进行更新
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) {
// 收集被清除
// 触发目标效果
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// 如果类型为数组,并且数组的 length 改变时
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// 如果 key 不是 undefined,就添加对应依赖到队列,比如新增、修改、删除
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// 根据type类型 处理新增、修改、删除逻辑
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
}
}
// 到这里就拿到了 targetMap[target][key],并存到 deps 里
// 接着是要将对应的 effect 取出,调用 triggerEffects 执行
// 开发环境赋值 eventInfo
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
// deps内只有一个effect时直接取出执行,多个依赖则循环执行
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
这里根据映射关系已经将依赖函数
全部收集至了deps
中,接着就是遍历deps
取出effect
副作用函数并执行
triggerEffects()
这里就是派发更新
的最后一步更新
,从deps
中取出effect
副作用函数并执行,这样也就达到了响应式更新的作用。
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// 遍历 effect 的集合函数
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
// 有 scheduler 则执行
if (effect.scheduler) {
effect.scheduler()
} else {
// 执行 effect 副作用函数
effect.run()
}
}
}
}
四、总结
看完本篇文章不知道你对依赖收集
以及派发更新
是否已经有了一个基本的认识,那这里呢我们就最后在总结一下依赖收集
以及派发更新
的全过程。
在读取我们的响应式数据时,响应式数据的get
会拦截我们的读取操作,并通过track()
进行依赖收集。在track()
内会使用ReactiveEffect
将我们的原始副作用函数注册为统一的effect
副作用函数并存入targetMap
(存储副作用函数的桶,WeakMap数据结构)中,targetMap
的键是原始对象 target
,值是一个Map实例
,而Map的键是原始对象 target 的key
,值是一个由副作用函数组成的Set
这样一个完整的映射关系就建立了。当我们监听的key
发生变化时,就可以通过映射关系取出相对应的依赖函数
就是effect
,取出后就可以调用triggerEffects
并执行effect.run()
,这样就实现了派发更新
至此,本篇文章就结束了,如有问题欢迎各位大佬指导。若文内有任何不理解的地方也欢迎评论区提问~