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

异步更新队列 - 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 更新过程中使用的异步更新队列的相关代码。通过对异步更新队列的研究和学习&#xff0c;加深对 Vue 更新机制的理解 什么是异步更新队列 先看看下面的例子&#xff1a; <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认证,打造安全可靠的数据管理平台

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

Qt应用开发(基础篇)——滚屏区域类 QScrollArea

一、前言 QScrollArea类继承于QAbstractScrollArea&#xff0c;QAbstractScrollArea继承于QFrame&#xff0c;是Qt滚动视图的常用部件。 滚屏区域基类 QAbstractScrollArea 框架类 QFrame QScrollArea类提供了对另一个小部件的滚动视图&#xff0c;基础功能、滚动条控制、界面策…...

安装最新版chromedriver 116,亲测可用

Version Selection...

html题库

什么是HTML? HTML的全称为 超文本标记语言 &#xff0c;是一种 标记语言 。 它包括一系列标签 &#xff0c;通过这些标签可以将网络上的文档格式统一&#xff0c;使分散的 Internet 资源连接为一个逻辑整体。 DOCTYPE 的作用是什么&#xff1f;标准模式与兼容模式&#xff08;…...

Android11 中 LED 使用-RK3568

文章目录 前言原理图设备树驱动前言 现在我们来学习点亮LED 原理图 然后对应在核心板原理图上查找 Working_LEDEN_H_GPIO0_B7,如下图所示: 那么我们只要控制 GPIO0_B7 即可控制 led 的亮灭。 设备树 leds: leds {compatible = "gpio-leds";work_led: work {gpi…...

BC77 有序序列插入一个数

描述 有一个有序数字序列&#xff0c;从小到大排序&#xff0c;将一个新输入的数插入到序列中&#xff0c;保证插入新数后&#xff0c;序列仍然是升序。 输入描述 第一行输入一个整数(0≤N≤50)。 第二行输入N个升序排列的整数&#xff0c;输入用空格分隔的N个整数。 第三…...

通过脚本使用Cppcheck做静态测试并生成报告(Windows)

1.安装cppcheck 先从cppcheck官方网站下载cppcheck的安装包。 注&#xff1a; &#xff08;1&#xff09;官网地址&#xff1a;https://sourceforge.net/projects/cppcheck &#xff08;2&#xff09;截止2023年8月&#xff0c;官方发布的最新版本是cppcheck-2.11-x64-Setup.…...

工业安全生产信息化平台的基本架构和关键功能分享

工业安全生产信息化平台是指利用信息技术手段&#xff0c;将工业安全生产管理与数据采集、传输、处理相结合&#xff0c;实现对工业安全生产全过程的数字化、信息化、智能化管理的平台。它通过集成多种信息系统和设备&#xff0c;实现对重大危险源监控预警、安全风险分级管控、…...

每日一道面试题之session 和 cookie 有什么区别?

Session和Cookie是两种在Web开发中用于跟踪用户状态的机制&#xff1a; 它们之间的区别如下&#xff1a; 存储位置&#xff1a;Cookie是存储在用户浏览器中的小型文本文件&#xff0c;而Session是存储在服务器上的数据结构。 数据安全性&#xff1a;Cookie中的数据可以被用户…...

SHELL 基础 显示字符颜色, 修改历史命令,Linux里的命令 执行顺序

echo 打印命令 &#xff1a; 显示字符串 &#xff1a; [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 前端开发领域&#xff0c;我们经常会遇到一些不同的工具和框架&#xff0c;其中 Vue 和 JQuery, JQuery 是曾经备受欢迎的选择&#xff0c;而现在 Vue 是大多数人的选择。本文将探讨 Vue 和 JQuery 之间的区别&#xff0c;并讨论为什么越来越多的开发人员放弃 JQuery 而…...

Spring 中 Bean 注入与获取

Spring 中有哪些方式可以把 Bean 注入到 IOC 容器&#xff1f; 关于这个问题&#xff0c;我的回答入下&#xff1a;把 Bean 注入到 IOC 容器里面的方式有 7 种方式 1. 使用 xml 的方式来声明 Bean 的定义&#xff0c;Spring 容器在启动的时候会加载并解析这 个 xml&#xff0c;…...

STM32 中断复习

中断 打断CPU执行正常的程序&#xff0c;转而处理紧急程序&#xff0c;然后返回原暂停的程序继续运行&#xff0c;就叫中断。 在确定时间内对相应事件作出响应&#xff0c;如&#xff1a;温度监控&#xff08;定时器中断&#xff09;。故障处理&#xff0c;检测到故障&#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新增数据时间…...

非计算机科班如何丝滑转码

近年来&#xff0c;很多人想要从其他行业跳槽转入计算机领域。非计算机科班如何丝滑转码&#xff1f; 方向一&#xff1a;如何规划才能实现转码&#xff1f; 对于非计算机科班的人来说&#xff0c;想要在计算机领域实现顺利的转码并不是一件容易的事情&#xff0c;但也并非不…...

PyTorch深度学习实战(12)——数据增强

PyTorch深度学习实战&#xff08;12&#xff09;——数据增强 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.区域敏感策略 总结 负载均衡通器常有两种实现手段&#xff0c;一种是服务端负载均衡器&#xff0c;另一种是客户端…...

04 qt功能类、对话框类和文件操作

一 QT中时间和日期 时间 ---- QTime日期 ---- QDate对于Qt而言,在实际的开发过程中, 1)开发者可能知道所要使用的类 ---- >帮助手册 —>索引 -->直接输入类名进行查找 2)开发者可能不知道所要使用的类,只知道开发需求文档 ----> 帮助 手册,按下图操作: 1 …...

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周&#xff0c;有很多同学在写期末Java web作业时&#xff0c;运行tomcat出现乱码问题&#xff0c;经过多次解决与研究&#xff0c;我做了如下整理&#xff1a; 原因&#xff1a; IDEA本身编码与tomcat的编码与Windows编码不同导致&#xff0c;Windows 系统控制台…...

