写在前面
effect 可以说是 vue3.0 依赖收集和触发的关键,众多 api 都或多或少的使用到了这个 api。故此,对它的源码进行了一些阅读,本文为个人学习记录,有错误的地方希望各位大神指出。
effect 源码
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) { // 是 effect 的 runner 的话就直接给重载到 fn.raw ,然后包装返回
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) { // 不是懒加载的话就直接执行 effect,是的话需要自己手动执行一次,进行依赖收集
effect()
}
return effect
}
源码使用 ts 写的,所以有一些类型声明,下面是去掉类型声明的代码:
export function effect(
fn,
options
) {
if (isEffect(fn)) { // 是 effect 的 runner 的话就直接给重载到 fn.raw ,然后包装返回
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) { // 不是懒加载的话就直接执行 effect,是的话需要自己手动执行一次,进行依赖收集
effect()
}
return effect
}
流程解析
- 先判断传入的
fn是否已经是effect函数(调用isEffect进行判断)。如果是,则重载 fn 的值为 fn.raw (raw 属性保存了原始的fn) - 调用
createReactiveEffect根据配置去生成最终的effect。 options.lazy(如果此配置项为true,则需要自己手动调用去收集依赖) 不存在则执行effect,主要用来收集依赖,可以在对应依赖改变时触发执行effect- 将生成的
effect返回
流程图如下
上面的步骤其实逻辑很少,真正的逻辑都是在 createReactiveEffect 之中完成的,下面开始看这个函数的实现
createReactiveEffect 实现
**注:**以后的代码会直接将类型声明去掉
function createReactiveEffect(
fn,
options
) {
const effect = function reactiveEffect(...args) {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(...args) // 如果传入的回调函数有返回值,则将返回值返回出去
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
effect.id = uid++ // 标记每个 effect 的唯一 id
effect._isEffect = true // 标记当前的 effect 的类型,主要用于类型检测 (isEffect)
effect.active = true // 标记当前 effect 有没有被 stop
effect.raw = fn // 记录生成 effect 的源头函数
effect.deps = [] // 和当前 effect 依赖同一个数据的 effect
effect.options = options // 传入的配置项
return effect // 返回生成的 effect
}
流程解析
- 创建
effect函数 - 给
effect挂载各种后续需要的属性
这段代码中的重点是 reactiveEffect,下面对它进行解析
function reactiveEffect(...args) {
if (!effect.active) { // 如果 effect 被 stop,此时如果 options.scheduler 存在则返回 undefined 否则执行回调 fn
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) { // 表示当前 effect 没有正在被调用
cleanup(effect) // 将 effect 从 effect.deps 数组中移除,不然无法触发配置 onTrack
try {
enableTracking() // 表示可以执行 track(进行依赖收集)shouldTrack= true
effectStack.push(effect) // 将当前执行的 effect 加入正在执行的 effect 栈中
activeEffect = effect // 将当前执行的 effect 标记为正在执行的 effect
return fn(...args) // 如果传入的回调函数有返回值,则将返回值返回出去,执行对应回调
} finally {
effectStack.pop() // effect 执行完毕,将其推出栈
resetTracking() // 重置 shouldTrack,以便下一次 effect 调用
activeEffect = effectStack[effectStack.length - 1] // 重置当前执行的 effect
}
}
}
- 从上面的代码中可以看到,其实整个处理过程比较简单,常规的流程走一遍,做种使注册的回调执行,以来的数据更新。
- 其实这其中还涉及到了两个关键函数
track和tigger,前者是在首次执行 effect 时收集依赖,后者是在响应式的值改变时去触发对应数据的effect。 - 各种原理就是数据的
get会触发track,数据的set会触发tigger去执行对应数据的effect,更新依赖的值
track 收集依赖
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) { // shouldTrack 为true 表示需要追踪 activeEffect 表示当前不是在 effect() 执行环境中
return
}
let depsMap = targetMap.get(target) // 获取 targetMap 所有有依赖 effect 的属性
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // 获取依赖当前值的 `effect`
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) { // 注意,这里 activeEffect 可能是一个 effect 或者是 undefined
dep.add(activeEffect) // 添加依赖
activeEffect.deps.push(dep) // 将依赖记录在 effect.deps 中
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({ // 触发 options.onTrack
effect: activeEffect,
target,
type,
key
})
}
}
}
流程解析
- 此函数的流程也不复杂,首先判断当前环境中的公有变量
shouldTrack和activeEffect是否允许进行依赖收集。 - 从
targetMap(公有变量) 中查找是否有当前target的记录(这里记录的是target每个key以来的effects)。有的话取出,没有就新建 - 和查找
target依赖的过程类似,不同的是这里查找的具体的target.key的依赖的effect。 - 接下来
dep.has(activeEffect)就对应了reactiveEffect中的clearup调用来从effect.deps中移除自身的逻辑,这里可以看到,这么做主要是在开发模式下触发options.onTrack。
下面是流程图
tigger
响应式的值改变时执行对应数据记录的 effect
function trigger(
target,
type,
key,
newValue,
oldValue,
oldTarget
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// 如果 target 没有被记录过,直接返回
return
}
const effects = new Set() // 保存需要执行的 effect
const add = (effectsToAdd) => {
if (effectsToAdd) { // 执行的时候需要判断这个 effect 是否存在(还记得 tarck 中可能保存的 effect 是 undefined)
effectsToAdd.forEach(effect => {
// 当前正在执行的 effect 不添加,防止重复执行
if (effect !== activeEffect || !shouldTrack) {
effects.add(effect)
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}
// 执行的是清楚任务,也就是把某个 target 要被删除
if (type === TriggerOpTypes.CLEAR) {
// 在某个数据被清除之前执行所有的 effect
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) { // 如果这里是给数组的 length 赋值
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(
))
}
/**
* 下面的判断逻辑是当 map set object array 在 add,delete 和 set 属性时,如果有依赖是获取他们的 keys ,length 和 size 时,因为属性值得改变。
* 这些值也需要作出对应的改变。
*/
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map) // 对 map 数据执行 set 操作
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
const run = (effect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
流程分析
tigger的难点在于当有值是依赖数据的size和length,keys是的判断可能会比较绕,看的时候对一点耐心。- 下面是 1 中提到部分的流程图