深入源码设计!Vue3.js核心API——Computed实现原理

如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~

作者:前端小王hs

阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主

此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来

书籍:《Vue.js设计与实现》 作者:霍春阳

本篇博文将在书第4.8节的基础上进一步解析,附加了测试的代码运行示例,以及对书籍中提到的ES6中的数据结构及其特点进行阐述,方便正在学习Vue3想分析Vue3源码的朋友快速阅读

如有帮助,不胜荣幸

前置章节:

  1. 深入理解Vue3.js响应式系统基础逻辑
  2. 深入理解Vue3.js响应式系统设计之栈结构和循环问题
  3. 深入理解Vue3.js响应式系统设计之调度执行

核心API

懒执行lazy的effect

经过前置的章节的学习,可以发现设计的effect都是立即执行的,而在4.8节的开始,作者向我们设计了一个可以按意愿时间节点执行的effect,其设计逻辑非常简单,就是在传递effectoptions形参时添加内置lazy属性,然后再在effect函数内部进行判断,如果存在lazy,则返回effectFn给调用者。那么何时何地执行effectFn的决定权就到了调用者的手上

传递lazy代码如下:

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

完善判断lazyeffect代码如下:

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

那么现在,就可以使用一个变量去接收返回的effectFn,代码如下:

const effectFn = effect(() => {  console.log(obj.foo)  
}, { lazy: true })  // 手动执行副作用函数  
effectFn()

对于手动执行,我们可以使用res去接收调用函数的返回值,那么就需要在effectreturn res,代码又得继续完善:

function effect(fn, options = {}) {const effectFn = () => {cleanup(effectFn)activeEffect = effectFneffectStack.push(effectFn)// 将 fn 的执行结果存储到 res 中const res = fn() // 新增effectStack.pop()activeEffect = effectStack[effectStack.length - 1]// 将 res 作为 effectFn 的返回值return res // 新增}effectFn.options = optionseffectFn.deps = []if (!options.lazy) {effectFn()}return effectFn
}

那么重点来了,lazy和本节要实现的computed的关系在哪?(建议看完整篇再回过来看这个问题)

原因在于computed内部使用lazy去封装了getter,什么是getter?我们接着继续看

getter

从实现层面上看,gettercomputed接收的实参的昵称,代码如下:

function computed(getter) {...}

MDN的定义是:get 语法将对象属性绑定到查询该属性时将被调用的函数,有点抽象是不是?我们看文档中例子就行,代码如下:

const obj = {log: ['a', 'b', 'c'],get latest() {return this.log[this.log.length - 1];},
};console.log(obj.latest);
// Expected output: "c"

可以看到,在obj中使用了get functonName(){}的写法,那么这个作用是什么呢?答案是可以利用obj.functionName去返回动态计算值的属性,特别是不想以显示的方式返回的时候

当然,我们会可能会想,这种方式好像跟直接写函数差不太多,即下列代码所示:

const obj = {log: ['a', 'b', 'c'],latest() {return this.log[this.log.length - 1];},
};console.log(obj.latest());
// Expected output: "c"

区别在于使用get在调用时无需加(),使其看起来像调用一个属性,这样更简洁和更具有语义性,且要执行的逻辑只是计算内部的属性值,这就是使用get去计算的意义

实现computed

computedVue响应式核心API,用于声明计算属性计算属性是依赖于其他的属性依赖而存在的,如果其他的属性依赖变化了,那么就会触发computed进行重新计算,进而得到最新的计算属性,我们可以看下书中的示例,代码如下:

const sumRes = computed(() => obj.foo + obj.bar);effect(() => {// 在该副作用函数中读取 sumRes.valueconsole.log(sumRes.value);
});// 修改 obj.foo 的值
obj.foo++;

在这段示例代码中,sumRes就是声明的计算属性,其依赖于obj.fooobj.bar两个属性依赖,当obj.foo++执行后,会触发computed进而得到新的sumRes

需要注意的是,computed还具有缓存,即如果属于依赖不变的情况下,无论执行多少次,都不会触发computed的重新计算

那么computed是如何设计的呢?

其实读到这里,聪明的读者应该有个思路,就是sumRes就是getter所对应的obj,而sumRes.value就是触发了obj.getter,只不过这个getterget value(){},也就是:

const sumRes = {get value(){return ...}
}

初次实现computed

我们直接来看书中的源码是如何实现满足缓存的,代码如下:

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;
}