TDengine 快速体验(Docker 镜像方式)

简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能&#xff0c;本节首先介绍如何通过 Docker 快速体验 TDengine&#xff0c;然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker&#xff0c;请使用 安装包的方式快…...

【JavaEE】-- HTTP

1. HTTP是什么&#xff1f; HTTP&#xff08;全称为"超文本传输协议"&#xff09;是一种应用非常广泛的应用层协议&#xff0c;HTTP是基于TCP协议的一种应用层协议。 应用层协议&#xff1a;是计算机网络协议栈中最高层的协议&#xff0c;它定义了运行在不同主机上…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

五年级数学知识边界总结思考-下册

目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解&#xff1a;由来、作用与意义**一、知识点核心内容****二、知识点的由来&#xff1a;从生活实践到数学抽象****三、知识的作用&#xff1a;解决实际问题的工具****四、学习的意义&#xff1a;培养核心素养…...

vue3 字体颜色设置的多种方式

在Vue 3中设置字体颜色可以通过多种方式实现&#xff0c;这取决于你是想在组件内部直接设置&#xff0c;还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法&#xff1a; 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

Frozen-Flask :将 Flask 应用“冻结”为静态文件

Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是&#xff1a;将一个 Flask Web 应用生成成纯静态 HTML 文件&#xff0c;从而可以部署到静态网站托管服务上&#xff0c;如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...

P3 QT项目----记事本(3.8)

3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)

引言&#xff1a;为什么 Eureka 依然是存量系统的核心&#xff1f; 尽管 Nacos 等新注册中心崛起&#xff0c;但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制&#xff0c;是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

Robots.txt 文件

什么是robots.txt&#xff1f; robots.txt 是一个位于网站根目录下的文本文件&#xff08;如&#xff1a;https://example.com/robots.txt&#xff09;&#xff0c;它用于指导网络爬虫&#xff08;如搜索引擎的蜘蛛程序&#xff09;如何抓取该网站的内容。这个文件遵循 Robots…...