异步更新队列 - 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 会将自己加入到一个异步的更新队列中。然后更新队列会将传入的更新操作进行批量处理。
这样就达到了多次更新同时完成,提升了用户体验,减少了浏览器的开销,增强了性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/94715.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue的鼠标键盘事件

Vue的鼠标键盘事件 原生 鼠标事件(将v-on简写为) click // 点击 dblclick // 双击 mousedown // 按下 mousemove // 移动 mouseleave // 离开 mouseout // 移出 mouseenter // 进入 mouseover // 鼠标悬浮mousedown.left 键盘事件 keydown //键盘按下时触发 keypress …

C#工程建立后修改工程文件名与命名空间

使用之前的项目做二次开发&#xff0c;项目快结束的时候&#xff0c;需要把主项目的名称修改成我们想要的。 之前从来没有这么干过&#xff0c;记录一下。 步骤如下&#xff1a; 1&#xff1a;打开vs2010项目解决方案&#xff0c;重命名&#xff0c;如下图所示&#xff1a; …

微服务-Nacos(配置管理)

配置更改热更新 在Nacos中添加配置信息&#xff1a; 在弹出表单中填写配置信息&#xff1a; 配置获取的步骤如下&#xff1a; 1.引入Nacos的配置管理客户端依赖&#xff08;A、B服务&#xff09;&#xff1a; <!--nacos的配置管理依赖--><dependency><groupId&…

Git使用教程

一&#xff1a;Git是什么&#xff1f; Git是目前世界上最先进的分布式版本控制系统。文章下面有Git常用所有命令 二&#xff1a;SVN与Git的最主要的区别&#xff1f; SVN是集中式版本控制系统&#xff0c;版本库是集中放在中央服务器的&#xff0c;而干活的时候&#xff0c;用…

SQL Developer中的Data Redaction

SQL Developer中的Data Redaction用起来比命令行方便多了。可以选定表或视图&#xff0c;右键点击“遮盖保护”菜单。 但赋权方面有需要注意的地方。 假设Redact Admin是SYS&#xff0c;Redact User是HR。虽然SYS具备所有权限&#xff0c;但还是报以下错误。其实这个错误是针…

使用 PyTorch 进行高效图像分割:第 4 部分

一、说明 在这个由 4 部分组成的系列中&#xff0c;我们将使用 PyTorch 中的深度学习技术从头开始逐步实现图像分割。本部分将重点介绍如何实现基于视觉转换器的图像分割模型。 图 1&#xff1a;使用视觉转换器模型架构运行图像分割的结果。 从上到下&#xff0c;输入图像、地面…

Linux系列讲解 —— 【debugfs】交互式文件系统调试器

手册上说debugfs可以用于检查和更改ext2、ext3或ext4文件系统的状态。似乎很牛的样子&#xff0c;但是我并没有试验出来它多么强大的功能&#xff0c;无非就是在某些文件损坏导致无法删除的时候&#xff0c;我用debugfs来删除这些文件而已&#xff0c;如果有人知道它其他的妙用…

laravel-admin之 解决上传图片不显示 $form->image(‘image‘); 及 $grid->column(‘image‘);

参考 https://blog.csdn.net/u013164285/article/details/106017464 $grid->column(‘image’)->image(‘http://wuyan.cn’, 100, 100); // //设置服务器和宽高 图片上传的域名 上传的图片不显示 在 这里设置了图片的上传路径 在这里设置 域名 就可以回显图片

地理测绘基础知识(3)-观测与遮挡

在上一篇文章中&#xff0c;我们介绍了椭球模型下的一系列基础的坐标操作。本节&#xff0c;介绍观测与遮挡问题。 观测主要用于从观察点A观测大地标准点B&#xff0c;用来解决观测的仰角、方位角与大地坐标系之间的关系。 在没有GPS卫星的时代&#xff0c;为了测量一个位置的…

Unity小项目__小球吃零食

// Player脚本文件源代码 public class Player : MonoBehaviour {public Rigidbody rd; // 定义了一个刚体组件public int score 0; // 定义了一个计分器public Text scoreText; // 定义了一个文本组件public GameObject winText; // 定义了一个游戏物体用于检验游戏结束// S…

