深入源码设计!Vue3.js核心API——Computed实现原理
如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~
作者:前端小王hs
阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主
此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来
书籍:《Vue.js设计与实现》 作者:霍春阳
本篇博文将在书第4.8节的基础上进一步解析,附加了测试的代码运行示例,以及对书籍中提到的ES6中的数据结构及其特点进行阐述,方便正在学习Vue3或想分析Vue3源码的朋友快速阅读
如有帮助,不胜荣幸
前置章节:
- 深入理解Vue3.js响应式系统基础逻辑
- 深入理解Vue3.js响应式系统设计之栈结构和循环问题
- 深入理解Vue3.js响应式系统设计之调度执行

懒执行lazy的effect
经过前置的章节的学习,可以发现设计的effect都是立即执行的,而在4.8节的开始,作者向我们设计了一个可以按意愿时间节点执行的effect,其设计逻辑非常简单,就是在传递effect的options形参时添加内置lazy属性,然后再在effect函数内部进行判断,如果存在lazy,则返回effectFn给调用者。那么何时何地执行effectFn的决定权就到了调用者的手上
传递lazy代码如下:
effect(// 指定了 lazy 选项,这个函数不会立即执行() => {console.log(obj.foo)},// options{lazy: true}
)
完善判断lazy的effect代码如下:
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去接收调用函数的返回值,那么就需要在effect内return 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
从实现层面上看,getter是computed接收的实参的昵称,代码如下:
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
computed是Vue响应式的核心API,用于声明计算属性,计算属性是依赖于其他的属性依赖而存在的,如果其他的属性依赖变化了,那么就会触发computed进行重新计算,进而得到最新的计算属性,我们可以看下书中的示例,代码如下:
const sumRes = computed(() => obj.foo + obj.bar);effect(() => {// 在该副作用函数中读取 sumRes.valueconsole.log(sumRes.value);
});// 修改 obj.foo 的值
obj.foo++;
在这段示例代码中,sumRes就是声明的计算属性,其依赖于obj.foo和obj.bar两个属性依赖,当obj.foo++执行后,会触发computed进而得到新的sumRes
需要注意的是,computed还具有缓存,即如果属于依赖不变的情况下,无论执行多少次,都不会触发computed的重新计算
那么computed是如何设计的呢?
其实读到这里,聪明的读者应该有个思路,就是sumRes就是getter所对应的obj,而sumRes.value就是触发了obj.getter,只不过这个getter是get 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.foo或obj.bar发生了变化呢?在哪里将dirty置为true?
别忘了我们还可以在effect中传入schedular,也就是在调度器中置为true,代码如下:
const effectFn = effect(getter, { lazy: true, // 添加调度器,在调度器中将 dirty 重置为 true scheduler() { dirty = true; }
});
执行的逻辑(简化)是怎么样的?
- 读取
sumRes.value,触发getter getter执行时触发读取obj.foo和obj.bar- 此时
activeEffect栈顶的是封装了gettet的effectFn obj.foo和obj.bar与effectFn关联,执行完返回sumRes.value- 当触发
obj.foo改变时,取出封装了gettet的effectFn的执行 - 在
trigger中取出schedular执行,将ditry置为true
但现在,并不会重新输出sumRes.value的值
原因在于obj.foo和obj.bar关联的effect是封装了gettet的effectFn,也就是:
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.foo或obj.bar变化时,会重新执行console.log(sumRes.value)这个effect的效果,具体的逻辑可看下图:

这就是整个computed的实现原理
小记
这是写的第四篇关于vue3.js响应式设计的内容了,但发现看的人还是比较少的,不管是评论还是收藏数都几乎为
0,不知道是写的不好还是其他的原因
写的初衷还是如同开头说的那般,书中即使讲的明白,但如果缺少一定的基础,在逻辑上还是比较难梳理的,写出来一方面是方便我自己复习,另一方面也是希望能够帮助到想了解或者进阶学习Vue的同学
关于这篇,如果认真阅读了,最起码可以达到以下效果
- 学会如何去实现懒执行函数
- 了解和学习什么是
getter - 知道Vue团队是如何设计和实现
computed的 - 响应式系统在
computed的实现逻辑 - 面试时问到上述也会答得出
- 其他…
谢谢大家的阅读,如有错误的地方请私信笔者
笔者会在近期整理后续章节的笔记发布至博客中,希望大家能多多关注前端小王hs!
相关文章:
深入源码设计!Vue3.js核心API——Computed实现原理
如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~ 作者:前端小王hs 阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主 此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来 书籍&a…...
驾考小技巧:老北京布鞋!距离高考出分还剩3天,我却看到有些孩子已经拿了“满分”——早读(逆天打工人爬取热门微信文章解读)
我20年驾校4000多块钱,你呢? 引言Python 代码第一篇 洞见 距离高考出分还剩3天,我却看到有些孩子已经拿了“满分”第二篇 视频新闻结尾 引言 昨天的文章顺利发出 看来“梅西” 这两个字在我们这边 不是敏感词 只是很多个罗粉搞得有点过头了 …...
java-正则表达式 2
7. 复杂的正则表达式示例(续) 7.1 验证日期格式 以下正则表达式用于验证日期格式,例如YYYY-MM-DD。 import java.util.regex.*;public class RegexExample {public static void main(String[] args) {String[] dates {"2023-01-01&q…...
hadoop常见简单基础面试题
文章目录 hadoop简单基础面试题1. 请说下 HDFS 读写流程2. HDFS 在读取文件的时候,如果其中一个块突然损坏了怎么办3. HDFS 在上传文件的时候,如果其中一个 DataNode 突然挂掉了怎么办4. NameNode 在启动的时候会做哪些操作5.Secondary NameNode 了解吗&…...
泄漏检测(LDAR)在建档和检测过程中造假套路和不规范行为
第一章 建档环节造假和不规范 一、 企业行为: 企业为了节约检测费,采取部分建档,部分密封点检测的行为 二、 第三方检测公司不规范行为: 1、台账信息不准确,密封点命名不准确 &…...
Android CTS环境搭建
CTS即Compatibility Test Suite意为兼容性测试,是Google推出的Android平台兼容性测试机制。其目的是尽早发现不兼容性,并确保软件在整个开发过程中保持兼容性。只有通过CTS认证的设备才能合法的安装并使用Google market等Google应用。 搭建CTS测试环境需…...
比较Zig、Rust和C++
比较Zig、Rust和C这三种编程语言,我们可以从以下几个关键维度来进行: 设计理念 表格 语言 设计理念 Zig 简洁性、模块化、避免常见错误 Rust 内存安全、并发性、性能 C 性能优化、资源控制、可扩展性 内存安全 Zig通过严格的编译时检查、可选…...
路由的params参数,命名路由,路由的params参数,命名路由
上篇我们讲了vue路由的使用 今天我们来讲vue中路由的嵌套,路由的params参数,命名路由 一.路由的params参数 1.配置路由规则,使用children配置项: router:[{path:/about,component:About,},{path:component:Home,//通过children配置子路由c…...
java:CompletableFuture的简单例子
java:CompletableFuture的简单例子 package com.chz.myTest;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.uti…...
element的table获取当前表格行
需求:验证表格同一行的最低限价不能超过销售定价 思路:先获取当前行table的index,然后在做大小比较 1.局部html <el-table-column label"销售定价(元)" min-width"200px"><template slot"header"&…...
html做一个分组散点图图的软件
在HTML中创建一个分组散点图,可以结合JavaScript库如D3.js或Plotly.js来实现。这些库提供了强大的数据可视化功能,易于集成和使用。下面是一个使用Plotly.js创建分组散点图的示例: 要添加文件上传功能,可以让用户上传包含数据的文…...
【SQL】UNION 与 UNION ALL 的区别
在 SQL 中,UNION 和 UNION ALL 都用于将两个或多个结果集合并为一个结果集,但它们在处理重复数据方面有显著区别。以下是它们的详细区别: 1. UNION UNION 操作符用于合并两个或多个 SELECT 语句的结果集,并自动去除结果集中重复…...
分类判决界面---W-H、H-K算法
本篇文章是博主在人工智能等领域学习时,用于个人学习、研究或者欣赏使用,并基于博主对人工智能等领域的一些理解而记录的学习摘录和笔记,若有不当和侵权之处,指出后将会立即改正,还望谅解。文章分类在AI学习笔记&#…...
Python基础教程(三十):math模块
💝💝💝首先,欢迎各位来到我的博客,很高兴能够在这里和您见面!希望您在这里不仅可以有所收获,同时也能感受到一份轻松欢乐的氛围,祝你生活愉快! 💝Ὁ…...
你只是重新发现了一些东西
指北君关于另外一条思维路径的发现。 "自以为是"的顿悟时刻 有很多时候,我会"自以为是"的发现/发明一些东西。这种"自以为是"的时刻通常还带有一些骄傲自豪的情绪。这种感觉特别像古希腊博学家阿基米德 在苦思冥想如何测量不规则物体…...
【英伟达GPU的挑战者】Groq—AI大模型推理的革命者
目录 引言第一部分:Groq简介第二部分:Groq的特点与优势1、高性能推理加速2、近存计算技术3、专用ASIC芯片设计4、低延迟与高吞吐量5、成本效益分析6、易用性与集成性7、软件与硬件的协同设计 第三部分:Groq的使用指南1、准备工作2、简单使用样…...
Python学习路线
Python学习路线 领取资料 一、Python基础知识 Python入门:了解Python的安装方法、如何运行Python程序以及交互模式的使用,同时学习注释的添加方法。 数据类型:掌握Python中的各种数据类型,包括数字、布尔值、字符串、列表、元…...
C++ std::forward()
在线调试网站: https://wandbox.org/ #include <iostream> #include <thread> #include <mutex> void func(int &&args) {std::cout << args << std::endl; }int main () {int a 10;func(20); …...
常见的8种排序(含代码):插入排序、冒泡排序、希尔排序、快速排序、简单选择排序、归并排序、堆排序、基数排序
时间复杂度O(n^2) 1、插入排序 (Insertion Sort) 从第一个元素开始,该元素可以认为已经被排序;取出下一个元素,在已经排序的元素序列中从后向前扫描;如果该元素(已排序)大于新元素,将该元素移到…...
go语言day2
使用cmd 中的 go install ; go build 命令出现 go cannot find main module 错误怎么解决? go学习-问题记录(开发环境)go: cannot find main module; see ‘go help modules‘_go: no flags specified (see go help mod edit)-CSDN博客 在本…...
第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
C++ 基础特性深度解析
目录 引言 一、命名空间(namespace) C 中的命名空间 与 C 语言的对比 二、缺省参数 C 中的缺省参数 与 C 语言的对比 三、引用(reference) C 中的引用 与 C 语言的对比 四、inline(内联函数…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
浪潮交换机配置track检测实现高速公路收费网络主备切换NQA
浪潮交换机track配置 项目背景高速网络拓扑网络情况分析通信线路收费网络路由 收费汇聚交换机相应配置收费汇聚track配置 项目背景 在实施省内一条高速公路时遇到的需求,本次涉及的主要是收费汇聚交换机的配置,浪潮网络设备在高速项目很少,通…...
华为OD机试-最短木板长度-二分法(A卷,100分)
此题是一个最大化最小值的典型例题, 因为搜索范围是有界的,上界最大木板长度补充的全部木料长度,下界最小木板长度; 即left0,right10^6; 我们可以设置一个候选值x(mid),将木板的长度全部都补充到x,如果成功…...
Python竞赛环境搭建全攻略
Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型(算法、数据分析、机器学习等)不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...
C++_哈希表
本篇文章是对C学习的哈希表部分的学习分享 相信一定会对你有所帮助~ 那咱们废话不多说,直接开始吧! 一、基础概念 1. 哈希核心思想: 哈希函数的作用:通过此函数建立一个Key与存储位置之间的映射关系。理想目标:实现…...
