实现可调度执行 — 调度器函数
什么是可调度性?
可调度指的是当 trigger 触发副作用函数重新执行时,提供给使用者决定副作用函数执行的时机、次数和方式。
通过下面的代码举个栗子:
// 获得响应式数据 const data = reactive({ count: 1 }) // 注册副作用函数 effect(() => { console.log(data.count) }) data.count++ console.log('结束了') 复制代码
其对应的数据输出结果为:1 2 '结束了',假设使用者需要的输出顺序是:1 '结束了' 2,那么就需要当前的响应式系统支持 调度。
实现思路
- 给现有的 effect 函数多添加一个可选参数 options,允许使用者指定调度器,例如:
effect(()=>{ console.log(data.count) }, { // 将调度器设置名为 scheduler 的函数 scheduler(fn){ ... } }) 复制代码
- 在 effect 函数中注册副作用函数时,将这个 options 选项挂载到对应的副作用函数上
- 在 trigger 函数触发副作用函数重新执行时,通过直接调用 options 中传入的调度器函数,把控制权移交给使用者
具体代码实现
// 存储副作用函数 const bucket = new WeakMap() // 用于存储被注册的副作用函数 let activeEffect = null // effect 栈 const effectStack = [] // 用于接收并注册副作用函数 function effect(fn, options = {}) { const effectFn = () => { // 先调用 cleanup 函数完成旧依赖的清除工作 cleanup(effectFn) // 保存 fn activeEffect = effectFn // 在副作用函数调用前,将副作用函数入栈 effectStack.push(effectFn) // 执行 fn 函数,目的是初始化执行和触发 get 拦截 fn() // 副作用函数执行完成后出栈 effectStack.pop() // 将 activeEffect 指向栈顶(原先)的副作用函数 activeEffect = effectStack[effectStack.length - 1] } // 将 options 挂载到 effectFn 上 effectFn.options = options // 用于存储所有与其关联的副作用函数的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() } // 清除本次依赖相关的旧副作用函数 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 数组 effectFn.deps.length = 0 } // 响应式数据 function reactive(target) { return new Proxy(target, { get(target, key) { // 没有注册副作用函数,直接返回数据 if (!activeEffect) return Reflect.get(target, key) track(target, key) return Reflect.get(target, key) }, set(target, key, newVal) { target[key] = newVal trigger(target, key) return Reflect.set(target, key, newVal) } }) } // 收集依赖 function track(target, key) { // 从 bucket 获取 depsMap 的依赖关系 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 从 depsMap 获取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将与当前副作用函数存在联系的依赖集合 deps 添加到 activeEffect.deps 数组中 activeEffect.deps.push(deps) } // 触发依赖 function trigger(target, key) { // 获取对应的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 获取对应的 deps const effects = depsMap.get(key) // 构建新的 Set 避免递归 const effectsToRun = new Set() effects && effects.forEach(effectFn => { // 避免递归调用自身 if (effectFn !== activeEffect) effectsToRun.add(effectFn) }) // 是否执行调度器函数 effectsToRun.forEach(effectFn => { // 若副作用函数存在调度器,则调用调度器,并将 effectFn 函数作为参数传递 if (effectFn.options.scheduler) { effectFn.options.scheduler(effectFn) } else { // 否则直接执行副作用函数 effectFn() } }) } 复制代码
基于调度器控制执行次数
为什么需要控制执行次数?
直接通过如下栗子进行解释:
// 获得响应式数据 const data = reactive({ count: 1 }) // 注册副作用函数 effect(() => { console.log(data.count); }) data.count++ data.count++ 复制代码
在没有指定调度器时,以上代码执行后输出结果为:1 2 3,但假设其中的 2 只是个过渡阶段,使用者只关心最后的结果 3,那么执行三次打印操作就是多余的,即期望输出为:1 3
实现思路
- 定义一个任务队列 jobQueue,选择 Set 数据结构,目的是利用它的自动去重功能
- 每次调度执行时,先将当前副作用函数添加到 jobQueue 队列中
- 定义一个 flushJob 函数刷新 jobQueue 队列中的副作用函数
- 其中需要设定一个 isFlushing 表示正在刷新的标志,用于去判断是否需要执行,只有当 isFlushing = false 时才需要执行,保证 flushJob 函数在一个周期内只调用一次
- 最后通过 promise.then 来将刷新 jobQueue 队列的执行添加到微任务队列中
即支持通过如下方式进行调用:
// 获得响应式数据 const data = reactive({ count: 1 }) // 注册副作用函数 effect(() => { console.log(data.count); }, { scheduler(effectFn) { // 将副作用函数添加到 jobQueue 队列中 jobQueue.add(effectFn) // 调用 flushJob 刷新队列,减少不必要的执行 flushJob() } }) data.count++ data.count++ 复制代码
具体代码实现
// 存储副作用函数 const bucket = new WeakMap() // 用于存储被注册的副作用函数 let activeEffect = null // effect 栈 const effectStack = [] // 定义 jobQueue 任务队列 const jobQueue = new Set() // 通过 promise 微任务实现异步执行 const resolvedPromise = Promise.resolve() function nextTick(fn) { return fn ? resolvedPromise.then(fn) : resolvedPromise } // 表示当前是否正在刷新队列 let isFlushing = false // 刷新队列函数 function flushJob() { // 当前正在刷新队列,则直接结束 if (isFlushing) return // 一旦需要执行刷新队列,先将 isFlushing 置为 false isFlushing = true // 在微任务队列中刷新 jobQueue 队列 nextTick(() => { jobQueue.forEach(job => job()) }).finally(() => { // 刷新队列结束后,重置 isFlushing isFlushing = false }) } // 用于接收并注册副作用函数 function effect(fn, options = {}) { const effectFn = () => { // 先调用 cleanup 函数完成旧依赖的清除工作 cleanup(effectFn) // 保存 fn activeEffect = effectFn // 在副作用函数调用前,将副作用函数入栈 effectStack.push(effectFn) // 执行 fn 函数,目的是初始化执行和触发 get 拦截 fn() // 副作用函数执行完成后出栈 effectStack.pop() // 将 activeEffect 指向栈顶(原先)的副作用函数 activeEffect = effectStack[effectStack.length - 1] } // 将 options 挂载到 effectFn 上 effectFn.options = options // 用于存储所有与其关联的副作用函数的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() } // 清除本次依赖相关的旧副作用函数 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 数组 effectFn.deps.length = 0 } // 响应式数据 function reactive(target) { return new Proxy(target, { get(target, key) { // 没有注册副作用函数,直接返回数据 if (!activeEffect) return Reflect.get(target, key) track(target, key) return Reflect.get(target, key) }, set(target, key, newVal) { target[key] = newVal trigger(target, key) return Reflect.set(target, key, newVal) } }) } // 收集依赖 function track(target, key) { // 从 bucket 获取 depsMap 的依赖关系 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 从 depsMap 获取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将与当前副作用函数存在联系的依赖集合 deps 添加到 activeEffect.deps 数组中 activeEffect.deps.push(deps) } // 触发依赖 function trigger(target, key) { // 获取对应的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 获取对应的 deps const effects = depsMap.get(key) // 构建新的 Set 避免递归 const effectsToRun = new Set() effects && effects.forEach(effectFn => { // 避免递归调用自身 if (effectFn !== activeEffect) effectsToRun.add(effectFn) }) // 是否执行调度器函数 effectsToRun.forEach(effectFn => { // 若副作用函数存在调度器,则调用调度器,并将 effectFn 函数作为参数传递 if (effectFn.options.scheduler) { effectFn.options.scheduler(effectFn) } else { // 否则直接执行副作用函数 effectFn() } }) } 复制代码
最后
以上内容都是 Vue.js 内部响应式系统的实现思路,但其内部拥有一个更完善处理机制和边界情况,作为学习者而言了解其设计思路和设计原因其实也足够了,不过作为 coder 不要总是省略动手的过程。