WPF显示初始界面--SplashScreen

WPF显示初始界面–SplashScreen 前言 WPF应用程序的运行速度快&#xff0c;但并不能在瞬间启动。当第一次启动应用程序时&#xff0c;会有一些延迟&#xff0c;因为公共语言运行时&#xff08;CLR&#xff09;首先需要初始化.NET环境&#xff0c;然后启动应用程序。 对于WPF中…

【数据结构与算法】队列

文章目录 一&#xff1a;队列1.1 队列的概念1.2 队列的介绍1.3 队列示意图 二&#xff1a;数组模拟队列2.1 介绍2.2 思路2.3 代码实现2.3.1 定义队列基本信息2.3.2 初始化队列2.3.3 判断队列是否满&#xff0c;是否为空2.3.4 添加数据到队列2.3.5 获取队列数据&#xff0c;出队…

Shell学习笔记之基础部分

Shell基础&#xff1a; 查看操作系统支持的shell&#xff1a; [rootrhel9 ansible]# cat /etc/shells /bin/sh /bin/bash /usr/bin/sh /usr/bin/bashShell的基本元素&#xff1a; 声明&#xff1a;声明用哪个命令解释器来解释并执行当前脚本文件中的语句&#xff0c;一般写的…

浅谈5G技术会给视频监控行业带来的一些变革情况

5G是第五代移动通信技术&#xff0c;能够提供更高的带宽和更快的传输速度&#xff0c;这将为视频技术的发展带来大量机会。随着5G技术的逐步普及与商用&#xff0c;人们将能够享受到更加流畅的高清视频体验&#xff0c;并且5G技术还拥有更低的延迟和更高的网络容量。这些优势不…

Vue中实现自动匹配搜索框内容 关键字高亮文字显示

实现效果如下: 1.首先需要给输入框进行双向绑定 2.拿到搜索的结果去渲染页面 将返回的结果和搜索的关键字进行比对 如果相同的 就变红 上代码 html部分 //输入框<div class"search"><div class"shuru"><input type"请输入要查询的…

论文阅读——Imperceptible Adversarial Attack via Invertible Neural Networks

Imperceptible Adversarial Attack via Invertible Neural Networks 作者&#xff1a;Zihan Chen, Ziyue Wang, Junjie Huang*, Wentao Zhao, Xiao Liu, Dejian Guan 解决的问题&#xff1a;虽然视觉不可感知性是对抗性示例的理想特性&#xff0c;但传统的对抗性攻击仍然会产…

机器学习:基本介绍

机器学习介绍 Hnad-crafted rules Hand-crafted rules&#xff0c;叫做人设定的规则。那假设今天要设计一个机器人&#xff0c;可以帮忙打开或关掉音乐&#xff0c;那做法可能是这样&#xff1a; 设立一条规则&#xff0c;就是写一段程序。如果输入的句子里面看到**“turn of…

Android开发之性能优化:过渡绘制解决方案

1. 过渡绘制 屏幕上某一像素点在一帧中被重复绘制多次&#xff0c;就是过渡绘制。 下图中多个卡片跌在一起&#xff0c;但是只有第一个卡片是完全可见的。背后的卡片只有部分可见。但是Android系统在绘制时会将下层的卡片进行绘制&#xff0c;接着再将上层的卡片进行绘制。但其…

Vue2-配置脚手架、分析脚手架、render函数、ref属性、props配置项、mixin配置项、scoped样式、插件

&#x1f954;:总有一段付出了没有回报的日子 是在扎根 更多Vue知识请点击——Vue.js VUE2-Day6 配置脚手架脚手架结构render函数vue.js与vue.runtime.xxx.js的区别引入render函数为什么要引入残缺的vue呢&#xff1f; 脚手架默认配置ref属性props配置项传递数据接收数据注意点…

如何利用 EMC 模型解决能源服务提供商的瓶颈

01. 什么是合同能源管理&#xff1f; 合同能源管理(EMC-Energy Management Contract) 是一种新型的市场化节能机制,其实质就是以减少的能源费用来支付节能项目全部成本的节能投资方式。&#xff1a;节能服务公司与用能单位以契约形式约定节能项目的节能目标&#xff0c;节能服…