响应系统的作用与实现 (上)https://developer.aliyun.com/article/1392226
4.6 避免无限递归循环
既会读取 obj.foo 的值,又会设置 obj.foo 的值。会导致无限递归。
effect(() => { // 语句 obj.foo = obj.foo + 1 })
首先读取 obj.foo 的值,这会触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执
行。这样会导致无限递归地调用自己,于是就产生了栈溢出。
解决方案:
可以在 trigger 动作发生时增加守卫条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。
// trigger函数 function trigger(target, key) { const depsMap = bucket.get(target) // target Map if (!depsMap) return; const effects = depsMap.get(key) // effectFn Set const effectToRun = new Set() effects && effects.forEach(effectFn => { // 增加守卫条件 if (effectFn !== activeEffect) { // trigger触发执行的副作用函数如果和当前正在执行的副作用函数一样,就不触发执行 effectToRun.add(effectFn) } }) effectToRun && effectToRun.forEach(fn => { if (typeof fn === 'function') fn() }) }
4.7 调度执行
可调度性是响应系统非常重要的特性。首先我们需要明确什么是可调度性。所谓可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。
利用 Set 数据结构的自动去重能力
const data = { foo: 1 } let activeEffect,// 当前被激活的副作用函数 effectStack = [], // 副作用函数栈 jobQueue = new Set() // 任务队列,通过Set自动去重相同的副作用函数 const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap const p = Promise.resolve() // 使用promise实例将任务添加到微任务队列 let isFlushing = false // 是否正在刷新队列 function flushJob() { if (isFlushing) return // 如果正在刷新,则什么也不做 isFlushing = true // 正在刷新 p.then(() => { // 将副作用函数的执行放到微任务队列中 jobQueue.forEach(effectFn => effectFn()) // 取出任务队列中的所有副作用函数执行 }).finally(() => { isFlushing = false // 重置刷新标志 }) } function effect(fn, options = {}) { const effectFn = () => { // 副作用函数执行之前,将该函数从其所在的依赖集合中删除 cleanup(effectFn) // 当effectFn执行时,将其设置为当前激活的副作用函数 activeEffect = effectFn effectStack.push(activeEffect) // 将当前副作用函数推进栈 fn() // 当前副作用函数结束后,将此函数推出栈顶,并将activeEffect指向栈顶的副作用函数 // 这样:响应式数据就只会收集直接读取其值的副作用函数作为依赖 effectStack.pop() activeEffect = effectStack[effectStack.length - 1] } effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合 effectFn.options = options // 将用户传进来的options挂载到副作用函数effectFn上 effectFn() } function cleanup(effectFn) { for (let i = 0, len = effectFn.deps.length; i < len; i++) { let deps = effectFn.deps[i] // 依赖集合 deps.delete(effectFn) } effectFn.deps.length = 0 // 重置effectFn的deps数组 } const obj = new Proxy(data, { get(target, p, receiver) { track(target, p) return target[p] }, set(target, p, value, receiver) { target[p] = value trigger(target, p) // 把副作用函数取出并执行 return true } }) // track函数 function track(target, key) { if (!activeEffect) return // 没有正在执行的副作用函数 直接返回 let depsMap = bucket.get(target) if (!depsMap) { // 不存在,则创建一个Map bucket.set(target, depsMap = new Map()) } let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数 if (!deps) { // 不存在,则创建一个Set depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将副作用函数加进去 // deps就是当前副作用函数存在联系的依赖集合 // 将其添加到activeEffect.deps数组中 activeEffect.deps.push(deps) } // trigger函数 function trigger(target, key) { const depsMap = bucket.get(target) // target Map if (!depsMap) return; const effects = depsMap.get(key) // effectFn Set const effectToRun = new Set() effects && effects.forEach(effectFn => { // 增加守卫条件 if (effectFn !== activeEffect) { // trigger触发执行的副作用函数如果和当前正在执行的副作用函数一样,就不触发执行 effectToRun.add(effectFn) } }) effectToRun && effectToRun.forEach(fn => { if (fn.options.scheduler) { // 该副作用函数选项options中的调度器函数存在 fn.options.scheduler(fn) } else { // 如果不存在scheduler调度函数,则直接调用副作用函数 fn() } }) } effect( () => { console.log(obj.foo) }, { scheduler(fn) { // 每次调度时, 将副作用函数添加到任务队列中。注意:同一个副作用函数加进去会由于jobQueue是Set而去重 // 当宏任务完成后,值已经是最终状态,中间状态的值不会通过副作用函数体现出来 jobQueue.add(fn) // 调用flushJob刷新队列 flushJob() }, }) obj.foo++ obj.foo++ console.log(`over`)
4.8 计算属性 computed 与 lazy
实现懒执行的副作用函数,并且能够拿到副作用函数的执行结果
const data = { foo: 1, bar: 2 } let activeEffect,// 当前被激活的副作用函数 effectStack = [], // 副作用函数栈 jobQueue = new Set() // 任务队列,通过Set自动去重相同的副作用函数 const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap const p = Promise.resolve() // 使用promise实例将任务添加到微任务队列 let isFlushing = false // 是否正在刷新队列 function flushJob() { if (isFlushing) return // 如果正在刷新,则什么也不做 isFlushing = true // 正在刷新 p.then(() => { // 将副作用函数的执行放到微任务队列中 jobQueue.forEach(effectFn => effectFn()) // 取出任务队列中的所有副作用函数执行 }).finally(() => { isFlushing = false // 重置刷新标志 }) } function effect(fn, options = {}) { const effectFn = () => { // 副作用函数执行之前,将该函数从其所在的依赖集合中删除 cleanup(effectFn) // 当effectFn执行时,将其设置为当前激活的副作用函数 activeEffect = effectFn effectStack.push(activeEffect) // 将当前副作用函数推进栈 const res = fn() // lazy选项,getter函数,执行的结果res // 当前副作用函数结束后,将此函数推出栈顶,并将activeEffect指向栈顶的副作用函数 // 这样:响应式数据就只会收集直接读取其值的副作用函数作为依赖 effectStack.pop() activeEffect = effectStack[effectStack.length - 1] return res;// 将函数的结果传递出去,配合lazy选项 } effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合 effectFn.options = options // 将用户传进来的options挂载到副作用函数effectFn上 if (options.lazy) { // lazy的话就把副作用函数返回出去 return effectFn }else { // 否则就立即执行该副作用函数 effectFn() } } function cleanup(effectFn) { for (let i = 0, len = effectFn.deps.length; i < len; i++) { let deps = effectFn.deps[i] // 依赖集合 deps.delete(effectFn) } effectFn.deps.length = 0 // 重置effectFn的deps数组 } const obj = new Proxy(data, { get(target, p, receiver) { track(target, p) return target[p] }, set(target, p, value, receiver) { target[p] = value trigger(target, p) // 把副作用函数取出并执行 return true } }) // track函数 function track(target, key) { if (!activeEffect) return // 没有正在执行的副作用函数 直接返回 let depsMap = bucket.get(target) if (!depsMap) { // 不存在,则创建一个Map bucket.set(target, depsMap = new Map()) } let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数 if (!deps) { // 不存在,则创建一个Set depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将副作用函数加进去 // deps就是当前副作用函数存在联系的依赖集合 // 将其添加到activeEffect.deps数组中 activeEffect.deps.push(deps) } // trigger函数 function trigger(target, key) { const depsMap = bucket.get(target) // target Map if (!depsMap) return; const effects = depsMap.get(key) // effectFn Set const effectToRun = new Set() effects && effects.forEach(effectFn => { // 增加守卫条件 if (effectFn !== activeEffect) { // trigger触发执行的副作用函数如果和当前正在执行的副作用函数一样,就不触发执行 effectToRun.add(effectFn) } }) effectToRun && effectToRun.forEach(fn => { if (fn.options.scheduler) { // 该副作用函数选项options中的调度器函数存在 fn.options.scheduler(fn) } else { // 如果不存在scheduler调度函数,则直接调用副作用函数 fn() } }) } // 传递给effect函数注册的才是真正的副作用函数(getter),effectFn是包装过后的函数 // 通过执行包装后的effectFn函数可以得到副作用函数的结果,下面为obj.foo+obj.bar的结果 // const effectFn = effect( // () => obj.foo + obj.bar, // 将传递给effect的函数当做getter函数,该getter函数可以返回任何值 // { // lazy: true // } // ) // const value = effectFn() // console.log(value) function computed(getter) { // 缓存设置 let value, dirty = true // true意味着脏,则需要重新调用effectFn进行计算得到结果 const effectFn = effect(getter, { lazy: true, scheduler(fn) { // fn() // 此处看控制台 // const res = fn() // 此处要不要fn()都无所谓,因为不会产生影响,computed是一个计算属性,副作用函数是个getter // console.log('res', res) dirty = true // 通过调度器,将dirty设为脏 // computed依赖的响应式数据变化时,手动调用trigger函数触发响应 trigger(obj, 'value') } }) const obj = { get value() { // value属性是一个getter,当被obj.value时就会执行包装的副作用函数effectFn得到getter副作用的结果 if (dirty) { value = effectFn() dirty = false } if(activeEffect) { // 当读取value时,手动调用track函数进行追踪 track(obj, 'value') } return value } } return obj } const o = computed(() => { console.log('effect Fn') return obj.foo + obj.bar }) console.log(o.value) obj.foo++ console.log(o.value) console.log('-----------------------------------') effect(() => { console.log('另一个effect调用computed计算属性') console.log(o.value) }) obj.foo++
实现计算属性并且添加缓存功能
const data = { foo: 1, bar: 2 } let activeEffect,// 当前被激活的副作用函数 effectStack = [], // 副作用函数栈 jobQueue = new Set() // 任务队列,通过Set自动去重相同的副作用函数 const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap const p = Promise.resolve() // 使用promise实例将任务添加到微任务队列 let isFlushing = false // 是否正在刷新队列 function flushJob() { if (isFlushing) return // 如果正在刷新,则什么也不做 isFlushing = true // 正在刷新 p.then(() => { // 将副作用函数的执行放到微任务队列中 jobQueue.forEach(effectFn => effectFn()) // 取出任务队列中的所有副作用函数执行 }).finally(() => { isFlushing = false // 重置刷新标志 }) } function effect(fn, options = {}) { const effectFn = () => { // 副作用函数执行之前,将该函数从其所在的依赖集合中删除 cleanup(effectFn) // 当effectFn执行时,将其设置为当前激活的副作用函数 activeEffect = effectFn effectStack.push(activeEffect) // 将当前副作用函数推进栈 const res = fn() // lazy选项,getter函数,执行的结果res // 当前副作用函数结束后,将此函数推出栈顶,并将activeEffect指向栈顶的副作用函数 // 这样:响应式数据就只会收集直接读取其值的副作用函数作为依赖 effectStack.pop() activeEffect = effectStack[effectStack.length - 1] return res;// 将函数的结果传递出去,配合lazy选项 } effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合 effectFn.options = options // 将用户传进来的options挂载到副作用函数effectFn上 if (options.lazy) { // lazy的话就把副作用函数返回出去 return effectFn }else { // 否则就立即执行该副作用函数 effectFn() } } function cleanup(effectFn) { for (let i = 0, len = effectFn.deps.length; i < len; i++) { let deps = effectFn.deps[i] // 依赖集合 deps.delete(effectFn) } effectFn.deps.length = 0 // 重置effectFn的deps数组 } const obj = new Proxy(data, { get(target, p, receiver) { track(target, p) return target[p] }, set(target, p, value, receiver) { target[p] = value trigger(target, p) // 把副作用函数取出并执行 return true } }) // track函数 function track(target, key) { if (!activeEffect) return // 没有正在执行的副作用函数 直接返回 let depsMap = bucket.get(target) if (!depsMap) { // 不存在,则创建一个Map bucket.set(target, depsMap = new Map()) } let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数 if (!deps) { // 不存在,则创建一个Set depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将副作用函数加进去 // deps就是当前副作用函数存在联系的依赖集合 // 将其添加到activeEffect.deps数组中 activeEffect.deps.push(deps) } // trigger函数 function trigger(target, key) { const depsMap = bucket.get(target) // target Map if (!depsMap) return; const effects = depsMap.get(key) // effectFn Set const effectToRun = new Set() effects && effects.forEach(effectFn => { // 增加守卫条件 if (effectFn !== activeEffect) { // trigger触发执行的副作用函数如果和当前正在执行的副作用函数一样,就不触发执行 effectToRun.add(effectFn) } }) effectToRun && effectToRun.forEach(fn => { if (fn.options.scheduler) { // 该副作用函数选项options中的调度器函数存在 fn.options.scheduler(fn) } else { // 如果不存在scheduler调度函数,则直接调用副作用函数 fn() } }) } // 传递给effect函数注册的才是真正的副作用函数(getter),effectFn是包装过后的函数 // 通过执行包装后的effectFn函数可以得到副作用函数的结果,下面为obj.foo+obj.bar的结果 // const effectFn = effect( // () => obj.foo + obj.bar, // 将传递给effect的函数当做getter函数,该getter函数可以返回任何值 // { // lazy: true // } // ) // const value = effectFn() // console.log(value) function computed(getter) { // 缓存设置 let value, dirty = true // true意味着脏,则需要重新调用effectFn进行计算得到结果 const effectFn = effect(getter, { lazy: true, scheduler(fn) { // fn() // 此处看控制台 // const res = fn() // 此处要不要fn()都无所谓,因为不会产生影响,computed是一个计算属性,副作用函数是个getter // console.log('res', res) dirty = true // 通过调度器,将dirty设为脏 // computed依赖的响应式数据变化时,手动调用trigger函数触发响应 trigger(obj, 'value') } }) const obj = { get value() { // value属性是一个getter,当被obj.value时就会执行包装的副作用函数effectFn得到getter副作用的结果 if (dirty) { value = effectFn() dirty = false } if(activeEffect) { // 当读取value时,手动调用track函数进行追踪 track(obj, 'value') } return value } } return obj } const o = computed(() => { console.log('effect Fn') return obj.foo + obj.bar }) console.log(o.value) obj.foo++ console.log(o.value) console.log('-----------------------------------') effect(() => { console.log('另一个effect调用computed计算属性') console.log(o.value) }) obj.foo++
4.9 watch 的实现原理
所谓 watch,其本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。
watch 的实现本质上就是利用了 effect 以及 options.scheduler 选项。