实现vue3响应式系统核心-computed

2024-01-30 19:12

本文主要是介绍实现vue3响应式系统核心-computed,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

简介

在之前的文章中介绍了 effect 函数,它用来注册副作用函数,同时它也允许指定一些选项参数 options,例如指定 scheduler调度器来控制副作用函数的执行时机和方式;也介绍了用来追踪和收集依赖的 track函数,以及用来触发副作用函数重新执行的 trigger函数。综合这些内容,我们就可以实现 Vue.js 中一个非常重要并且非常有特色的能力——计算属性。

代码地址: https://github.com/SuYxh/share-vue3

代码并没有按照源码的方式去进行组织,目的是学习、实现 vue3 响应式系统的核心,用最少的代码去实现最核心的能力,减少我们的学习负担,并且所有的流程都会有配套的图片,图文 + 代码,让我们学习更加轻松、快乐。

每一个功能都会提交一个 commit ,大家可以切换查看,也顺变练习练习 git 的使用。

computed 实现

在深入讲解计算属性之前,我们需要先来聊聊关于懒执行的 effect,即 lazyeffect。这是什么意思呢?

举个例子,现在我们所实现的 effect函数会立即执行传递给它的副作用函数。但在有些场景下,我们并不希望它立即执行,而是希望它在需要的时候才执行,例如计算属性。

这时我们可以通过在 options 中添加lazy 属性来达到目的,如下面的代码所示:

effect(// 指定了 lazy 选项,这个函数不会立即执行() => {console.log(obj.foo);},// options{lazy: true}
);

老套路,还通过配置项传入:

function effect(fn, options = {}) {const effectFn = () => {cleanup(effectFn);activeEffect = effectFn;effectStack.push(effectFn);fn();effectStack.pop();activeEffect = effectStack[effectStack.length - 1];}effectFn.options = options;effectFn.deps = [];// 只有非 lazy 的时候,才执行if (!options.lazy) { // 新增// 执行副作用函数effectFn();}// 将副作用函数作为返回值返回return effectFn; // 新增
}

如果我们把传递给 effect 的函数看作一个 getter,那么这个 getter 函数可以返回任何值,这样我们在手动执行副作用函数时,就能够拿到其返回值:

const effectFn = effect(// getter 返回 obj.foo 与 obj.bar 的和() => obj.foo + obj.bar,{ lazy: true }
);// value 是 getter 的返回值
const value = effectFn();

为了实现这个目标,我们需要再对 effect 函数做一些修改,如以下代码所示:

function effect(fn, options = {}) {const effectFn = () => {// ...const res = fn();// ...return res}// ...return effectFn; 
}

通过代码可以看到,传递给 effect 函数的参数 fn 才是真正的副作用函数,而 effectFn是我们包装后的副作用函数。为了通过effectFn 得到真正的副作用函数fn的执行结果,我们需要将其保存到res变量中,然后将其作为effectFn函数的返回值。

基础实现

现在我们已经能够实现懒执行的副作用函数,并且能够拿到副作用函数的执行结果了,接下来就可以实现计算属性了,如下所示:

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

我们定义一个 computed 函数,它接收一个 getter 函数作为参数,把 getter 函数作为副作用函数,用它创建一个lazyeffectcomputed 函数的执行会返回一个对象,该对象的 value 属性是一个访问器属性,只有当读取 value 的值时,才会执行 effectFn 并将其结果作为返回值返回。

编写单测

新建一个 computed.spec.js

it("base computed", () => {// 创建响应式对象const obj = reactive({ price: 100, num: 10 });const allPrice = computed(() => obj.price * obj.num)expect(allPrice.value).toBe(1000);
});
运行单测

image-20240118170122880

没毛病,咱们继续!

增加缓存

上面的代码多次访问 allPrice.value 的值,每次访问都会调用 effectFn 重新计算。所以我们需要进行修改:

function computed(getter) {// value 用来缓存上一次计算的值let value;// dirty 标志,用来标识是否需要重新计算值,为 true 则意味着“脏”,需要计算let dirty = true;const effectFn = effect(getter, {lazy: true});const obj = {get value() {// 只有“脏”时才计算值,并将得到的值缓存到 value 中if (dirty) {value = effectFn();// 将 dirty 设置为 false,下一次访问直接使用缓存到 value 中的值dirty = false;}return value;}};return obj;
}

新增了两个变量 valuedirty,其中 value 用来缓存上一次计算的值,而dirty 是一个标识,代表是否需要重新计算。当我们通过 allPrice.value 访问值时,只有当 dirty true 时才会调用 effectFn重新计算值,否则直接使用上一次缓存在 value 中的值。这样无论我们访问多少次 allPrice.value,都只会在第一次访问时进行真正的计算,后续访问都会直接读取缓存的 value 值。

那么问题又来了,如果此时我们修改 obj.numobj.price的值,再访问 allPrice.value,会发现访问到的值没有发生变化。

问题就是 dirty 的状态我们只进行了关闭,并没有进行打开,那么什么时候打开呢?

function computed(getter) {let value;let dirty = true;const effectFn = effect(getter, {lazy: true,// 添加调度器,在调度器中将 dirty 重置为 truescheduler() {dirty = true;}});const obj = {get value() {//...return value;}};return obj;
}

我们为 effect 添加了 scheduler调度器函数,它会在 getter 函数中所依赖的响应式数据变化时执行,这样我们在 scheduler函数内将 dirty重置为 true,当下一次访问allPrice.value时,就会重新调用 effectFn 计算值,这样就能够得到预期的结果了。

增加依赖收集

现在的计算属性已经趋于完美了,但还有一个缺陷,当我们在另外一个 effect 中读取计算属性的值时就会被暴露出来,看看下面的 case。

编写单测
it("computed track and trigger", () => {const mockFn = vi.fn();// 创建响应式对象const obj = reactive({ price: 100, num: 10 });// 创建计算属性const allPrice = computed(() => obj.price * obj.num);effect(() => {mockFn()console.log(allPrice.value);})expect(mockFn).toHaveBeenCalledTimes(1);obj.num = 20;expect(allPrice.value).toBe(2000);expect(mockFn).toHaveBeenCalledTimes(2);
});

如以上代码所示,allPrice是一个计算属性,并且在另一个 effect 的副作用函数中读取了 allPrice.value 的值。如果此时修改 obj.num的值,我们期望副作用函数重新执行,就像我们在 Vue.js 的模板中读取计算属性值的时候,一旦计算属性发生变化就会触发重新渲染一样。

运行单测

image-20240118172427066

可以看到,修改obj.num 的值, mockFn函数并没有被调用,也就是说修改值并不会触发副作用函数的渲染,因此我们说这是一个缺陷。

问题分析

从本质上看这就是一个典型的 effect 嵌套。一个计算属性内部拥有自己的 effect,并且它是懒执行的,只有当真正读取计算属性的值时才会执行。对于计算属性的 getter函数来说,它里面访问的响应式数据只会把 computed内部的 effect收集为依赖。而当把计算属性用于另外一个 effect时,就会发生 effect嵌套,外层的 effect不会被内层 effect中的响应式数据收集。

解决

当读取计算属性的值时,我们可以手动调用 track 函数进行追踪;当计算属性依赖的响应式数据发生变化时,我们可以手动调用 trigger函数触发响应:

function computed(getter) {let value;let dirty = true;const effectFn = effect(getter, {lazy: true,// 添加调度器,在调度器中将 dirty 重置为 truescheduler() {dirty = true;trigger(obj, 'value')}});const obj = {get value() {if (dirty) {console.log('执行 effectFn');value = effectFn();dirty = false;}track(obj, 'value')return value;}};return obj;
}

修改 track 方法,if (!activeEffect) return target[key] 改成 if (!activeEffect) return, 否则会出现死循环

再次运行 case,发现就没有问题啦。大家可以自行调试看看此时收到的依赖都是什么。

image-20240118173918934

抽离代码

新建一个 computed文件,写入:

import { effect, track, trigger } from "./main";
export function computed(getter) {let value;let dirty = true;const effectFn = effect(getter, {lazy: true,// 添加调度器,在调度器中将 dirty 重置为 truescheduler() {dirty = true;trigger(obj, "value");},});const obj = {get value() {if (dirty) {value = effectFn();dirty = false;}track(obj, "value");return value;},};return obj;
}

运行测试

pnpm test

image-20240118174340233

看,我们忘了修改tracktrigger 以及测试代码中的导入,直接跑不通了。 修改后再次运行:

image-20240118174635635

测试就通过了,有单测,我们可以放心重构!

相关代码在 commit: (447ace7)实现 computed ,git checkout 447ace7 即可查看。

流程图

computed 整体流程图:

image-20240118181420641

引导扫码关注

一个前端小学生的学习之路,如果你喜欢前端,我们可以一起进行学习、交流、共建。可以添加好友,结伴学习,成长的路上不孤单!

这篇关于实现vue3响应式系统核心-computed的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/661259

相关文章

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

OpenCV实现实时颜色检测的示例

《OpenCV实现实时颜色检测的示例》本文主要介绍了OpenCV实现实时颜色检测的示例,通过HSV色彩空间转换和色调范围判断实现红黄绿蓝颜色检测,包含视频捕捉、区域标记、颜色分析等功能,具有一定的参考... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间

全面解析HTML5中Checkbox标签

《全面解析HTML5中Checkbox标签》Checkbox是HTML5中非常重要的表单元素之一,通过合理使用其属性和样式自定义方法,可以为用户提供丰富多样的交互体验,这篇文章给大家介绍HTML5中C... 在html5中,Checkbox(复选框)是一种常用的表单元素,允许用户在一组选项中选择多个项目。本

HTML5 搜索框Search Box详解

《HTML5搜索框SearchBox详解》HTML5的搜索框是一个强大的工具,能够有效提升用户体验,通过结合自动补全功能和适当的样式,可以创建出既美观又实用的搜索界面,这篇文章给大家介绍HTML5... html5 搜索框(Search Box)详解搜索框是一个用于输入查询内容的控件,通常用于网站或应用程

Python实现精准提取 PDF中的文本,表格与图片

《Python实现精准提取PDF中的文本,表格与图片》在实际的系统开发中,处理PDF文件不仅限于读取整页文本,还有提取文档中的表格数据,图片或特定区域的内容,下面我们来看看如何使用Python实... 目录安装 python 库提取 PDF 文本内容:获取整页文本与指定区域内容获取页面上的所有文本内容获取

基于Python实现一个Windows Tree命令工具

《基于Python实现一个WindowsTree命令工具》今天想要在Windows平台的CMD命令终端窗口中使用像Linux下的tree命令,打印一下目录结构层级树,然而还真有tree命令,但是发现... 目录引言实现代码使用说明可用选项示例用法功能特点添加到环境变量方法一:创建批处理文件并添加到PATH1

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

canal实现mysql数据同步的详细过程

《canal实现mysql数据同步的详细过程》:本文主要介绍canal实现mysql数据同步的详细过程,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的... 目录1、canal下载2、mysql同步用户创建和授权3、canal admin安装和启动4、canal

CSS3中的字体及相关属性详解

《CSS3中的字体及相关属性详解》:本文主要介绍了CSS3中的字体及相关属性,详细内容请阅读本文,希望能对你有所帮助... 字体网页字体的三个来源:用户机器上安装的字体,放心使用。保存在第三方网站上的字体,例如Typekit和Google,可以link标签链接到你的页面上。保存在你自己Web服务器上的字