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

news2024/12/25 9:39:07

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

作者:前端小王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 = effectFn
    effectStack.push(effectFn)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.options = options
  effectFn.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 = effectFn
    effectStack.push(effectFn)
    // 将 fn 的执行结果存储到 res 中
    const res = fn() // 新增
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 将 res 作为 effectFn 的返回值
    return res // 新增
  }
  effectFn.options = options
  effectFn.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.value
  console.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.coloradmin.cn/o/1850321.html

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

相关文章

linux普通: rocketmq的安装测试与可视化界面安装,git的 (linux) 安装

全文目录,一步到位 1.前言简介1.1 专栏传送门(rabbitmq) 2. rocketmq使用及安装2.0 开放端口2.1 rocketmq版本说明2.2 具体操作2.2.1 修改文件2.2.2 具体启动指令ps: 查看日志 2.3.3 jps查看java进程2.3.4 测试运行情况> 步骤一: 临时指定nameserver注册中心位置> 步骤二…

Nginx 搭建域名访问环境

1.Nginx配置文件 server {listen 80;server_name www.gulimall.com;#charset koi8-r;#access_log /var/log/nginx/log/host.access.log main;location / {proxy_pass http://192.168.232.1:10001;}#error_page 404 /404.html;# redirect server error p…

学习redis根本不愁,简单明了

一、redis是什么? 在认识redis之前,我们先说一下什么是NoSQL? 1. NoSQL NoSQL,顾名思义就是不仅仅是SQL,泛指非关系数据库。 2. NoSQL的四大分类 (1)键值(key-value)存储…

程序猿成长之路之数据挖掘篇——决策树分类算法(1)——信息熵和信息增益

决策树不仅在人工智能领域发挥着他的作用,而且在数据挖掘中也在分类领域中独占鳌头。了解决策树的思想是学习数据挖掘中的分类算法的关键,也是学习分类算法的基础。 什么是决策树 用术语来说,决策树(Decision Tree)是…

写代码必用字体

下载链接 字体下载链接 使用情况/截图 软件:DEV-CPP 系统:Win10专业版 自带判等、大于、小于等符号的专属字体

微信小程序开发---自定义底部tabBar

自定义tabBar注意事项: 在自定义 tabBar 模式下 ,为了保证低版本兼容以及区分哪些页面是 tab 页,app.json文件中 tabBar 的相关配置项需完整声明,但这些字段不会作用于自定义 tabBar 的渲染。所有 tabBar 的样式都由该自定义组件…

java:JWT的简单例子

【pom.xml】 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3.12.RELEASE</version> </dependency> <dependency><groupId>org.springf…

Hadoop 2.0 大家族(三)

目录 五、Hive&#xff08;一&#xff09;Hive简介&#xff08;二&#xff09;Hive入门 六、Oozie&#xff08;一&#xff09;Oozie简介&#xff08;二&#xff09;Oozie入门 五、Hive Hive是一个构建在Hadoop上的数据仓库框架&#xff0c;它起源于Facebook内部信息处理平台。H…

java基于ssm+jsp快递管理系统源码(适合新手)

在管理信息系统的生命周期中&#xff0c;仅过了需求分析、系统设计等阶段之后&#xff0c;便开始了系统实施阶段。在系统分析和设计阶段&#xff0c;系统开发工作主要是集中在逻辑、功能和技术设计上&#xff0c;系统实施阶段要继承此前面各个阶段的工作成果&#xff0c;将技术…

钡铼BL110在智慧气象站实现Modbus转MQTT无线接入主流云

随着物联网&#xff08;IoT&#xff09;技术的发展&#xff0c;各行各业都在积极探索将智能设备与云平台相结合&#xff0c;以提升系统的智能化和自动化水平。智慧气象站作为其中重要的一环&#xff0c;通过实时监测环境数据&#xff0c;为农业、交通、航空等行业提供精准的气象…

FPGA 690T 高速存储设计

高速存储设计会有各种需求的考虑&#xff0c;那么对应的方案也不完全相同&#xff0c;这篇文章出一期纯FPGA实现的高速存储方案。用纯fpga实现高速存储板卡有易国产化&#xff0c;功耗低和体积小等特点&#xff0c;缺点就是灵活性不是很强&#xff0c;实现标准ext4和nfs文件系统…

AI大模型日报#0622:Claude 3.5 Sonnet超越GPT-4o、盘古大模型跳级发布、松鼠AI多模态教育大模型

导读&#xff1a;AI大模型日报&#xff0c;爬虫LLM自动生成&#xff0c;一文览尽每日AI大模型要点资讯&#xff01;目前采用“文心一言”&#xff08;ERNIE-4.0-8K-latest&#xff09;生成了今日要点以及每条资讯的摘要。欢迎阅读&#xff01;《AI大模型日报》今日要点&#xf…

不同版本的 Rocky Linux 快速更换阿里镜像源

环境&#xff1a;兼容 Rocky Linux 任意版本。 搞服务器系统从 CentOS 折腾到 Rocky Linux&#xff0c;然后又折腾到 Alma Linux&#xff1b;最近因为 RKE2 没有做 Alma Linux 的兼容性&#xff0c;又折腾到了 Rocky Linux &#xff0c;真的是一把鼻涕一把泪呀。但是实在是不理…

关于jupyter notebook的使用经验

jupyter notebook 第一点&#xff0c;调整每次打开jupyter notebook的时候的位置第二点&#xff0c;如何设置jupyter notebook可以使用本地anaconda创建的虚拟环境呢&#xff1f;第三点&#xff0c;使用jupyter notebook的技巧 以下三点都是独立的&#xff0c;可以根据自己的需…

Web项目部署后浏览器刷新返回Nginx的404错误对应解决方案

data: 2024/6/22 16:05:34 周六 limou3434 叠甲&#xff1a;以下文章主要是依靠我的实际编码学习中总结出来的经验之谈&#xff0c;求逻辑自洽&#xff0c;不能百分百保证正确&#xff0c;有错误、未定义、不合适的内容请尽情指出&#xff01; 文章目录 1.源头2.排错3.原因4.解…

政务大厅引导系统:AR、VR技术革新引领政务服务体验升级

一、传统政务大厅面临的普遍痛点 随着城市的发展和政务服务需求的增长&#xff0c;传统的政务大厅面临着诸多挑战和痛点&#xff1a; 信息不对称&#xff1a;政务大厅内各部门信息分散&#xff0c;群众难以快速获取全面准确的服务信息&#xff0c;导致办事效率低下。 办事流…

如何恢复电脑硬盘删除数据?提供一套实用恢复方案

在数字化时代&#xff0c;电脑硬盘中存储的数据对于个人和企业来说都至关重要。然而&#xff0c;有时我们可能会不小心删除了一些重要文件&#xff0c;或者因为某种原因导致数据丢失。这时候&#xff0c;恢复硬盘上被删除的数据就显得尤为重要。本文将为您提供一套实用的电脑硬…

JAVA大型医院绩效考核系统源码:​医院绩效考核实施的难点痛点

JAVA大型医院绩效考核系统源码&#xff1a;​医院绩效考核实施的难点痛点 绩效考核数字化综合管理系统是一个基于数字化技术的管理平台&#xff0c;用于帮助企业、机构等组织进行绩效考评的各个环节的管理和处理。它将绩效考评的各个环节集成到一个系统中&#xff0c;包括目标…

Vue79-路由组件独有的2个新的生命周期钩子

一、需求 news.vue路由组件被缓存了&#xff08;因为想要保留里面的输入框的数据&#xff01;&#xff09;&#xff0c;导致&#xff0c;路由页面切走&#xff0c;组件也不会被销毁&#xff0c;所以&#xff0c;beforeDestroy()函数就不会被执行&#xff0c;所以&#xff0c;定…

java—Mybatis缓存

缓存的作用 缓存(cache&#xff09;的作用是为了减轻数据库的压力&#xff0c;提高查询性能。 为什么使用缓存 mysql数据库保存的数据均在硬盘中&#xff0c;CPU是不会直接和硬盘进行交互的&#xff0c;因为硬盘的数据传输率很低&#xff0c;而CPU的数据传输率很高, CPU和内存直…