深入剖析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);
相关推荐
子兮曰11 小时前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰11 小时前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万11 小时前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝11 小时前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋12 小时前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁12 小时前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王952714 小时前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大14 小时前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师14 小时前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学15 小时前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端