深入剖析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 选项。如果 lazy 为 true,effect 函数将不再立即执行 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. 基础结构

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

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

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

  return obj;
}

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

2. 添加缓存机制

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

  • 当 dirty 为 true 时,表示值是"脏"的,需要重新计算。
  • 当 dirty 为 false 时,表示值是干净的,可以返回缓存值。

同时,我们还需要一个 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 函数中访问 computed 的 value,会发生什么?

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 改变时,computed 的 scheduler 会被调用,将 dirty 设为 true。但 effect 函数并不知道 fullName.value 的值变了,所以它不会重新执行。

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

我们可以在 computed 的 get 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);
相关推荐
程序员大阳5 分钟前
副队长CSS教程(33)–圆角与透明
前端·css·透明·圆角
志尊宝16 分钟前
Vue3 零基础每日笔记(082):文件上传实战——el-upload 校验、自定义上传、头像预览
前端·javascript·vue.js·笔记·#javascript·#vue.js
鬼手点金41 分钟前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
柳杉42 分钟前
借助 Codex 和可视化工作流,从调研到完整实现,3D 大屏竟然可以这么简单
前端·ai编程·数据可视化
IMPYLH1 小时前
HTML 的 <wbr> 元素
前端·html
Dovis(誓平步青云)2 小时前
常用命令藏得太深?怎么做一个快捷命令坞
运维·前端·自动化·音视频·prometheus
IT_陈寒2 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
网易云信2 小时前
大模型能上岗做销售吗?我们让 12 款国产模型交了 420 份作业
前端·后端
蜗牛互联网3 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python
计算机魔术师3 小时前
Perplexity:每个 Project 都是一个 Git 仓库
前端