Vue源码学习 - 异步更新队列 和 nextTick原理

目录

  • 前言
  • 一、Vue异步更新队列
  • 二、nextTick 用法
  • 三、原理分析
  • 四、nextTick 源码解析
    • 1)环境判断
    • 2)nextTick()
  • 五、补充

前言

在我们使用Vue的过程中,基本大部分的 watcher 更新都需要经过 异步更新 的处理。而 nextTick 则是异步更新的核心。

官方对其的定义:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

一、Vue异步更新队列

Vue 可以做到 数据驱动视图更新,我们简单写一个案例实现下:

<template><h1 style="text-align:center" @click="handleCount">{{ value }}</h1>
</template><script>
export default {data () {return {value: 0}},methods: {handleCount () {for (let i = 0; i <= 10; i++) {this.value = iconsole.log(this.value)}}}
}
</script>

vue异步更新dom

当我们触发这个事件,视图中的 value 肯定会发生一些变化。
这里可以思考下,Vue是如何管理这个变化的过程呢?比如上面这个案例,value 被循环了10次,那 Vue 会去渲染dom视图10次吗?显然是不会的,毕竟这个性能代价太大了。其实我们只需要 value 最后一次的赋值。

实际上 Vue 是 异步更新 视图的,也就是说等 handleCount() 事件执行完,检查发现只需要更新 value,然后再一次性更新数据和Dom,避免无效更新。

总之,Vue 的 数据更新 和 DOM更新 都是异步的,Vue 会将数据变更添加到队列中,在下一个事件循环中进行批量更新,然后异步地将变更应用于实际的 DOM 元素,以保持视图与数据的同步。

Vue官方文档也印证了我们的想法,如下:

Vue 在更新 DOM 时是 异步 执行的。只要侦听到 数据变化 ,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

详细可见:Vue官方文档 - 异步更新队列

二、nextTick 用法

看例子,比如当 DOM 内容改变后,我们需要获取最新的元素高度。

<template><div>{{ name }}</div>
</template><script>
export default {data () {return {name: ''}},methods: {},mounted () {console.log(this.$el.clientHeight)this.name = '铁锤妹妹'console.log(this.name, 'name')console.log(this.$el.clientHeight)this.$nextTick(() => {console.log(this.$el.clientHeight)})}
}
</script>

在这里插入图片描述

从打印结果可以看出,name数据虽然更新了,但是前两次元素高度都是0,只有在 nextTick 中才能拿到更新后的 Dom 值,具体是什么原因呢?下面就分析下它的原理吧。

这个实例也可参考学习:watch监听和$nextTick结合使用处理数据渲染完成后的操作方法

三、原理分析

在执行 this.name = '铁锤妹妹' 的时候,就会触发 Watcher 更新,watcher 会把自己放入一个队列。

// src/core/observer/watcher.ts
update () {if (this.lazy) {// 如果是计算属性this.dirty = true} else if (this.sync) {// 如果要同步更新this.run()} else {// 将 Watcher 对象添加到调度器队列中,以便在适当的时机执行其更新操作。queueWatcher(this)}
}

用队列的原因是比如多个数据变更,直接更新视图多次的话,性能就会降低,所以对视图更新做一个异步更新的队列,避免不必要的计算和 DOM 操作。在下一轮事件循环的时候,刷新队列并执行已去重的工作(nextTick的回调函数),组件重新渲染,更新视图。

然后调用 nextTick() ,响应式派发更新的源码如下:

// src/core/observer/scheduler.tsexport function queueWatcher(watcher: Watcher) {// ...// 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用nextTick(flushSchedulerQueue)
}function flushSchedulerQueue () {queue.sort(sortCompareFn)for (index = 0; index < queue.length; index++) {watcher = queue[index]watcher.run()// ...省略细节代码}
}

这里参数 flushSchedulerQueue 方法就会被放入事件循环,主线程任务执行完之后就会执行这个函数,对 watcher 队列排序遍历、执行 watcher 对应的 run() 方法,然后render,更新视图。

也就是说 this.name = '铁锤妹妹' 的时候,任务队列简单理解成这样 [flushSchedulerQueue]

下一行 console.log(this.name, 'name') 检验下 name 数据是否更新。

然后下一行 console.log(this.$el.clientHeight) ,由于更新视图任务 flushSchedulerQueue 在任务队列中还没有执行,所以无法拿到更新后的视图。

然后执行 this.$nextTick(fn) 时候,添加一个异步任务,这时任务队列简单理解成这样 [flushSchedulerQueue, fn]

然后 同步任务 都执行完毕,接着按顺序执行任务队列中的 异步任务。第一个任务执行就会更新视图,后面自然能得到更新后的视图了。

四、nextTick 源码解析

1)环境判断

主要判断用哪个宏任务或者微任务,因为宏任务耗费时间大于微任务,所以优先使用 微任务,判断顺序如下:
Promise =》 MutationObserver =》 setImmediate =》 setTimeout

