前言
数接上文继续看《Vue.js的设计与实现》,第四章 响应系统的作用与实现部分内容笔记,老实说还有些地方不太通透,需要回头完善下
第四章 响应系统的作用与实现
1.响应式数据与副作用函数
副作用函数:函数的执行会直接或间接影响其他函数的执行
1 2 3 4 5
| const obj = {text:'hello world'} function effect() { //effect 函数的执行会读取 obj.text document.body.innerText = obj.text }
|
2.响应式数据的基本实现
如何实现响应式数据集?
如果我们能拦截一个对象的读取和设置操作,就能实现响应式,ES2015之前通过Object.defineProperty函数实现,这也是vue2.js采用的方式,在ES2015+中,我们可以使用代理对象Proxy来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const bucket = new set()
const obj = {text:'hello world'}
const obj = new Proxy(data,{ get(target,key){ bucket.add(effect) return target[key] } set(target,key,newval) { target[key] = newVal bucket,forEach(fn=>fn()) retrun true } }
|
1 2 3 4 5 6 7 8 9
| function effect() { document.body.innerText = obj.text }
effect()
setTimeout(()=>{ obj.text = 'hello vue3' },1000)
|
如上通过拦截一个对象的读取和设置操作,可以实现一个简单的响应式数据。
并有上面例子,一个响应式系统的工作流程:
当读取操作发生时,将副作用函数收集到‘桶’中;
当设置操作发生时,从‘桶’中取出副作用函数并执行
3.实现一个相对完善的响应式系统
首先,你可以使用 WeakMap 来创建一个存储响应式数据的桶。WeakMap 是一个键值对的集合,其中键是对象,值是任意类型的数据。由于 WeakMap 的键是弱引用,不会对对象的垃圾回收产生影响,适合用于存储响应式数据。
然后,你可以使用 Map 来构建一个依赖收集系统,用于追踪依赖关系。在 Vue 3 中,响应式数据变化时需要通知相关的依赖进行更新,而 Map 可以帮助我们记录依赖关系,并在数据变化时触发更新。
如何使用 WeakMap 和 Map 来实现一个简单的响应系统?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| const reactiveMap = new WeakMap();
const depMap = new Map();
function reactive(obj) { if (reactiveMap.has(obj)) { return reactiveMap.get(obj); }
const proxy = new Proxy(obj, { get(target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); trigger(target, key); return result; }, });
reactiveMap.set(obj, proxy); return proxy; }
function track(target, key) { const depKey = getDepKey(target, key); if (depMap.has(depKey)) { const deps = depMap.get(depKey); if (!deps.has(activeEffect)) { deps.add(activeEffect); } } else { const deps = new Set([activeEffect]); depMap.set(depKey, deps); } }
function trigger(target, key) { const depKey = getDepKey(target, key); const deps = depMap.get(depKey); if (deps) { deps.forEach(effect => { effect(); }); } }
function getDepKey(target, key) { return `${target}-${key}`; }
let activeEffect = null;
function effect(fn) { activeEffect = fn; fn(); activeEffect = null; }
const state = reactive({ count: 0 });
effect(() => { console.log(`Count: ${state.count}`); });
state.count++;
|
我们使用了 reactive
函数来实现对象的响应化,track
函数用于收集依赖,trigger
函数用于触发更新。我们还使用了 effect
函数来包裹需要追踪依赖关系的代码块。
当 state.count
发生变化时,trigger
函数会触发更新,并执行相关的依赖函数,从而实现响应式的更新机制。
总结来说,使用 WeakMap 结合 Map 可以很好地构建一个相对完善的响应系统,在 Vue 3 中可以更高效地管理响应式数据和依赖关系。
4.分支切换与cleanup
分支切换导致冗余副作用函数进行不必要的更新。vue3中为了解决这个问题,我们需要在每次副作用函数执行之前,清除上一次建立的响应联系(笔记有待完善,没太懂这一块的)
5.避免无限递归循环
假如在同一个副作用函数中同时读取和设置某个响应式数据的值,会产生什么结果呢?
1 2 3
| effectRegister(() => { objProxy.count = objProxy.count + 1 })
|
浏览器控制台报错了,出现无限递归循环,栈溢出了。
原因很明显了,首先读取objProxy.count,并把副作用函数存储到依赖中,紧接着又修改objProxy.count,把副作用函数取出来执行,其结果就是,副作用函数在自己内部递归调用自己,栈就溢出了。
解决方法:
通过分析可以发现,读取和设置操作都是在同一个副作用函数中进行的,此时无论track收集的副作用函数还是trigger要触发执行的副作用函数,其实都是同一个,也就是当前的 activeEffect。因此,可以增加守卫条件,当 trigger 要触发执行的副作用函数就是当前正在执行的副作用函数(activeEffect)时,则不触发执行。
1 2 3 4 5 6 7 8 9 10 11 12 13
| function trigger (target, key) { const depsMap = bucket.get(target) if (!depsMap) { return }
const deps = depsMap.get(key) const depsToRun = new Set(deps) deps && deps.forEach((effectFn) => { (effectFn !== activeEffect) && depsToRun.add(effectFn) }) depsToRun && depsToRun.forEach(effectFn => effectFn()) }
|
6.调度执行
什么是可调度
可调度性是响应式系统非常重要的特性。所谓可调度,指的是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式
调度器
用户在调用 effectRegister 函数注册副作用函数时,可以传递第二个参数 options。options 中允许指定一个 scheduler 调度函数。
1 2 3 4 5 6 7 8
| effectRegister(() => { console.log(objProxy.count) }, { scheduler (fn) { } })
|
同时,在 effectRegister 函数中我们需要把 options 挂载到副作用函数上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function effectRegister (fn, options = {}) { const effectFn = () => { cleanup(effectFn) activeEffect = effectFn effectStack.push(activeEffect) fn() effectStack.pop() activeEffect = effectStack[effectStack.length -1] }
effectFn.options = options effectFn.deps = [] effectFn() }
|
有了调度函数,我们在 trigger 函数中触发副作用函数重新执行时,就可以直接调用用户传进来的调度函数,从而把控制权交给用户:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function trigger (target, key) { const depsMap = bucket.get(target) if (!depsMap) { return }
const deps = depsMap.get(key) const depsToRun = new Set(deps) deps && deps.forEach((effectFn) => { (effectFn !== activeEffect) && depsToRun.add(effectFn) }) depsToRun && depsToRun.forEach(effectFn => { if (effectFn.options.scheduler) { effectFn.options.scheduler(effectFn) } else { effectFn( ) } }) }
|
个人博客
耀耀切克闹 (yaoyaoqiekenao.com)
其他相关文章
《Vue.js的设计与实现》阅读笔记(一) - 掘金 (juejin.cn)