Vue3响应式系统原理:scheduler调度

185 阅读3分钟

问题

响应式对象属性的变化,会触发副作用函数的同步执行。

const data = {
  value: 1,
};

const obj = new Proxy(data, ...)

myEffect(() => {
  console.log(obj.value)
})

obj.value++

console.log('end')

上面的代码,输出的顺序如下:

// 1
// 2
// end

完整的代码如下。 其中effect.js代码如下:

// effect.js

// 存放副作用函数的集合容器,用Set数据结构,是为了防止相同的副作用函数重复收集
const bucket = new WeakMap();

// 表示当前正在运行的副作用函数
let activeEffect = null;

// 副作用栈
let effectStack = [];

// 用于执行副作用函数的函数
export function myEffect(fn) {
  const effectFn = () => {
    // 清除依赖
    cleanup(effectFn);
    // 执行副作用函数
    activeEffect = effectFn;

    effectStack.push(activeEffect)
    fn();

    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  };
  // 存储该副作用哦函数相关联的依赖
  effectFn.deps = []
  effectFn();
}

// 响应式对象。响应式对象为原始对象的Proxy代理
export const myReactive = (data) =>
  new Proxy(data, {
    get(target, key) {
      if (!activeEffect) return target[key];
      track(target, key);
      return target[key];
    },
    set(target, key, val) {
      target[key] = val;
      trigger(target, key);
      return true;
    },
  });

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    depsMap = new Map();
    bucket.set(target, depsMap);
  }
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  deps.add(activeEffect);

  // 将deps增加到activeEffect的deps中
  activeEffect.deps.push(deps);
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if(!depsMap) return
  const effects = depsMap.get(key)

  // const effectsToRun = new Set(effects)
  const effectToRun = new Set()
  effects && effects.forEach(effectFn => {
    // 当前的副作用函数和activeEffect不一样,才会添加到执行集合中
    if(effectFn !== activeEffect) {
      effectToRun.add(effectFn)
    }
  })
  effectToRun && effectToRun.forEach(fn => fn())
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]; // deps为对象属性关联的deps
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}
// debug.js

import { myEffect, myReactive } from './effect.js'
// 原始对象
const data = {
  value: 1,
};

// 响应式对象
const obj = myReactive(data);

myEffect(() => {
  console.log(obj.value)
})

obj.value++

console.log('end')

// 输出顺序如下:
// 1
// 2
// end

有没有一种机制,可以让副作用函数到执行按照我们自定义的方式运行呢?比如说当响应式对象属性变化后,延迟3s后才会触发运行副作用函数呢?答案是有的,为此我们需要将自定义的调度逻辑作为参数,传递给myEffect方法。

分析

我们可以将调度相关的逻辑,作为myEffect的第二个参数传递给它。

// 调度参数
const options = {
 scheduler(fn) {
     setTimeout(() => {
         fn()
     }, 3000)
 }
}
myEffect(fn, options) {
    ...
}

里面有一个scheduler方法,相关的调度逻辑都在这个方法中编写。

我们完善下myEffect的代码,将myEffect传递的第二个参数options挂载到副作用函数上。

myEffect(fn, options) {
    const effectFn = () => {
        ...
        fn()
        ...
    }
    // 将options挂载到effectFn上
    effectFn.options = options
}

修改trigger函数,在运行副作用函数的时候,判断副作用函数上是否有scheduler参数,如果有的话,按照scheduler的逻辑来运行副作用函数。

function trigger(target, key) {
  ...
  effectToRun && effectToRun.forEach(fn => {
    if(fn.options.scheduler) { // 判断是否有scheduler定义
      fn.options.scheduler(fn)
    }else {
      fn()
    }
  })
}

完整的代码见这里

总结

正常情况下,响应式对象属性的变化,会同步触发副作用函数的运行。通过设置effect函数中增加第二个参数options,将调度逻辑作为一个属性绑定到副作用函数身上,然后在运行当前的副作用函数的时候,判断是否有这个属性,如果有的话,通过这个属性对应的调度逻辑运行副作用函数,从而实现了控制副作用函数的自定义运行的目的。

代码

github.com/wdskuki/js-…

参考

  1. 《Vue设计与实现》,作者:霍春阳,ISBN: 9787115583864