// src/core/util/next-tick.tsexport let isUsingMicroTask = false  // 是否启用微任务开关const callbacks: Array<Function> = [] //回调队列
let pending = false  // 异步控制开关,标记是否正在执行回调函数// 该方法负责执行队列中的全部回调
function flushCallbacks() {// 重置异步开关pending = false// 防止nextTick里有nextTick出现的问题// 所以执行之前先备份并清空回调队列const copies = callbacks.slice(0)callbacks.length = 0// 执行任务队列for (let i = 0; i < copies.length; i++) {copies[i]()}
}// timerFunc就是nextTick传进来的回调等... 细节不展开
let timerFunc
// 判断当前环境是否支持原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()timerFunc = () => {p.then(flushCallbacks)if (isIOS) setTimeout(noop)}isUsingMicroTask = true
} else if (!isIE &&typeof MutationObserver !== 'undefined' &&(isNative(MutationObserver) ||// PhantomJS and iOS 7.xMutationObserver.toString() === '[object MutationObserverConstructor]')
) {// 当原生 Promise 不可用时,timerFunc 使用原生 MutationObserver// MutationObserver不要在意它的功能,其实就是个可以达到微任务效果的备胎let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {// 使用 MutationObservercounter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {// 使用setImmediate,虽然也是宏任务,但是比setTimeout更好timerFunc = () => {setImmediate(flushCallbacks)}
} else {// 最后的倔强,timerFunc 使用 setTimeouttimerFunc = () => {setTimeout(flushCallbacks, 0)}
}

然后进入核心的 nextTick。

2)nextTick()

这里代码不多,主要逻辑就是:

  • 把传入的回调函数放进回调队列 callbacks
  • 执行保存的异步任务 timeFunc,就会遍历 callbacks 执行相应的回调函数了。
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {let _resolve// 把回调函数放入回调队列callbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e: any) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {// 如果异步开关是开的,就关上,表示正在执行回调函数,然后执行回调函数pending = truetimerFunc()}// 如果没有提供回调,并且支持 Promise,就返回一个 Promiseif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
}

可以看到最后有返回一个 Promise 是可以让我们在不传参的时候用的,如下

this.$nextTick().then(()=>{ ... })

五、补充

  • 在 vue 生命周期中,如果在 created() 钩子进行 DOM 操作,也一定要放在 nextTick() 的回调函数中。
  • 因为在 created() 钩子函数中,页面的 DOM未渲染,这时候也没办法操作 DOM,所以,此时如果想要操作 DOM,必须将操作的代码放在 nextTick() 的回调函数中

本文到此也就结束了,希望对大家有所帮助。

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

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

相关文章

MacOS本地安装Hadoop3

金翅大鹏盖世英&#xff0c;展翅金鹏盖世雄。 穿云燕子锡今鸽&#xff0c;踏雪无痕花云平。 ---------------- 本文密钥&#xff1a;338 ----------------- 本文描述了在macbook pro的macos上安装hadoop3的过程&#xff0c;也可以作为在任何类linux平台上安装hadoop3借鉴。 …

4、Linux驱动开发:设备-设备号设备号注册

目录 &#x1f345;点击这里查看所有博文 随着自己工作的进行&#xff0c;接触到的技术栈也越来越多。给我一个很直观的感受就是&#xff0c;某一项技术/经验在刚开始接触的时候都记得很清楚。往往过了几个月都会忘记的差不多了&#xff0c;只有经常会用到的东西才有可能真正记…

Verilog语法学习——LV2_异步复位的串联T触发器

