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

news2024/10/7 14:32:58

目录

  • 前言
  • 一、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 = i
        console.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.ts

export 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.ts

export 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.x
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // 当原生 Promise 不可用时,timerFunc 使用原生 MutationObserver
  // MutationObserver不要在意它的功能,其实就是个可以达到微任务效果的备胎
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
  // 使用 MutationObserver
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最后的倔强,timerFunc 使用 setTimeout
  timerFunc = () => {
    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 = true
    timerFunc()
  }
  // 如果没有提供回调,并且支持 Promise,就返回一个 Promise
  if (!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.coloradmin.cn/o/800220.html

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

相关文章

【能量管理系统( EMS )】基于粒子群算法对光伏、蓄电池等分布式能源DG进行规模优化调度研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

基于量子同态加密的安全多方凸包协议

摘要安全多方计算几何(SMCG)是安全多方计算的一个分支。该协议是为SMCG中安全的多方凸包计算而设计的。首先&#xff0c;提出了一种基于量子同态加密的安全双方值比较协议。由于量子同态加密的性质&#xff0c;该协议可以很好地保护量子电路执行过程中数据的安全性和各方之间的…

分享一套功能齐全的免费开源MES系统

万界星空科技的开源MES功能&#xff1a; 1、基础数据管理&#xff1a; 2、质量管理&#xff1a; 质检项目维护&#xff0c;根据物料或者型号管理质检项目。检验页面&#xff0c;抽检确认。 3、工艺文件管理 &#xff1a;工艺参数&#xff0c;BOM文件&#xff0c;导入导出 报表&…

【树莓派入门】

一、镜像烧录 烧录器&#xff1a;Raspberry Pi Imager 下载链接&#xff1a;树莓派镜像烧录器下载 创建 ssh 文件 手动创建一个空白记事本.txt文件&#xff0c;命名为ssh&#xff0c;重命名&#xff0c;删掉.txt扩展名。将这个文件放入SD卡的boot盘中 wpa_supplicant.conf …

电流源电路

3.3.3电流源电路 镜像电流源 电路 分析 仿真 比例电流源 电路 分析 仿真 加射极输出器的电流源1 电路 分析 仿真 加射极输出器的电流源2 电路 分析 仿真 威尔逊电流源 电路 分析 仿真

Docker 全栈体系(八)

Docker 体系&#xff08;高级篇&#xff09; 六、Docker轻量级可视化工具Portainer 1. 是什么 Portainer 是一款轻量级的应用&#xff0c;它提供了图形化界面&#xff0c;用于方便地管理Docker环境&#xff0c;包括单机环境和集群环境。 2. 安装 官网 https://www.portain…

QTday3消息弹框/计时器

闹钟小软件 widget.cpp #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QDebug> #include <QPushButton> #include <QLabel> #include <QTimer> #include <QTimerEvent> #include <QTime> #include <QMessageB…

版本适配好帮手 Android SDK Upgrade Assistant / Android Studio Giraffe新功能

首先是新版本一顿下载↓&#xff1a; Download Android Studio & App Tools - Android Developers 在Tools中找到Android SDK Upgrade Assistant 可以在此直接查看SDK升级相关信息&#xff0c;不用跑到WEB端去查看了。 例如看一下之前经常要对老项目维护的android 12蓝牙…

umy-ui树形结构表格懒加载用法详解

效果图 在做后台时&#xff0c;使用的iview组件库中的树形表格&#xff0c;但数据量过大时会导致页面卡死&#xff0c;借助umy-ui的虚拟表格完美解决了数据量大卡顿的问题。 先放文档&#xff1a;http://www.umyui.com/umycomponent/u-table-column-api 安装 npm install u…

Ubuntu Server版 之 mysql 系列

Ubuntu 分 桌面版 和 服务版 桌面版 &#xff1a;有额外的简易界面 服务版&#xff1a;是纯黑框的。没有任何UI界面的可言 安装mysql 安装位置 一般按照的位置存放在 /usr/bin 中 sudo apt-get install mysql-server查看mysql的状态 service mysql status mysql 安全设置…

对原型、原型链的理解

在 JavaScript 中是使用构造两数来新建一个对象的&#xff0c;每一个构造函数的内部都有一个 prototype 属性&#xff0c;它的属性值是一个对象&#xff0c;这个对象包含了可以由该构造西数的所有实例共享的属性和方法。当使用构造函数新建一个对象后&#xff0c;在这个对象的内…

js加载和长任务

js加载和长任务 本文将讲解以下浏览器如何加载js&#xff0c;并介绍一些可以提高网页加载速度的方法。 Evaluate Script 如果我们在devtools的performance中分析过网站的加载性能&#xff0c;可能会看到一个很长的任务&#xff0c;叫做Evaluate Script. 在这种情况下&#x…

IDS(Intrusion Detection Systems)

计算机安全的三大中心目标是&#xff1a;保密性(Conf idential ity)、完整性(Integrity)、可用性(Availability)。 身份认证与识别、访问控制机制、加密技术、防火墙技术等技术共同特征就是集中在系统的自身加固和 防护上&#xff0c;属于静态的安全防御技术&#xff0c;缺乏主…

【微服务架构设计】微服务不是魔术:处理超时

微服务很重要。它们可以为我们的架构和团队带来一些相当大的胜利&#xff0c;但微服务也有很多成本。随着微服务、无服务器和其他分布式系统架构在行业中变得更加普遍&#xff0c;我们将它们的问题和解决它们的策略内化是至关重要的。在本文中&#xff0c;我们将研究网络边界可…

上门居家养老小程序社区养老小程序开发方案详解

居家养老管理社区养老小程序有哪些功能呢&#xff1f; 1.选择养老服务类型 医疗护理&#xff0c;家政服务预约&#xff0c;上门助浴、上门做饭&#xff0c;上门助餐&#xff0c;生活照护&#xff0c;康复理疗、精神慰藉、委托代办等。各项服务的详情介绍。 2.选择预约时间 选择…

Linux_NVR_SDK 编译应用 -基于iTOP-RK3568开发板

在源码 build/app 目录下有发布版本的 RKMPI 以及编译配置&#xff0c;目前的编译方式只支持 Cmake 脚本。如果要编译其他应用&#xff0c;可以参考 build/app/build/build.sh 脚本配置编译工具链。 以编译 RKMPI 应用程序为例&#xff0c;进行示范&#xff1a; 1. 使能 sdk…

BIM与GIS融合:公路设计施工信息化的创新解决方案

随着信息技术的不断发展和应用&#xff0c;建筑行业也迎来了数字化转型。BIM&#xff08;建筑信息模型&#xff09;和GIS&#xff08;地理信息系统&#xff09;作为建筑行业中的重要技术&#xff0c;它们的融合对于实现公路施工信息化具有重要意义。Bim技术可以提供精细的建筑信…

(无人机方向)ros小白之键盘控制无人机(终端方式)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一&#xff1a;配置pycharm的ros开发环境二&#xff1a;核心代码讲解三 效果演示XTDrone 四 完整代码 前言 ubuntu 18.04 pycharm ros melodic 做一个在终端中…

Android 截图功能实现

Android 截图功能实现 简介效果图功能实现1. 截取当前可见范围屏幕2. 截取当前可见范围屏幕&#xff08;不包含状态栏&#xff09;3. 截取某个控件4. 截取ScrollView5. 长截图6. 截屏动画效果7. 显示截屏结果&#xff0c;自动消失6. 完整代码 简介 在Android应用中开发截图功能…

Kibana+Prometheus+node_exporter 监控告警部署

下载好三个软件包 一、prometheus安装部署 1、解压 linxxubuntu:~/module$ tar -xvf prometheus-2.45.0-rc.0.linux-amd64.tar.gz 2、修改配置文件的IP地址 # my global config global:scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is ever…