响应式的基本概念
响应式是指当数据发生变化时,系统会自动更新与数据相关的 DOM 结构。
在 Vue2 中,响应式系统的实现基于 Object.defineProperty。然而,Object.defineProperty 有一些局限,如:无法监听数组的变化、需要遍历对象的每个属性进行监听、性能开销较大。
在 Vue3 中,响应式系统的实现基于 ES6 的 Proxy 对象。Proxy 可以直接监听对象和数组的变化,而无需对每个属性进行监听,从而大大提高性能。同时,Proxy 也可以解决 Object.defineProperty 无法监听数组的问题。
响应式的关键在于vue的依赖收集机制。
简化模型
为了更直观的理解vue依赖收集的模型,我们先来看一个“简单”的功能描述:
已知watcher函数,调用了一些“外部函数”:
function watcher () { console.log('watcher start') 函数1(); 函数2(); console.log('watcher end') }
能否设计一个依赖收集系统,使这些“外部函数”运行时,watcher也会随之运行?
关键:如何判断函数间的调用关系?
看似有点难,实际一点也不简单,我们需要知道函数间调用关系。我们先看个例子:
function A() { console.log('A') } function B() { console.log('B') } function C() { console.log('C') } ... function watcher () { console.log('watcher start!') /* *这里调用了上面的某些函数* */ console.log('watcher end!') } /* *这里运行了某些函数* */ watcher(); - watcher start! - A - B - wathcer end! - C
从运行结果我们可以看出watcher内部一定调用了A、B函数:
为啥?js是单线程的。
C函数一定在watcher外面吗?不一定。例如:
function watcher () { console.log('start') A() B() setTimeout(()=>{ C() }) console.log('end') } watcher();
C函数这种咋办?不管!我们只管肯定没问题的!
我们由此可以确定
函数watcher执行期间,凡是运行过的函数,一定是watcher内部调用过的函数
根据这个原理,我们设计依赖收集系统如下:
// 当前的监听函数 let activeEffect = null // 副作用函数 function effect (watcher) { activeEffect = watcher // watcher执行的期间就是依赖收集的阶段 watcher(true) activeEffect= null } // isTracking:是否是依赖收集阶段 function A (isTracking = false) { if (isTracking) { // 依赖收集阶段,effects就是A的监听函数集合 A.effects = A.effects || new Set() A.effects.add(activeEffect) } else { // 依赖运行阶段 console.log('A触发了') A.effects.forEach(fn => fn(true)) } } function B (isTracking = false) { /*** 与A类似 ***/ }
测试一下效果
看起来达到了要求。
将上面代码优化一下,最终如下:
let activeEffect = null; function effect (watcher) { activeEffect = watcher; watcher(true); activeEffect = null; } const bucket = new WeakMap(); function track (target) { const effects = bucket.get(target) || new Set(); activeEffect && effects.add(activeEffect); bucket.set(target, effects); } function trigger (target) { bucket.get(target)?.forEach?.(fn => fn(true)); } function A (isTracking = false) { if (isTracking) { track(A); } else { console.log('A触发了') trigger(A); } } function B (isTracking = false) { }
这里将之前 A.effects = A.effects || new Set();依赖收集流程提取成track函数,监听函数的触发流程抽离为trigger函数;这样,我们实现了一个简单的依赖收集系统。
Vue依赖收集模型
我们知道Vue3是通过Proxy实现的依赖收集流程,Proxy示例:
1. Proxy对象get监听,set触发
Vue3中,Proxy代理数据在被读取时“依赖收集”,在被赋值时会“触发依赖”;我们试一下上面完成的依赖收集系统,看下效果:
const data = { value: 1, } const proxyData = new Proxy(data, { get(target, key) { track(target); return target[key]; }, set(target, key, value) { trigger(target); target[key] = value; } })
测试一下
测试代码如下:
终端运行结果:
看起来效果不错!但是下面的例子里有问题:
一个无关的属性key的赋值也会触发监听函数!这不是我们想要的。为了精确监听,还需要细化依赖收集系统。
2. “key”级依赖
我们可以将对象的属性作为基本单位进行依赖收集。改造如下:
// 依赖收集函数,这里精确到keyfunction track (target, key) { const effects = bucket.get(target) || new Map(); const keyMap = effects.get(key) || new Set(); effects.set(key, keyMap); bucket.set(target, effects); activeEffect && keyMap.add(activeEffect);}// 依赖触发函数,这里精确到keyfunction trigger (target, key) { const effects = bucket.get(target); if (!effects) return; const keyMap = effects.get(key); if (!keyMap) return; keyMap.forEach(effect => effect());} const data = { value: 1}const proxyData = new Proxy(data, { get(target, key) { // 具体到key进行收集 track(target, key); return target[key] }, set(target, key, value) { // 触发到key trigger(target, key); target[key] = value }})
这里试一下效果
这样就实现了精确到属性的监听系统。看到这里,似乎完成的很不错了,但是看到下面的例子:
这里value属性由false变为true后,属性data的就已不再参与监听函数内的逻辑了;监听函数不应该再响应data属性,但实际上并没有。因为依赖关系已经固化,data属性只要变化就一定会触发监听,不管是否真的需要:
3. 分支切换
为了优化这一点,应将依赖关系实时更新,将多余的监听去除。为此,vue采取的策略是:
每次监听函数运行前,都要将自己的依赖关系清除;然后在运行期间重建依赖关系。(版权归掘金硬毛巾原作者所有,侵删)
审核编辑:黄飞
全部0条评论
快来发表一下你的评论吧 !