回顾
import {reactive, effect} from '../../lib/mini-vue.esm.js'
const obj = reactive({msg: 'hello world'})
effect(() => {
console.log(obj.msg)
})
上一章分享了通过effect函数中传入一个函数(以下简称回调函数),当回调函数中包含有对响应式数据的属性访问的时候,便建立了回调函数与obj的msg属性间的关联,当obj.msg属性更新时,回调函数会再次执行。
原理:effect是一个注册函数,回调函数传入后会将其封装并赋值给全局变量activeEffect,然后执行回调函数。回调函数中包含了对响应式数据的放回,所以会触发getter然后将activeEffect这个全局变量放到“桶”中,当setter触发时从“桶”中取出并执行。
上图以设计模式的概念描绘effect函数的过程,绿色为全局变量(Decorator:装饰器对象、Proxy:代理对象、Wacther监听器对象)
# ReactiveEffect类的创建// 修改前
// 副作用函数
function effect(fn, options) {
function effectFn() {
activeEffect = effectFn;
activeEffectStack.push(activeEffect);
cleanup(activeEffect)
const res = fn();
activeEffectStack.pop();
activeEffect = activeEffectStack[activeEffectStack.length - 1];
return res;
}
if (!effectFn.deps) effectFn.deps = []
options && (effectFn.options = options)
if (options && !options.lazy) {
effectFn();
}
return effectFn
}
// 修改后
export function effect(fn, options = {}) {
// 本质就是fn的Decorator类型
const _effect = new ReactiveEffect(fn);
// 把用户传过来的值合并到 _effect 对象上去
// 缺点就是不是显式的,看代码的时候并不知道有什么值
extend(_effect, options);
_effect.run();
// 把 _effect.run 这个方法返回
// 让用户可以自行选择调用的时机(调用 fn)
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
cleanup优化
effect(() => {
if (obj.flag){
console.log(obj.msg)
}
})
在上述代码中,假如obj.flag为true,那么回调函数与obj的flag和msg属性都建立了关联,当obj.flag改为false时,回调执行。执行完成后该回调与obj的msg属性的关联关系应该丢弃,所以上一章在回调函数执行前增加了cleanup函数用来清空关联关系,回调函数执行重新建立关联关系。ps:类似于表单初始化时应该先清空表单内容。
function cleanup(effect) {
for(let i=0;i<effect.deps.length;i++) {
const dep = effect.deps[i]
dep.delete(effect)
}
}
上述清除函数有优化点,因为cleanup是一个O(n)遍历,且只有中特定的案例中才会需要,为此需要每一个案例中都执行一次cleanup不值得,所以需要优化。
首先我们既然不适用cleanup方法,那么替换方案就应该是我们应该区分出哪些ReactiveEffect实例是应该删除的而哪些实例是不应该删除的,为此我们需要在存储有ReactiveEffect实例的dep上打上标记位。再结合之前的特例发现,如果这个特例的ReactiveEffect实例在过去搜集过,但是在更新中没有搜集到它,那么它就是应该被删除的!!!所以我们需要两个标记位,一个为w,表示过去收集过,另一个为n,表示它在更新中。
// 副作用递归深度
let effectTrackDepth = 0;
// 优化标志位
export let trackOpBit = 1;
const maxMarkerBits = 30;
首先标记位考虑boolean值,但是细思发现effect函数是一个可嵌套的函数,当一次更新需要触发一个递归深度很深的ReactiveEffect时需要考虑递归的深度对性能的影响,所以用effectTrackDepth记录递归的深度,然后需要一个值设置递归的阈值,结合js中位运算为32位带符号的整数,并且最高位为符号位,所以阈值为30,位掩码修改trackOpBit可以提高运行效率,trackOpBit就是标记位变量。
export const newTracked = (dep) => {
return (dep.n & trackOpBit) > 0
}
export const wasTracked = (dep) => {
return (dep.w & trackOpBit) > 0
}
用位掩码判断执行效率更高。
// 遇到这种极端案例时就直接cleanup
effect(() => {
console.log(obj.msg)
// ....其中省略嵌套30层
effect(() => {
console.log(obj.msg)
})
})
export class ReactiveEffect {
deps = [];
constructor(public fn, scheduler?) {}
run() {
try {
// 通过activeEffect传递响应式副作用
activeEffect = this;
trackOpBit = 1 << ++effectTrackDepth;
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
} else {
cleanupEffect(this);
}
activeEffectStack.push(this);
return this.fn();
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this);
}
trackOpBit = 1 << --effectTrackDepth;
// 回溯响应式副作用
activeEffectStack.pop();
activeEffect = activeEffectStack[activeEffectStack.length - 1];
}
}
}
export const initDepMarkers = ({deps}) => {
if (!deps.length) return;
for(let i =0;i<deps.length;i++) {
// 标记为已存在,挂载时依赖集为空
deps[i].w |= trackOpBit
}
}
function trackEffect(dep) {
//...
if (effectTrackDepth <= maxMarkerBits) {
// 没有打上新标记位的打上本轮标记
if (!newTracked(dep)) {
dep.n |= trackOpBit;
shouldTrack = !wasTracked(dep);
}
} else {
shouldTrack = dep.has(activeEffect);
}
//...
}
如果flag更新为false后再执行回调函数,是不会track obj的msg属性,trackEffect不会触发则没有新标记位。
export const finalizeDepMarkers = (effect) => {
const {deps} = effect;
if (deps.length) {
let ptr = 0;
for(let i=0;i<deps.length;i++) {
const dep = deps[i];
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
deps[ptr++] = dep
}
// 重置
dep.w&=~trackOpBit;
dep.n&=~trackOpBit;
}
// 裁剪
deps.length=ptr
}
}
在回调函数执行后有过去标记位但没有新标记位的依赖,会删除其中的ReactiveEffect。最后需要裁剪deps。(类似于算法,在[1,2,3,0,3,0,3,4]中过滤掉0,并返回原数组)。
总结
1.封装ReactiveEffect对象。 2.优化cleanup函数。