当前位置: 首页 > news >正文

深入理解Vue3.js响应式系统设计之调度执行

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

作者:前端小王hs

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

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

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

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

如有帮助,不胜荣幸

前置章节:

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

调度执行

在前面的章节我们学习到,只要修改了代理对象obj的属性就会触发trigger()执行,那么在4.7节,作者向我们介绍了如何去实现可控制的去执行trigger(),也就是决定副作用函数执行的时机、次数以及方式(原文)

那么在这一节,将会涉及到宏任务微任务知识,这是JavaScript高阶知识,我们先来介绍下

JavaScript中,异步任务被分为两种主要的类别:宏任务(MacroTask)和微任务(MicroTask)

这两种任务类型在事件循环(Event Loop) 中被处理,但它们有不同的执行优先级和时机

宏任务包括:

  1. script(整体代码)
  2. setTimeout
  3. setInterval
  4. setImmediate(Node.js环境)
  5. I/O
  6. UI渲染(浏览器)
  7. MessageChannel(消息通道)
  8. postMessage
  9. requestAnimationFrame(浏览器)

微任务包括:

  1. Promise.then() 或 .catch()
  2. MutationObserver(浏览器)
  3. process.nextTick(Node.js环境)
  4. queueMicrotask()(较新的API)

执行的过程是,在每次宏任务执行完后,会检查微任务队列是否有微任务,如果有,那么执行(且执行完所有微任务),如果没有,则从宏任务队列中继续执行下一个宏任务。也就是说,微任务是在两个宏任务之间执行的

而如果存在同步代码,则宏任务队列异步函数会在同步代码执行后再执行,可看下面这个例子:

执行情况

现在,我们来看一个需求

const data = { foo: 1 };
const obj = new Proxy(data, { /* ... */ });effect(() => {console.log(obj.foo);
});obj.foo++;console.log('结束了');

这段代码中,正常的输出顺序是12、最后是结束了,这点非常简单,但如果想把2放在结束了之后呢?那么很明显,就需要控制副作用函数的执行时机

结合上面我们说的宏任务,是不是思路就来了?把结束了这个script语句放到执行完fn()之后即可,下面我们来看看是如何实现的

第一步:给effect函数涉及一个选项参数options,其实就是传入一个对象,对象内是一个可执行函数,参数为fn(),代码如下:

// 书中源代码
effect(() => {console.log(obj.foo);},// options{// 调度器 scheduler 是一个函数  scheduler(fn) {// ... 这里是调度器函数的实现}}
);

第二步:在effect内部将options挂载到effectFn上,代码如下:

function effect(fn, options = {}) {const effectFn = () => {cleanup(effectFn)activeEffect = effectFneffectStack.push(effectFn)fn()effectStack.pop()activeEffect = effectStack[effectStack.length - 1]}effectFn.options = options // 新增effectFn.deps = []effectFn()
}

第三步:在trigger中进行判断,如果effectFn.options存在scheduler,那么执行scheduler,代码如下:

function trigger(target, key) {const depsMap = bucket.get(target)if (!depsMap) returnconst effects = depsMap.get(key)const effectsToRun = new Set(effects && effects.forEach(effectFn => {if (effectFn !== activeEffect) {effectsToRun.add(effectFn)}})effectsToRun.forEach(effectFn => {// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递if (effectFn.options.scheduler) {effectFn.options.scheduler(effectFn)} else {// 否则直接执行副作用函数(之前的默认行为)effectFn()}})
}

那么现在,当我们传入scheduler(fn)之后,就会被保存在effectFn.options中,当再次触发trigger时,就会执行effectFn.optionsfn(),关键是scheduler里应该怎么处理呢?其实非常简单,只需添加一个setTimeout,然后在定时器里执行fn(),还记得我们在上面说过,这是一个宏任务,代码如下:

// effect()内
{scheduler(fn) {  // 将副作用函数放到宏任务队列中执行  setTimeout(fn);}
}

现在,我们来分析一下代码的执行顺序:

  1. 执行effect,那么会输出1,以及把scheduler(fn){}传给options
  2. 执行obj.foo++,那么先会涉及到一个读取,但这里我们无需关心读取,只看trigger
  3. trigger中执行effectFn.optionsfn(),那么这是一个宏任务,会被送进队列中等待当前宏任务执行完成
  4. 由于setTimeout是一个异步函数,而console.log('结束了')是一个同步代码,所以会先输出结束了,然后再输出完成了++之后的2

下面,我们再来看一个更加进阶的操作,我们直接分析需求和实现过程。首先是需求,代码如下:

const data = { foo: 1 };
const obj = new Proxy(data, { /* ... */ });  effect(() => {console.log(obj.foo);
});obj.foo++;  
obj.foo++;

通过逻辑,我们知道会按顺序输出123,现在的需求是直接输出13,也就是跳过21我们知道,直接由fn()初次执行就会输出,那3呢?

我们来看一下解决的实现过程,代码如下:

// 定义一个任务队列
const jobQueue = new Set();
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve();// 一个标志代表是否正在刷新队列
let isFlushing = false;
function flushJob() {// 如果队列正在刷新,则什么都不做if (isFlushing) return;// 设置为 true,代表正在刷新isFlushing = true;// 在微任务队列中刷新 jobQueue 队列p.then(() => {jobQueue.forEach(job => job());}).finally(() => {// 结束后重置 isFlushingisFlushing = false;});
}effect(() => {console.log(obj.foo);
}, {scheduler(fn) {// 每次调度时,将副作用函数添加到 jobQueue 队列中jobQueue.add(fn);// 调用 flushJob 刷新队列flushJob();}
});obj.foo++;
obj.foo++;

我们分析一下实现的过程:

  1. 执行effect,那么首先输出当前的1,并且把scheduler里的函数放进effectFn.options
  2. 执行obj.foo++,读取我们还是不分析,我们直接看触发时执行scheduler里的函数
  3. 执行jobQueue.add(fn),这是一个Set数据结构,也就是里面的值是唯一的
  4. 执行flushJob,将isFlushing置为true,然后把jobQueue里的每一项fn()添加到微任务队列中,并且在执行完后会将isFlushing置为false,不过此时并没有执行
  5. 我们知道,微任务之后在宏任务执行完后才会执行,那么此时flushJob执行完了,obj.foo++也执行完成了,foo变为2
  6. 注意!flushJob同步代码obj.foo++也是同步代码,所以先执行obj.foo++,那么在这里的时候,jobQueue.add(fn);是无效的,因为Set里的都是唯一值,那么他会执行flushJob
  7. 执行flushJob时,由于前面我们已经置为了ture,所以直接return
  8. 那么当所有的同步代码执行完后,就查看微任务队列了,就把p.then里的代码拿出来执行
  9. 执行console.log(obj.foo);,此时就已经了是3了!

如何设计只执行一次?结合Set只保存唯一值、微任务的特性和函数开关(布尔值

分析的时候是不是觉得有点绕?只要搞清楚了同步和异步以及宏任务和微任务就清晰了,但步骤确实是有点多,也不得不佩服高级前端工程师的设计思路,一环套一环

如果你看到这里,那么可以和hr夸夸其谈:

  • 什么是宏任务和微任务
  • 有同步函数和异步函数、异步函数有微任务时的执行顺序
  • 如何设计只执行一次的逻辑

结语

那么到这里,我们就跟着书籍解决了如何实现调度的问题,下一节笔记我们来探讨如何实现computed,这是实现响应式核心的关键API

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

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

相关文章:

  • FlinkCDC介绍及使用
  • 【论文速读】|对BusyBox进行模糊测试:利用大语言模型和崩溃重用挖掘嵌入式系统中的漏洞
  • Ubuntu 22.04.1 安装ubuntu有道词典时错误发生
  • 2352.相等行列对
  • Jmeter性能 之 “查看结果树” 界面功能介绍
  • 各种开发语言运行时占用内存情况比较
  • 视频智能分析平台LntonAIServer安防监控视频平台行人入侵检测算法核心特点及其应用价值
  • 网络与协议安全复习 - 电子邮件安全
  • Java宝藏实验资源库(4)对象数组
  • redis-基础篇(1)
  • ubuntu server 22.04安装 fdfs
  • 上海国际嵌入式展 - 基于树莓派5和CODESYS的16轴运动控制解决方案
  • <Python><paddleocr>基于python使用百度paddleocr实现图片文字识别与替换
  • 前后端分离的后台管理系统源码,快速开发OA、CMS网站后台管理、毕业设计项目
  • android 一个manifest 可以有 多个 hal 吗 ?
  • 2017-09-12 前端日报
  • C# 免费离线人脸识别 2.0 Demo
  • Javascript Math对象和Date对象常用方法详解
  • JavaScript 无符号位移运算符 三个大于号 的使用方法
  • leetcode388. Longest Absolute File Path
  • miaov-React 最佳入门
  • SOFAMosn配置模型
  • windows-nginx-https-本地配置
  • 构建二叉树进行数值数组的去重及优化
  • 后端_MYSQL
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用(一)
  • 算法之不定期更新(一)(2018-04-12)
  • 微信小程序开发问题汇总
  • 小程序上传图片到七牛云(支持多张上传,预览,删除)
  • 正则表达式
  • HanLP分词命名实体提取详解
  • PostgreSQL之连接数修改
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • ​第20课 在Android Native开发中加入新的C++类
  • #laravel部署安装报错loadFactoriesFrom是undefined method #
  • $ git push -u origin master 推送到远程库出错
  • (02)Cartographer源码无死角解析-(03) 新数据运行与地图保存、加载地图启动仅定位模式
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (每日一问)操作系统:常见的 Linux 指令详解
  • (四)模仿学习-完成后台管理页面查询
  • (一一四)第九章编程练习
  • (原)Matlab的svmtrain和svmclassify
  • (转)C语言家族扩展收藏 (转)C语言家族扩展
  • .Net IOC框架入门之一 Unity
  • .NET 反射的使用
  • .NET/C# 如何获取当前进程的 CPU 和内存占用?如何获取全局 CPU 和内存占用?
  • .NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表
  • @Autowired注解的实现原理
  • @require_PUTNameError: name ‘require_PUT‘ is not defined 解决方法
  • @Transactional注解下,循环取序列的值,但得到的值都相同的问题
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798
  • [001-03-007].第07节:Redis中的事务
  • [240903] Qwen2-VL: 更清晰地看世界 | Elasticsearch 再次拥抱开源!
  • [ACL2022] Text Smoothing: 一种在文本分类任务上的数据增强方法
  • [AI Google] 使用 Gemini 取得更多成就:试用 1.5 Pro 和更多智能功能