LV2_异步复位的串联T触发器 题目来源于牛客网 [牛客网在线编程_Verilog篇_Verilog快速入门 (nowcoder.com)](https://www.nowcoder.com/exam/oj?page1&tabVerilog篇&topicId301) 题目 题目描述&#xff1a; 用verilog实现两个串联的异步复位的T触发器的逻辑&#x…

【LeetCode】141.环形链表

题目 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#…

opencv-22 图像几何变换01-缩放-cv2.resize()(图像增强,图像变形,图像拼接)

什么是几何变换&#xff1f; 几何变换是计算机图形学中的一种图像处理技术&#xff0c;用于对图像进行空间上的变换&#xff0c;而不改变图像的内容。这些变换可以通过对图像中的像素位置进行调整来实现。 常见的几何变换包括&#xff1a; 平移&#xff08;Translation&#x…

STM32MP157驱动开发——按键驱动(tasklet)

文章目录 “tasklet”机制&#xff1a;内核函数定义 tasklet使能/ 禁止 tasklet调度 tasklet删除 tasklet tasklet软中断方式的按键驱动程序(stm32mp157)tasklet使用方法&#xff1a;button_test.cgpio_key_drv.cMakefile修改设备树文件编译测试 “tasklet”机制&#xff1a; …

【Ansible】Ansible自动化运维工具之playbook剧本

playbook 一、playbook 的概述1. playbook 的概念2. playbook 的构成 二、playbook 的应用1. 安装 httpd 并启动2. 定义、引用变量3. 指定远程主机 sudo 切换用户4. when条件判断5. 迭代6. Templates 模块6.1 添加模板文件6.2 修改主机清单文件6.3 编写 playbook 7. tags 模块 …

vue权限按钮的实现

鉴权函数 由于下面几种方式都需要用到鉴权函数&#xff0c;所以将其放置在组件外面&#xff0c;供组件或其他文件调用。 // src/utils/hasPermission.jsimport { usePermissionStore } from /stores import array from lodash/array export const hasPermission (value, def…

EXCEL,如何比较2个表里的数据差异(使用数据透视表)

目录 1 问题: 需要比较如下2个表的内容差异 1.1 原始数据喝问题 1.2 提前总结 2 使用EXCEL公式方法 2.1 新增辅助列&#xff1a; 辅助index 2.2 具体公式 配合条件格式 使用 3 数据透视表方法 3.1 新增辅助列&#xff1a; 辅助index 3.2 需要先打开 数据透视表向导 …

Ubuntu 20.04 Ubuntu18.04安装录屏软件Kazam

1.在Ubuntu Software里面输入Kazam&#xff0c;就可以找不到这个软件&#xff0c;直接点击install就可以了 2.使用方法&#xff1a; 选择Screencast&#xff08;录屏&#xff09; Fullscreen&#xff08;全屏&#xff09;-----Windows&#xff08;窗口&#xff09;--------Ar…

20.3 HTML表格

1. table表格 table标签是HTML中用来创建表格的元素. table标签通常包含以下子标签: - th标签: 表示表格的表头单元格(table header), 用于描述列的标题. - tr标签: 表示表格的行(table row). - td标签: 表示表格的单元格(table data), 通常位于tr标签内, 用于放置单元格中的…

数据结构之动态顺序表(附带完整程序)

&#x1f388;基本概念 &#x1f308;一.线性表、顺序表的定义 ☀️&#xff08;1&#xff09;线性表&#xff1a; 是n个具有相同特性的数据元素的有限序列。线性表在逻辑上是线性结构&#xff0c;但在物理上存储时&#xff0c;通常以数组和链式结构的形式存储。 ☀️&…

c# 此程序集中已使用了资源标识符

严重性 代码 说明 项目 文件 行 禁止显示状态 错误 CS1508 此程序集中已使用了资源标识符“BMap.NET.WindowsForm.BMapControl.resources” BMap.NET.WindowsForm D:\MySource\Decompile\BMap.NET.WindowsForm\CSC 1 活动 运行程序时&a…

Mock-MOCO使用过程

一、jar包下载&#xff1a;https://github.com/dreamhead/moco 二、准备mock的json文件 data.json内容&#xff1a; ####GET请求 [{"description": "response使用Content-Type为charsetGBK编码格式来查看返回信息为中文的内容","request": {&q…

《Elasticsearch 源码解析与优化实战》第5章:选主流程

《Elasticsearch 源码解析与优化实战》第5章&#xff1a;选主流程 - 墨天轮 一、简介 Discovery 模块负责发现集群中的节点&#xff0c;以及选择主节点。ES 支持多种不同 Discovery 类型选择&#xff0c;内置的实现称为Zen Discovery ,其他的包括公有云平台亚马逊的EC2、谷歌…

Ansible单yaml文件部署Zabbix5.0监控平台

文章目录 Ansible单yaml文件部署Zabbix5.0监控平台节点规划案例实施基础环境准备编写剧本文件ZabbixWeb界面(1)改中文(2)添加监控主机 Ansible单yaml文件部署Zabbix5.0监控平台 节点规划 IP主机名节点192.168.200.10ansibleAnsible节点192.168.200.20zabbix-serverZabbix-ser…

深度学习入门(一):神经网络基础

一、深度学习概念 1、定义 通过训练多层网络结构对位置数据进行分类或回归&#xff0c;深度学习解决特征工程问题。 2、深度学习应用 图像处理语言识别自然语言处理 在移动端不太好&#xff0c;计算量太大了&#xff0c;速度可能会慢 eg.医学应用、自动上色 3、例子 使用…

Effective Java 案例分享(八)

39、使用注解而不是通过命名规则分类 如果需要对定义class&#xff0c;property&#xff0c;或者method进行分类管理&#xff0c;推荐的做法是使用注解对其添加类别&#xff0c;而不是通过命名规则分类。这里以JUnit为例&#xff1a; 在JUnit 3中&#xff0c;如果要写测试的方…

linux环境安装mysql数据库

一&#xff1a;查看是否自带mariadb数据库 命令&#xff1a;rpm -qa | grep mariadb 如果自带数据库则卸载掉重新安装 命令&#xff1a;yum remove mariadb-connector-c-3.1.11-2.el8_3.x86_64 二&#xff1a;将压缩文件上传到/user/local/mysql文件夹 或者直接下载 命令&a…

基于ssm+mysql+html道路养护管理系统

基于ssmmysqlhtml道路养护管理系统 一、系统介绍二、功能展示1.道路信息管理2.损害类型信息管理3.损害类型信息管理4.评定等级信息管理5.日常巡查信息管理6.定期检查信息管理 四、获取源码 一、系统介绍 系统主要功能&#xff1a;道路信息管理、损害类型信息管理、评定等级信息…