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…

MATLAB算法实战应用案例精讲-【深度学习】预训练模型GPTXLNet

目录 GPT 1. 介绍 1.1 GPT的动机 2. 模型结构 3. GPT训练过程 3.1 无监督的预训练

【LeetCode】141.环形链表

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

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

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

IT行业面试攻略:技巧与心态的平衡

引言&#xff1a;在面试IT公司时&#xff0c;调整好心态是取得优秀表现的关键。面试心态直接影响着我们在面试中的自信程度和表现。面对这一挑战&#xff0c;我们需要学会积极自信、认识到紧张是正常的、进行充分准备以及以积极的心态去迎接面试。只有在拥有正确的心态下&#…

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…

Java 设计模式 - 单例模式 - 保证类只有一个实例

单例模式 - 保证类只有一个实例 为什么使用单例模式&#xff1f;单例模式的实现方式1. 饿汉式&#xff08;Eager Initialization&#xff09;2. 懒汉式&#xff08;Lazy Initialization&#xff09;3. 双重检查锁&#xff08;Double-Checked Locking&#xff09;4. 静态内部类&…

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

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

微信小程序开发6

一、分包-基础概念 1.1、什么是分包 分包指的是把一个完整的小程序项目&#xff0c;按照需求划分为不同的子包&#xff0c;在构建时打包成不同的分包&#xff0c;用户在使用时按需进行加载。 1.2、分包的好处 对小程序进行分包的好处主要有以下两点&#xff1a; 可以优化小程序…

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…

【NetCore】04-作用域与对象释放行为

文章目录 作用域 作用域由IServiceScope接口承载 对象释放 实现IDisposable接口类型释放 1.DI只负责释放由其创建的对象实例 2.DI在容器或子容器释放时&#xff0c;释放由其创建的对象实例 建议 1.避免在根容器获取实现IDisposable接口的瞬时服务 2.避免手动创建实现了IDispo…

uniAPP 浙政钉 入门手册

uniAPP 如何运行钉钉小程序&#xff1a; 运行钉钉小程序 调试工具导入项目 及 相关平台使用&#xff1a; 专有钉钉 浙政钉 前端 对接流程 常见调试工具&#xff0c;遇到的问题&#xff1a; 采坑记录 下载小程序 IDE 环境配置文件 专有钉钉–环境配置文件

网络安全 Day22-mariadb数据库用户管理

数据库用户管理 1. mariadb数据库用户管理2. mariadb数据库用户授权3. 用户回收授权4. 使客户端连接数据库 1. mariadb数据库用户管理 用户的格式: 用户主机范围 合起来才算一个用户授权主机范围 只能从本机访问: localhost或127.0.0.1或10.0.0.166(指定IP)授权整个网段: 授权…