likes
comments
collection
share

第二章-effectjs的优化

作者站长头像
站长
· 阅读数 15

第二章-effectjs的优化

回顾

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触发时从“桶”中取出并执行。

第二章-effectjs的优化

上图以设计模式的概念描绘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函数。

转载自:https://juejin.cn/post/7248199693934510136
评论
请登录