往期回顾
- 系列开篇与响应式基本实现
- effect 函数注册副作用
- 建立副作用函数与被操作字段之间的联系
- 封装 track 和 trigger 函数
- 分支切换与 cleanup
- 嵌套的 effect 与 effect 栈
- 避免无限递归循环
基础调度执行
上一节中我们解决了无限递归循环的问题。
本节将为我们的响应式系统实现调度执行。这一功能十分重要,是后续实现 computed 和 watch 的基础。
思路
可调度性
可调度性是响应式系统非常重要的特性。
所谓可调度,指的是当响应式数据变化触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。
我们可以为 effect 函数设计一个选项参数 options,允许用户指定调度器;当 trigger 动作触发副作用函数执行时,把副作用函数交给调取器执行,从而把控制权交给用户。
js
effect(
// 副作用函数
() => {
/* ... */
},
// options 参数
{
// 调度器 scheduler 是一个函数
scheduler(effectFn) {
/* ... */
}
}
)
代码
js
// 用一个全局变量存储被注册的副作用函数
let activeEffect
// 新增 effect 栈
const effectStack = []
// effect 函数用于注册副作用函数
// 新增参数 options,用来指定调度器(options 对象的 scheduler 属性)
function effect(fn, options = {}) {
// 包装真实副作用函数的函数,包含清除和再收集逻辑
const effectFn = () => {
// 调用 cleanup 完成清除工作
cleanup(effectFn)
// effectFn 每次执行都会重新进行一次依赖收集,并且被收集的副作用函数就是它自己
activeEffect = effectFn
// 调用真实副作用函数之前,先将当前 effect 压入栈中
effectStack.push(effectFn)
// 在 fn 这次执行中,effectFn 会作为副作用函数被收集到 fn 读取了的依赖的集合中
fn()
// 当前副作用函数执行完毕之后,弹出栈
effectStack.pop()
// 并把 activeEffect 还原为之前的值
activeEffect = effectStack[effectStack.length - 1]
}
// 将 options 挂载到 effectFn 上
effectFn.options = options
// effctFn.deps 数组用来储存该副作用函数的所有依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
function cleanup(effectFn) {
// 遍历 deps 数组
effectFn.deps.forEach( deps => {
// 把副作用函数从集合中删除
deps.delete(effectFn)
})
// 重置副作用函数的依赖集合,因为之后要再重新收集一次的
effectFn.deps.length = 0
}
// 存储副作用函数的桶
const bucket = new WeakMap()
// 原始数据
const data = { text1: 'text1', text2: 'text2' }
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 把副作用函数收集到桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表设置操作成功
return true
}
})
// track 函数 在 get 拦截函数中被调用,用来追踪副作用函数
function track(target, key) {
// 没有 activeEffect 直接 return
if (!activeEffect) return target[key]
// 根据 target 从"桶"中取得 depsMap,也是Map类型:key --> effects
let depsMap = bucket.get(target)
// 如果不存在 depsMap,就新建一个 Map 并与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 再根据 key 从 depsMap 中取得 deps。
// deps是一个 Set 类型,储存所有与当前 key 相关联的副作用函数
let deps = depsMap.get(key)
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 最后将当前激活的副作用函数添加到"桶"里
deps.add(activeEffect)
// 也将这个集合添加到副作用函数的 deps 数组中
activeEffect.deps.push(deps)
}
// trigger 函数 在 set 拦截函数中被调用,用来触发更新
function trigger(target, key) {
const depsMap = bucket.get(target)
// 如果这个对象没有被追踪的依赖,没有需要重新运行的副作用函数,直接 return
if (!depsMap) return
const effects = depsMap.get(key)
// 新建空集合存储本次触发更新要执行的副作用函数
const effectsToRun = new Set()
// 如果当前 activeEffect 在依赖集合里,本次触发更新不执行它
effects?.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果一个副作用函数存在到调度器,则调用该调度器,并将副作用函数作为参数传入
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
已实现
我们给 effect 函数新增了一个参数 options,用 options 的 scheduler 属性来允许用户指定执行副作用函数的调度器,实现了响应式系统的可调度性。
缺陷/待实现
下一节中,我们将同样利用 options 参数,实现懒执行的副作用函数。