深入剖析Vue框架:实现精简的computed

一、前言

在上一篇中,我们已经实现了一个基础的响应式系统。如果不知道怎么构建一个基础的响应式系统,深入剖析 Vue 响应式系统:从零实现一个精简版 。现在,我们将在此基础上,手动实现 Vue 的 computed 函数。

源码地址:github.com/chuyuan132/...

二、了解 computed 的核心特性

要实现 computed,首先要理解它的两个核心特性:

  1. 懒执行 (Lazy Evaluation) :只有当你真正访问 computed 属性的值时,它才会执行计算。
  2. 缓存 (Caching) :如果依赖的响应式数据没有发生变化,computed 会返回上一次缓存的值,而不是重新计算。这大大提升了性能。

computed 的实现会借用我们之前实现的 effect 函数。如果不了解effect如何实现的,请浏览深入剖析 Vue 响应式系统:从零实现一个精简版。为了满足懒执行的特性,我们需要改造 effect 函数,增加一个 lazy 选项。

三、改造 effect 函数以支持懒执行

在我们之前的 effect 函数中,只要调用 effect(fn),副作用函数 fn 就会立即执行。为了实现懒执行,我们可以添加一个 lazy 选项。如果 lazytrueeffect 函数将不再立即执行 fn,而是直接返回 effectFn 本身。

javascript 复制代码
// ...(省略其他代码)

const defaultOptions = {
  scheduler: null,
  lazy: false,
};

function effect(fn, options = defaultOptions) {
  function effectFn() {
    // ...(省略原有逻辑)
    const res = fn(); // 副作用函数执行,返回其返回值
    // ...(省略原有逻辑)
    return res;
  }
  effectFn.deps = [];
  effectFn.options = options;

  if (!options.lazy) {
    // 如果不是懒执行,则立即执行
    effectFn();
  }
  // 无论是否立即执行,都返回 effectFn
  return effectFn;
}

四、实现 computed 函数

有了支持懒执行的 effect 函数,我们就可以着手实现 computed 了。computed 函数接受一个 getter 函数作为参数,并返回一个对象,该对象有一个 value 属性。

1. 基础结构

首先,我们利用 effectlazy 选项来创建 computed 的基本结构。

javascript 复制代码
function computed(getter) {
  const effectFn = effect(getter, { lazy: true });

  const obj = {
    get value() {
      // 访问 value 时,才执行 effectFn
      return effectFn();
    },
  };

  return obj;
}

这段代码已经实现了懒执行,但还缺少缓存功能。

2. 添加缓存机制

为了实现缓存,我们需要一个变量来追踪 computed 是否需要重新计算。我们称这个变量为 dirty

  • dirtytrue 时,表示值是"脏"的,需要重新计算。
  • dirtyfalse 时,表示值是干净的,可以返回缓存值。

同时,我们还需要一个 value 变量来保存缓存的结果。

javascript 复制代码
function computed(getter) {
  let dirty = true;
  let value = undefined;

  const effectFn = effect(getter, { lazy: true });

  const obj = {
    get value() {
      // 只有当 dirty 为 true 时才重新计算
      if (dirty) {
        // 执行 effectFn 并将结果缓存到 value
        value = effectFn();
        // 重新计算后,将 dirty 设为 false
        dirty = false;
      }
      return value;
    },
  };

  return obj;
}

这段代码实现了缓存,但 dirty 一旦变为 false 就再也没有机会变回 true 了。我们如何让它在依赖变化时重新变为 true 呢?

3. 依赖变化时重新计算

getter 函数内部的响应式依赖发生变化时,dirty 应该被重置为 true。这里,scheduler 调度器就派上用场了。

我们可以给 effect 函数传入一个 scheduler 选项。当 getter 的依赖变化时,scheduler 函数会被调用。我们可以在这个函数里将 dirty 设为 true

javascript 复制代码
function computed(getter) {
  let dirty = true;
  let value = undefined;

  const effectFn = effect(getter, {
    lazy: true,
    scheduler: () => {
      // 依赖变化时,将 dirty 设为 true
      dirty = true;
    },
  });

  const obj = {
    get value() {
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      return value;
    },
  };

  return obj;
}

4. 解决嵌套副作用函数的依赖问题

现在,如果我们在另一个 effect 函数中访问 computedvalue,会发生什么?

javascript 复制代码
const name = createProxy({ firstName: 'Jack', lastName: 'Chen' });

const fullName = computed(() => `${name.firstName} ${name.lastName}`);

effect(() => {
  // 当 fullName.value 变化时,这个副作用函数应该重新执行
  console.log(fullName.value);
});

// 依赖改变,fullName.value 应该更新
name.firstName = 'Mike';

name.firstName 改变时,computedscheduler 会被调用,将 dirty 设为 true。但 effect 函数并不知道 fullName.value 的值变了,所以它不会重新执行。

为了解决这个问题,我们需要让 computed 能够像普通响应式数据一样被追踪。也就是说,当 computed 的值发生变化时,它应该能够触发依赖它的副作用函数。

我们可以在 computedget value() 内部进行 依赖收集(track ,并在 scheduler触发依赖(trigger

javascript 复制代码
// ... (省略其他代码)

// 优化后的 computed 函数
function computed(getter) {
  let dirty = true;
  let value = undefined;

  // 使用 effect 的 scheduler,当依赖变化时,触发 computed 的更新
  const runner = effect(getter, {
    lazy: true,
    scheduler() {
      if (!dirty) {
        dirty = true;
        // 触发 computed 值的依赖
        trigger(proxy, 'value');
      }
    },
  });

  const obj = {
    get value() {
      if (dirty) {
        value = runner();
        dirty = false;
      }
      // 在访问 computed.value 时,进行依赖收集
      track(proxy, 'value');
      return value;
    },
  };
  
  return obj
}

经过以上优化,我们成功地实现了一个完整的 computed 函数,它同时具备了懒执行缓存响应式的特性。

五、演示

现在,你可以用新的 computed 函数来测试一下,看看它是否能正常工作。

javascript 复制代码
 const obj = {
    count: 0,
 };

const proxyObj = createProxy(obj);

const result = computed(() => proxyObj.count + 1);

effect(() => {
console.log("访问计算属性的effect函数", result.value);
});

setTimeout(() => {
proxyObj.count = 2;
}, 1000);
相关推荐
局i2 小时前
ES6 类与继承:现代 JavaScript 面向对象编程
前端·javascript·es6
白菜上路2 小时前
C# Web API Mapster基本使用
前端·c#
叫我詹躲躲2 小时前
偷偷收藏!前端老鸟绝不外传的150个JS插件,让你效率翻3倍…
前端·vue.js
会豪2 小时前
如何让自己的前端项目更优雅
前端
uhakadotcom2 小时前
致新人:如何编写自己的第一个VSCode插件,以使用@vscode/vsce来做打包工具为例
前端·面试·github
流***陌2 小时前
用工招聘小程序:功能版块与前端设计解析
前端·小程序
之恒君2 小时前
typescript(tsconfig.json - esModuleInterop)
前端·typescript
夏天19952 小时前
React:聊一聊状态管理
前端·javascript·react.js
李剑一2 小时前
低代码平台现在为什么不行了?之前为什么行?
前端·面试