完美的从0撸出来一个 Vue3 computed 实现逻辑

377 阅读7分钟

一、开篇

想要更加深入了解 Vue3的响应原理,建议看一下 保姆式地带你从0到1实现Vue3 Effect,对你阅读这里的代码更容易理解。

二、响应数据的完成代码

// 存储副作用的桶
const bucket = new WeakMap();
// 原始数据
const data = { text: "Hello World" };

// 用一个全局变量存储被注册的副作用函数
let activeEffect;

// effect 栈
const effectStack = [];

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);
    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用从桶中取出并执行
    trigger(target, key);
    return true;
  },
});

// 在get 拦截函数内调用 track 函数 追踪变化
function track(target, key) {
  // 没有 activeEffect 直接 return
  if (!activeEffect) return;
  // 根据 target 从 桶中取出 depsMap,它也是一个map 类型,key-->effects
  let depsMap = bucket.get(target);
  // 如果 depsMap 不存在,创建一个新的map,并与 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }

  // 再根据 key 从depsMap 中取得 deps,它是一个 Set 集合
  // 里面存储着所有与当前 key 相关的副作用函数:effects
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 最后将当前激活的副作用函数添加到桶里
  deps.add(activeEffect);
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 effectFn.deps 数组中
  activeEffect.deps.push(deps);
}

// 在 set 拦截函数内调用 trigger 函数 触发变化
function trigger(target, key) {
  // 从桶中取出 depsMap,它也是一个map 类型,key-->effects
  const depsMap = bucket.get(target);
  if (!depsMap) return true;
  // 再根据 key 从depsMap 中取得 副作用函数 effects
  const effects = depsMap.get(key);

  const effects2Run = new Set();
  effects &&
    effects.forEach((effectFn) => {
      // 如果trigger触发执行的副作用函数与当前的副作用函数相同,则不触发执行
      if (effectFn !== activeEffect) {
        effects2Run.add(effectFn);
      }
    });

  effects2Run.forEach((effectFn) => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn);
    } else {
      // 否则直接执行副作用函数
      effectFn();
    }
  });
}

// 用于注册副作用函数
function effect(fn, options = {}) {
  const effectFn = () => {
    // 删除与 effectFn 相关的关系
    cleanup(effectFn);
    // 调用effect函数的时候,将 effectFn 赋值给 activeEffect
    activeEffect = effectFn;
    // 在调用副作用函数之前,将当前副作用函数添加到 effectStack 栈中
    effectStack.push(effectFn);
    // 执行副作用函数
    fn();
    // 在调用副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  };
  // 将 options 挂在到 effectFn 上
  effectFn.options = options;
  // effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  // 只有非lazy的时候才会执行
  if (!options.lazy) {
    effectFn();
  }
  // 将副作用函数作为返回值返回
  return effectFn;
}

function cleanup(effectFn) {
  // 遍历 effectFn.deps 数组
  effectFn.deps.forEach((deps) => {
    // 将 effectFn 从依赖集合中删除
    deps.delete(effectFn);
  });
  // 清空 effectFn.deps
  effectFn.deps.length = 0;
}

const effectFn = effect(
  () => {
    document.body.innerText = obj.text;
  },
  {
    lazy: true,
  }
);

三、计算属性computed

1. 副作用函数作为getter,得到计算的值

const effectFn = effect(
  () => {
    return obj.bar + obj.foo
  },
  {
    lazy: true,
  }
);
// 手动执行
const value = effectFn()

需要修改effect 注册副作用函数 image.png

2. 去除手动执行,待使用的时候,获取计算值

const data = { foo: "foo", bar: "bar" };
const obj = new Proxy(data, {...})

function computed(getter) {
  // 把getter 作为副作用函数,创建一个lazy 的effect
  const effectFn = effect(getter, { lazy: true });
  const obj = {
    // 当读取 value 时,才执行effectFn
    get value() {
      return effectFn();
    },
  };
  // 将 obj 返回
  return obj;
}

document.body.innerText = computed(() => {
  return obj.bar + obj.foo;
}).value;

效果图 image.png

3. 缓存

每次获取value的时候,都会去 执行effectFn(),如果依赖数据没有发生变化,应该获取缓存的数据,而非再次执行effectFn()获取

function computed(getter) {
  // 缓存数据
  let value;
  // dirty 为true,标识需要重新获取计算获取值
  let dirty = true;
  // 把getter 作为副作用函数,创建一个lazy 的effect
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      // 依赖的响应数据发生变化的时候,将 dirty 设置为 true,再次获取value的时候,需要重新计算
      dirty = true;
    },
  });
  const obj = {
    // 当读取 value 时,才执行effectFn
    get value() {
      if (dirty) {
        value = effectFn();
        // 下次获取的时候,直接从缓存中获取数据
        dirty = false;
      }
      return value;
    },
  };
  // 将 obj 返回
  return obj;
}

4、computed 计算的值转成响应式

const sum = computed(()=>{
    return obj.bar + obj.text;
})
effect(function effectFn(){
    document.body.innerText = sum.value
})

更改obj.bar / obj.text值,并不会触发 effectFn 副作用函数重新执行,需要在computed内实现 track 和 trigger