可以看到是添加了一个value变量存储getter计算过后的值,并设置了一个开关dirty,第一次为true即产生计算,而计算过后就会置为false,下一次读取时就不会重新计算

但现在又出现了一个问题,如果下次读取时obj.fooobj.bar发生了变化呢?在哪里将dirty置为true

别忘了我们还可以在effect中传入schedular,也就是在调度器中置为true,代码如下:

const effectFn = effect(getter, {  lazy: true,  // 添加调度器,在调度器中将 dirty 重置为 true  scheduler() {  dirty = true;  }  
});

执行的逻辑(简化)是怎么样的?

  1. 读取sumRes.value,触发getter
  2. getter执行时触发读取obj.fooobj.bar
  3. 此时activeEffect栈顶的是封装了getteteffectFn
  4. obj.fooobj.bareffectFn关联,执行完返回sumRes.value
  5. 当触发obj.foo改变时,取出封装了getteteffectFn的执行
  6. trigger中取出schedular执行,将ditry置为true

但现在,并不会重新输出sumRes.value的值

原因在于obj.fooobj.bar关联的effect是封装了getteteffectFn,也就是:

effect(() => obj.foo + obj.bar, {  lazy: true,  // 添加调度器,在调度器中将 dirty 重置为 true  scheduler() {  dirty = true;  }  
});

只会重新执行() => obj.foo + obj.bar,而不是console.log(sumRes.value),所以下一步是要解决如何修改完obj.foo后能够重新执行console.log(sumRes.value)的问题

完善computed

其实设计的关键在于schedular,我们知道在trigger中,如果存在schedular会执行schedular而不是effectFn,所以可以在schedular中执行console.log(sumRes.value),但真正执行的其实是执行封装其的effectFn

那该如何拿到这个console.log(sumRes.value)呢?可以设计一个关联,就是obj.value与封装console.log(sumRes.value)这个effectFn的关联

我们知道,在执行完获取sumRes.value之后,此时此时activeEffect栈顶的是封装了console.log(sumRes.value)effectFn,那么就可以在执行完获取sumRes.value之后调用track,代码如下:

const obj = {  get value() {if (dirty) {value = effectFn()dirty = false}// 当读取 value 时,手动调用 track 函数进行追踪track(obj, 'value')return value}
}

然后再在scheduler中,调用trigger,取出effectFn执行,代码如下:

// computed内
const effectFn = effect(getter, {lazy: true,scheduler() {dirty = true;trigger(obj, 'value')},
});

那么现在,当obj.foo++时,就会重新执行console.log(sumRes.value),也就实现了当obj.fooobj.bar变化时,会重新执行console.log(sumRes.value)这个effect的效果,具体的逻辑可看下图:

执行过程

这就是整个computed的实现原理

小记

这是写的第四篇关于vue3.js响应式设计的内容了,但发现看的人还是比较少的,不管是评论还是收藏数都几乎为
0,不知道是写的不好还是其他的原因

写的初衷还是如同开头说的那般,书中即使讲的明白,但如果缺少一定的基础,在逻辑上还是比较难梳理的,写出来一方面是方便我自己复习,另一方面也是希望能够帮助到想了解或者进阶学习Vue的同学

关于这篇,如果认真阅读了,最起码可以达到以下效果

  1. 学会如何去实现懒执行函数
  2. 了解和学习什么是getter
  3. 知道Vue团队是如何设计和实现computed
  4. 响应式系统在computed的实现逻辑
  5. 面试时问到上述也会答得出
  6. 其他…

谢谢大家的阅读,如有错误的地方请私信笔者

笔者会在近期整理后续章节的笔记发布至博客中,希望大家能多多关注前端小王hs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/857959.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【AI技术】GPT-4o背后的语音技术猜想

前言: 本篇文章全文credit 给到 台大的李宏毅老师,李宏毅老师在机器学习上风趣幽默、深入浅出的讲解,是全宇宙学AI、讲中文学生的福音,强力推荐李宏毅老师的机器学习课程和深度学习 人工智能导论; 李宏毅老师的个人长…

wordpress教程自动采集并发布工具

随着互联网的快速发展,越来越多的人开始关注网络赚钱。而对于许多人来说,拥有一个自己的个人网站是一个不错的选择。然而,要让自己的个人网站内容丰富多样,就需要不断地进行更新。那么,有没有一种方法可以让我们轻松地…

1-Wire的使用

代码: ds18b20.c /*《AVR专题精选》随书例程3.通信接口使用技巧项目:1-Wire 单总线的使用文件:ds1820.c说明:DS18B20驱动文件。为了简单,没有读取芯片地址,也没有计算校验作者:邵子扬时间&…

实现文件分片合并功能并使用Github Actions自动编译Release

一、编译IOS镜像 1.1 编译 起因是公司电脑使用的Win11 23H2的预览版,这个预览版系统的生命周期只到2024-09-18,到期后就会强制每两小时重启。这是Windows强制升级系统的一种手段。 虽然公司里的台式电脑目前用不到,但是里面还保留许多旧项…

Why RAG is slower than LLM?

I used RAG with LLAMA3 for AI bot. I find RAG with chromadb is much slower than call LLM itself. Following the test result, with just one simple web page about 1000 words, it takes more than 2 seconds for retrieving: 我使用RAG(可能是指某种特定的…

【大数据 复习】第8章 Hadoop架构再探讨

一、概念 1.Hadoop1.0的核心组件(仅指MapReduce和HDFS,不包括Hadoop生态系统内的Pig、Hive、HBase等其他组件),主要存在以下不足: (1)抽象层次低,需人工编码 (2&#xf…

md5在ida中的识别

ida中 识别md5 ,先右键转为hex 或者按h _DWORD *__fastcall MD5Init(_DWORD *result) {*result 0;result[1] 0;result[2] 1732584193;result[3] -271733879;result[4] -1732584194;result[5] 271733878;return result; }在ida中当然也可以使用搜索 search imdate-value …

xss.haozi.me靶场通关参考

url&#xff1a;https://xss.haozi.me/ 文章目录 0x000x010x020x030x040x050x060x070x080x090x0A0x0B0x0C00xD00xE00xF0x100x110x12 0x00 先看js代码&#xff0c;第一关给你热热手&#xff0c;没给你加过 payload&#xff1a; <script>alert(1)</script>0x01 这…

【大数据】—量化交易实战案例(基础策略)

声明&#xff1a;股市有风险&#xff0c;投资需谨慎&#xff01;本人没有系统学过金融知识&#xff0c;对股票有敬畏之心没有踏入其大门&#xff0c;所以只能写本文来模拟炒股。 量化交易&#xff0c;也被称为算法交易&#xff0c;是一种使用数学模型和计算机算法来分析市场数…

FlinkCDC pipeline模式 mysql-to-paimon.yaml

flinkcdc 需要引入&#xff1a; source端&#xff1a; flink-cdc-pipeline-connector-mysql-xxx.jar、mysql-connector-java-xxx.jar、 sink端&#xff1a; flink-cdc-pipeline-connector-paimon-xxx.jar flinkcdc官方提供connect包下载地址&#xff0c;pipeline模式提交作业和…

Python 函数注解,给函数贴上小标签

目录 什么是函数注解? 为什么使用函数注解? 如何编写函数注解? 实战演练 与类型提示(Type Hints)的关系 类型安全的运算器 什么是函数注解? 函数注解(Function Annotations)是Python 3中新增的一个特性,它允许为函数的参数和返回值指定类型。 这些注解不会改变…

Paimon Trino Presto的关系 分布式查询引擎

Paimon支持的引擎兼容性矩阵&#xff1a; Trino 是 Presto 同项目的不同版本&#xff0c;是原Faceboo Presto创始人团队核心开发和维护人员分离出来后开发和维护的分支&#xff0c;Trino基于Presto&#xff0c;目前 Trino 和 Presto 都仍在继续开发和维护。 参考&#xff1a;

win10 安装openssl并使用openssl创建自签名证书

win10创建自签名证书 下载安装配置openssl 下载地址&#xff1a; https://slproweb.com/download/Win64OpenSSL-3_3_1.exe https://slproweb.com/products/Win32OpenSSL.html 完成后安装&#xff0c;一路next&#xff0c;到达选位置的之后选择安装的位置&#xff0c;我这里选…

已成功见刊检索的国际学术会议论文海报展示(2)

【先投稿先送审】第四届计算机、物联网与控制工程国际学术会议&#xff08;CITCE 2024) 大会官网&#xff1a;www.citce.org 时间地点&#xff1a;2024年11月1-3日&#xff0c;中国-武汉 收录检索&#xff1a;EI Compendex&#xff0c;Scopus 主办单位&#xff1a;四川师范…

计算机组成原理 —— 存储系统(主存储器基本组成)

计算机组成原理 —— 存储系统&#xff08;主存储器基本组成&#xff09; 0和1的硬件表示整合结构寻址按字寻址和按字节寻址按字寻址按字节寻址区别总结 字寻址到字节寻址转化 我们今天来看一下主存储器的基本组成&#xff1a; 0和1的硬件表示 我们知道一个主存储器是由存储体…

C++ | Leetcode C++题解之第174题地下城游戏

题目&#xff1a; 题解&#xff1a; class Solution { public:int calculateMinimumHP(vector<vector<int>>& dungeon) {int n dungeon.size(), m dungeon[0].size();vector<vector<int>> dp(n 1, vector<int>(m 1, INT_MAX));dp[n][m …

【大数据 复习】第7章 MapReduce(重中之重)

一、概念 1.MapReduce 设计就是“计算向数据靠拢”&#xff0c;而不是“数据向计算靠拢”&#xff0c;因为移动&#xff0c;数据需要大量的网络传输开销。 2.Hadoop MapReduce是分布式并行编程模型MapReduce的开源实现。 3.特点 &#xff08;1&#xff09;非共享式&#xff0c;…

MySQL学习笔记-进阶篇-视图和存储过程

四、视图和存储过程 视图 存储过程 基本语法 创建 CREATE PROCEDURE ([参数列表]) BEGIN --SQL END; 调用 CALL 存储过程名&#xff08;[参数列表]&#xff09; 查看 --查看指定数据库的存储过程及状态信息 SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SHCEMA…

indexedDB---掌握浏览器内建数据库的基本用法

1.认识indexedDB IndexedDB 是一个浏览器内建的数据库&#xff0c;它可以存放对象格式的数据&#xff0c;类似本地存储localstore&#xff0c;但是相比localStore 10MB的存储量&#xff0c;indexedDB可存储的数据量远超过这个数值&#xff0c;具体是多少呢&#xff1f; 默认情…

【软件设计】详细设计说明书(word原件,项目直接套用)

软件详细设计说明书 1.系统总体设计 2.性能设计 3.系统功能模块详细设计 4.数据库设计 5.接口设计 6.系统出错处理设计 7.系统处理规定 软件全套资料&#xff1a;本文末个人名片直接获取或者进主页。