异步更新队列 - Vue2 响应式
前言
这篇文章分析了 Vue 更新过程中使用的异步更新队列的相关代码。通过对异步更新队列的研究和学习,加深对 Vue 更新机制的理解
什么是异步更新队列
先看看下面的例子:
<div id="app"><div id="div" v-if="isShow">被隐藏的内容</div><input @click="getDiv" value="按钮" type="button"></div><script>let vm = new Vue({el: '#app',data: {//控制是否显示#divisShow: false},methods:{getDiv: function () {this.isShow=truevar content = document.getElementById('div').innerHTML;console.log('content',content)}}})
</script>
- 上面的例子是,点击按钮显示被隐藏的 div,同时打印 div 内部 html 的内容。
- 按照我们一般的认知,应该是点击按钮能够显示 div 并且在控制台看到 div 的内部 html 的内容。
但是实际执行的结果确是,div 可以显示出来,但是打印结果的时候会报错,错误原因就是 innerHTML 为 null,也就是 div 不存在。
只有当我们再次点击按钮的时候才会打印出 div 里面的内容。这就是 Vue 的异步更新队列的结果
异步更新队列的概念
Vue 的 dom 更新是异步的,当数据发生变化时 Vue 不是立刻去更新 dom,而是开启一个队列,并缓冲在同一个事件中循环发生的所有数据变化。
在缓冲时,会去除重复的数据,避免多余的计算和 dom 操作。在下一个事件循环 tick 中,刷新队列并执行已去重的工作。
-
所以上面的代码报错是因为当执行 this.isShow=true 时,div 还未被创建出来,知道下次 Vue 事件循环时才开始创建
-
查重机制降低了 Vue 的开销
-
异步更新队列实现的选择:由于浏览器的差异,Vue 会根据当前环境选择 Promise.then 或者 MuMutationObserver,如果两者都不支持,则会用 setImmediate 或者 setTimeout 代替
异步更新队列解析
异步队列源码入口
通过之前对 Vue 数据响应式的分析我们知道,当 Vue 数据发生变化时,会触发 dep 的 notify() 方法,该方法通知观察者 watcher 去更新 dom,我们先看一下这的源码
- from src/core/observer/dep.js
//直接看核心代码notify () {//这是Dep的notify方法,Vue的会对data数据进行数据劫持,该方法被放到data数据的set方法中最后执行//也就是通知更新操作// stabilize the subscriber list firstconst subs = this.subs.slice()if (process.env.NODE_ENV !== 'production' && !config.async) {subs.sort((a, b) => a.id - b.id)}for (let i = 0, l = subs.length; i < l; i++) {// !!!核心:通知watcher进行数据更新//这里的subs[i]其实是Dep维护的一个watcher数组,所以我们下面是执行的watcher中的update方法subs[i].update()}}
- 上面的代码简单来说就是 dep 通知 watcher 尽心更新操作,我们看一下 watcher 相关的代码
from :src/core/observer/watcher.js
//这里只展示部分核心代码//watcher的update方法update () {/* istanbul ignore else *///判断是否存在lazy和sync属性if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {//核心:将当前的watcher放到一个队列中queueWatcher(this)}}
- 上面 watcher 的 update 更新方法简单来说就是调用了一个 queueWatcher 方法,这个方法其实是将当前的 watcher 实例放入到一个队列中,以便完成后面的异步更新队列操作
异步队列入队
下面看看 queueWatcher 的逻辑 from src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {const id = watcher.id//去重的操作,先判断是否在当前队列中存在,避免重复操作if (has[id] == null) {has[id] = trueif (!flushing) {queue.push(watcher)} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}// queue the flushif (!waiting) {waiting = trueif (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()return}// 启动异步任务(刷新当前的计划任务)nextTick(flushSchedulerQueue)}}}
- 上面这段 queueWatcher 的代码的主要作用就是对任务去重,然后启动异步任务,进行跟新操作。接下来我们看一线 nextTick 里面的操作
from src/core/util/next-tick.js
//cb:
export function nextTick (cb?: Function, ctx?: Object) {let _resolve//callbacks:这个方法维护了一个回调函数的数组,将回调函数添家进数组callbacks.push(() => {//添加错误处理if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = true//启动异步函数timerFunc()}// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
- 这里的核心,其实就在 timerFunc 的函数上,该函数根据不同的运行时环境,调用不同的异步更新队列,下面看一下代码
from src/core/util/next-tick.js
/**这部分逻辑就是根据环境来判断timerFunc到底是使用什么样的异步队列**/let timerFunc//首选微任务执行异步操作:Promise、MutationObserver//次选setImmediate最后选择setTimeout// 根据当前浏览器环境选择用什么方法来执行异步任务if (typeof Promise !== 'undefined' && isNative(Promise)) {//如果当前环境支持Promise,则使用Promise执行异步任务const p = Promise.resolve()timerFunc = () => {//最终是执行的flushCallbacks方法p.then(flushCallbacks)//如果是IOS则回退,因为IOS不支持Promiseif (isIOS) setTimeout(noop)}//当前使用微任务执行isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && (//如果当前浏览器支持MutationObserver则使用MutationObserverisNative(MutationObserver) ||MutationObserver.toString() === '[object MutationObserverConstructor]')) {let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {//如果支持setImmediate,则使用setImmediatetimerFunc = () => {setImmediate(flushCallbacks)}} else {//如果上面的条件都不满足,那么最后选择setTimeout方法来完成异步更新队列timerFunc = () => {setTimeout(flushCallbacks, 0)}}
- 从上面代码可以看出,不论 timerFunc 使用的是什么样的异步更新队列,最终执行的函数还是落在了 flushCallbacks 上面,那么我们来看一看,这个方法到底是什么
from src/core/util/next-tick.js
function flushCallbacks () {pending = false//拷贝callbacks数组内容const copies = callbacks.slice(0)//清空callbackscallbacks.length = 0//遍历执行for (let i = 0; i < copies.length; i++) {//执行回调方法copies[i]()}}
- 上面的这个方法就是遍历执行了我们 nextTick 维护的那个回调函数数组, 其实就是将数组的方法依次添加进异步队列进行执行。同时清空 callbacks 数组为下次更新作准备。
上面这几段代码其实都是 watcher 的异步队列更新中的入队操作,通过 queueWatcher 方法中调用的 nextTick(flushSchedulerQueue), 我们知道,其实是将 flushSchedulerQueue 这个方法入队
异步队列的具体更新方法
所以下面我们看一下 flushSchedulerQueue 这个方法到底执行了什么操作
from src/core/observer/scheduler.js
/**我们这里只粘贴跟本次异步队列更新相关的核心代码**///具体的更新操作
function flushSchedulerQueue () {currentFlushTimestamp = getNow()flushing = truelet watcher, id//重新排列queue数组,是为了确保://更新顺序是从父组件到子组件//用户的watcher先于render 的watcher执行(因为用户watcher先于render watcher创建)//当子组件的watcher在父组件的watcher执行时被销毁,则跳过该子组件的watcherqueue.sort((a, b) => a.id - b.id)//queue数组维护的一个watcher数组//遍历queue数组,在queueWatcher方法中我们将传入的watcher实例push到了该数组中for (index = 0; index < queue.length; index++) {watcher = queue[index]if (watcher.before) {watcher.before()}id = watcher.id//清空has对象里面的"id"属性(这个id属性之前在queueWatcher方法里面查重的时候用到了)has[id] = null//核心:最终执行的其实是watcher的run方法watcher.run()//下面是一些警告提示,可以先忽略if (process.env.NODE_ENV !== 'production' && has[id] != null) {circular[id] = (circular[id] || 0) + 1if (circular[id] > MAX_UPDATE_COUNT) {warn('You may have an infinite update loop ' + (watcher.user? `in watcher with expression "${watcher.expression}"`: `in a component render function.`),watcher.vm)break}}}//调用组件updated生命周期钩子相关,先跳过const activatedQueue = activatedChildren.slice()const updatedQueue = queue.slice()resetSchedulerState()callActivatedHooks(activatedQueue)callUpdatedHooks(updatedQueue)if (devtools && config.devtools) {devtools.emit('flush')}
}
- 上面的一堆 flushSchedulerQueue 代码,简单来说就是排列了 queue 数组,然后遍历该数组,执行 watcher.run 方法。所以,异步队列更新当我们入队完以后,真正执行的方法其实是 watcher.run 方法
下面我们来继续看一下 watcher.run 方法,到底执行了什么操作
from src/core/observer/watcher.js
/*** Scheduler job interface.* Will be called by the scheduler.* 上面这段英文注释 是官方注释,从这我们看出该方法最终会被scheduler调用*/run () {if (this.active) {//这里调用了watcher的get方法const value = this.get()if (value !== this.value ||// Deep watchers and watchers on Object/Arrays should fire even// when the value is the same, because the value may// have mutated.isObject(value) ||this.deep) {// set new valueconst oldValue = this.valuethis.value = valueif (this.user) {try {this.cb.call(this.vm, value, oldValue)} catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)}} else {this.cb.call(this.vm, value, oldValue)}}}}
- 上述 run 方法最终要的操作就是调用了 watcher 的 get 方法,该方法我们在之前的源码分析有讲过,主要实现的功能是调用了 data 数据的 get 方法,获取最新数据。
至此,Vue 异步更新队列的核心代码我们就分析完了,为了便于理清思路,我们来一张图总结一下
关于 Vue.$nextTick
我们都知道 . n e x t T i c k 方 法 , 其 实 这 个 ∗ ∗ .nextTick 方法,其实这个 ** .nextTick 方法,其实这个∗∗nextTick** 方法就是直接调用的上面的 nextTick 方法
from src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {return nextTick(fn, this)}
- 由上面的代码我们可以看出,$nextTick 是将我们传入的回调函数加入到了异步更新队列,所以它才能实现 dom 更新后回调
注意,$nextTick() 是会将我们传入的函数加入到异步更新队列中的,但是这里有个问题,如果我们想获得 dom 更新后的数据,我们应该把该逻辑放到更新操作之后
因为加入异步队列先后的问题,如果我们在更新数据之前入队的话 ,是获取不到更新之后的数据的
总结
总结起来就是,当触发数据更新通知时,dep 通知 watcher 进行数据更新,这时 watcher 会将自己加入到一个异步的更新队列中。然后更新队列会将传入的更新操作进行批量处理。
这样就达到了多次更新同时完成,提升了用户体验,减少了浏览器的开销,增强了性能。
相关文章:

异步更新队列 - Vue2 响应式
前言 这篇文章分析了 Vue 更新过程中使用的异步更新队列的相关代码。通过对异步更新队列的研究和学习,加深对 Vue 更新机制的理解 什么是异步更新队列 先看看下面的例子: <div id"app"><div id"div" v-if"isShow&…...
【Unity的URP渲染管线下实现扩展后处理Volume组件_TemporalAntiAliasing(TAA)_抗锯齿(附带下载链接)】
【Unity的URP渲染管线下的TAA抗锯齿】 背景:1. Unity内置的抗锯齿只能够满足部分画面需求。展示一个锯齿示例。2. 在75寸大屏电视上跑通展示一个锯齿示例。- 在Camera上配置3. 安装了一个TAA组建,最后打包APK在安卓机上运行报错。- 经过测试排查,发现是没有将后处理的shader…...

NineData通过AWS FTR认证,打造安全可靠的数据管理平台
近日,NineData 作为新一代的云原生智能数据管理平台,成功通过了 AWS(Amazon Web Service)的 FTR 认证。NineData 在 FTR 认证过程中表现出色,成功通过了各项严格的测试和评估,在数据安全管理、技术应用、流…...

Qt应用开发(基础篇)——滚屏区域类 QScrollArea
一、前言 QScrollArea类继承于QAbstractScrollArea,QAbstractScrollArea继承于QFrame,是Qt滚动视图的常用部件。 滚屏区域基类 QAbstractScrollArea 框架类 QFrame QScrollArea类提供了对另一个小部件的滚动视图,基础功能、滚动条控制、界面策…...

安装最新版chromedriver 116,亲测可用
Version Selection...
html题库
什么是HTML? HTML的全称为 超文本标记语言 ,是一种 标记语言 。 它包括一系列标签 ,通过这些标签可以将网络上的文档格式统一,使分散的 Internet 资源连接为一个逻辑整体。 DOCTYPE 的作用是什么?标准模式与兼容模式(…...

Android11 中 LED 使用-RK3568
文章目录 前言原理图设备树驱动前言 现在我们来学习点亮LED 原理图 然后对应在核心板原理图上查找 Working_LEDEN_H_GPIO0_B7,如下图所示: 那么我们只要控制 GPIO0_B7 即可控制 led 的亮灭。 设备树 leds: leds {compatible = "gpio-leds";work_led: work {gpi…...
BC77 有序序列插入一个数
描述 有一个有序数字序列,从小到大排序,将一个新输入的数插入到序列中,保证插入新数后,序列仍然是升序。 输入描述 第一行输入一个整数(0≤N≤50)。 第二行输入N个升序排列的整数,输入用空格分隔的N个整数。 第三…...
通过脚本使用Cppcheck做静态测试并生成报告(Windows)
1.安装cppcheck 先从cppcheck官方网站下载cppcheck的安装包。 注: (1)官网地址:https://sourceforge.net/projects/cppcheck (2)截止2023年8月,官方发布的最新版本是cppcheck-2.11-x64-Setup.…...
工业安全生产信息化平台的基本架构和关键功能分享
工业安全生产信息化平台是指利用信息技术手段,将工业安全生产管理与数据采集、传输、处理相结合,实现对工业安全生产全过程的数字化、信息化、智能化管理的平台。它通过集成多种信息系统和设备,实现对重大危险源监控预警、安全风险分级管控、…...
每日一道面试题之session 和 cookie 有什么区别?
Session和Cookie是两种在Web开发中用于跟踪用户状态的机制: 它们之间的区别如下: 存储位置:Cookie是存储在用户浏览器中的小型文本文件,而Session是存储在服务器上的数据结构。 数据安全性:Cookie中的数据可以被用户…...

SHELL 基础 显示字符颜色, 修改历史命令,Linux里的命令 执行顺序
echo 打印命令 : 显示字符串 : [rootserver ~]# echo this is SHELL language this is SHELL language [rootserver ~]# echo this is SHELL language this is SHELL language [rootserver ~]# echo "this is SHELL language" this is SH…...
Vue 和 JQuery 的区别在哪?为什么 JQuery 会被 Vue 取代?
在 Web 前端开发领域,我们经常会遇到一些不同的工具和框架,其中 Vue 和 JQuery, JQuery 是曾经备受欢迎的选择,而现在 Vue 是大多数人的选择。本文将探讨 Vue 和 JQuery 之间的区别,并讨论为什么越来越多的开发人员放弃 JQuery 而…...
Spring 中 Bean 注入与获取
Spring 中有哪些方式可以把 Bean 注入到 IOC 容器? 关于这个问题,我的回答入下:把 Bean 注入到 IOC 容器里面的方式有 7 种方式 1. 使用 xml 的方式来声明 Bean 的定义,Spring 容器在启动的时候会加载并解析这 个 xml,…...

STM32 中断复习
中断 打断CPU执行正常的程序,转而处理紧急程序,然后返回原暂停的程序继续运行,就叫中断。 在确定时间内对相应事件作出响应,如:温度监控(定时器中断)。故障处理,检测到故障&#x…...

Django的模型
定义模型 from django.db import models class User(models.Model):# 类属性是表示表的字段username models.CharField(max_length50,uniqueTrue)password models.CharField(max_length200)create_time models.DateTimeField(auto_now_addTrue) # auto_now_add新增数据时间…...
非计算机科班如何丝滑转码
近年来,很多人想要从其他行业跳槽转入计算机领域。非计算机科班如何丝滑转码? 方向一:如何规划才能实现转码? 对于非计算机科班的人来说,想要在计算机领域实现顺利的转码并不是一件容易的事情,但也并非不…...

PyTorch深度学习实战(12)——数据增强
PyTorch深度学习实战(12)——数据增强 0. 前言1. 图像增强1.1 仿射变换1.2 亮度修改1.3 添加噪音1.4 联合使用多个增强方法 2. 对批图像执行图像增强3. 利用数据增强训练模型小结系列链接 0. 前言 数据增强是指通过对原始数据进行一系列变换和处理&…...

SpringCloud Ribbon中的7种负载均衡策略
SpringCloud Ribbon中的7种负载均衡策略 Ribbon 介绍负载均衡设置7种负载均衡策略1.轮询策略2.权重策略3.随机策略4.最小连接数策略5.重试策略6.可用性敏感策略7.区域敏感策略 总结 负载均衡通器常有两种实现手段,一种是服务端负载均衡器,另一种是客户端…...

04 qt功能类、对话框类和文件操作
一 QT中时间和日期 时间 ---- QTime日期 ---- QDate对于Qt而言,在实际的开发过程中, 1)开发者可能知道所要使用的类 ---- >帮助手册 —>索引 -->直接输入类名进行查找 2)开发者可能不知道所要使用的类,只知道开发需求文档 ----> 帮助 手册,按下图操作: 1 …...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...

【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...

HTML 列表、表格、表单
1 列表标签 作用:布局内容排列整齐的区域 列表分类:无序列表、有序列表、定义列表。 例如: 1.1 无序列表 标签:ul 嵌套 li,ul是无序列表,li是列表条目。 注意事项: ul 标签里面只能包裹 li…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...

ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...