function computed(getter) {
  // 缓存数据
  let value;
  // dirty 为true,标识需要重新获取计算获取值
  let dirty = true;
  // 把getter 作为副作用函数,创建一个lazy 的effect
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      // 依赖的响应数据发生变化的时候,将 dirty 设置为 true,再次获取value的时候,需要重新计算
      dirty = true;
      // 触发对应的副作用函数
      trigger(obj, "value");
    },
  });
  const obj = {
    // 当读取 value 时,才执行effectFn
    get value() {
      if (dirty) {
        value = effectFn();
        // 下次获取的时候,直接从缓存中获取数据
        dirty = false;
      }
      // 拦截读取操作,将当前的副作用函数收集到 bucket 中
      track(obj, "value");
      return value;
    },
  };
  // 将 obj 返回
  return obj;
}

effect(function effectFn() {
  document.body.innerText = computed(() => {
    return obj.bar + obj.foo;
  }).value;
});

效果 Kapture 2022-05-15 at 15.38.44.gif

四、完成代码

// 存储副作用的桶
const bucket = new WeakMap();
// 原始数据
const data = { foo: "foo", bar: "bar" };

// 用一个全局变量存储被注册的副作用函数
let activeEffect;

// effect 栈
const effectStack = [];

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);
    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用从桶中取出并执行
    trigger(target, key);
    return true;
  },
});

// 在get 拦截函数内调用 track 函数 追踪变化
function track(target, key) {
  // 没有 activeEffect 直接 return
  if (!activeEffect) return;
  // 根据 target 从 桶中取出 depsMap,它也是一个map 类型,key-->effects
  let depsMap = bucket.get(target);
  // 如果 depsMap 不存在,创建一个新的map,并与 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }

  // 再根据 key 从depsMap 中取得 deps,它是一个 Set 集合
  // 里面存储着所有与当前 key 相关的副作用函数:effects
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 最后将当前激活的副作用函数添加到桶里
  deps.add(activeEffect);
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 effectFn.deps 数组中
  activeEffect.deps.push(deps);
}

// 在 set 拦截函数内调用 trigger 函数 触发变化
function trigger(target, key) {
  // 从桶中取出 depsMap,它也是一个map 类型,key-->effects
  const depsMap = bucket.get(target);
  if (!depsMap) return true;
  // 再根据 key 从depsMap 中取得 副作用函数 effects
  const effects = depsMap.get(key);

  const effects2Run = new Set();
  effects &&
    effects.forEach((effectFn) => {
      // 如果trigger触发执行的副作用函数与当前的副作用函数相同,则不触发执行
      if (effectFn !== activeEffect) {
        effects2Run.add(effectFn);
      }
    });

  effects2Run.forEach((effectFn) => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn);
    } else {
      // 否则直接执行副作用函数
      effectFn();
    }
  });
}

// 用于注册副作用函数
function effect(fn, options = {}) {
  const effectFn = () => {
    // 删除与 effectFn 相关的关系
    cleanup(effectFn);
    // 调用effect函数的时候,将 effectFn 赋值给 activeEffect
    activeEffect = effectFn;
    // 在调用副作用函数之前,将当前副作用函数添加到 effectStack 栈中
    effectStack.push(effectFn);
    // 执行副作用函数,得到返回值
    const res = fn();
    // 在调用副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    // 将返回值 return 出去
    return res;
  };
  // 将 options 挂在到 effectFn 上
  effectFn.options = options;
  // effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  // 只有非lazy的时候才会执行
  if (!options.lazy) {
    effectFn();
  }
  // 将副作用函数作为返回值返回
  return effectFn;
}

function cleanup(effectFn) {
  // 遍历 effectFn.deps 数组
  effectFn.deps.forEach((deps) => {
    // 将 effectFn 从依赖集合中删除
    deps.delete(effectFn);
  });
  // 清空 effectFn.deps
  effectFn.deps.length = 0;
}

function computed(getter) {
  // 缓存数据
  let value;
  // dirty 为true,标识需要重新获取计算获取值
  let dirty = true;
  // 把getter 作为副作用函数,创建一个lazy 的effect
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      // 依赖的响应数据发生变化的时候,将 dirty 设置为 true,再次获取value的时候,需要重新计算
      dirty = true;
      // 触发对应的副作用函数
      trigger(obj, "value");
    },
  });
  const obj = {
    // 当读取 value 时,才执行effectFn
    get value() {
      if (dirty) {
        value = effectFn();
        // 下次获取的时候,直接从缓存中获取数据
        dirty = false;
      }
      // 拦截读取操作,将当前的副作用函数收集到 bucket 中
      track(obj, "value");
      return value;
    },
  };
  // 将 obj 返回
  return obj;
}

effect(function effectFn() {
  document.body.innerText = computed(() => {
    return obj.bar + obj.foo;
  }).value;
});

五、总结

  1. effect 副作用注册函数,不立即执行副作用函数
  2. 将副作用函数计算的值return 出来
  3. computed内置实现一个 getter track 和 setter trigger。

六、参考

《Vue.js 设计与实现》