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 = 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.mfbz.cn/a/48181.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

视频剪辑矩阵分发系统Unable to load FFProbe报错技术处理?

问题一 报错处理 对于视频剪辑矩阵分发系统中出现的“Unable to load FFProbe”报错问题&#xff0c;可以采取以下技术处理措施进行解决。 1.检查系统中是否正确安装了FFProbe工具&#xff0c;并确保其路径正确配置。 2.检查系统环境变量是否正确设置&#xff0c;包括FFPr…

CSS鼠标样式(cursor)

CSS cursor 属性值 属性值示意图描述auto默认值&#xff0c;由浏览器根据当前上下文确定要显示的光标样式default 默认光标&#xff0c;不考虑上下文&#xff0c;通常是一个箭头none不显示光标initial将此属性设置为其默认值inherit从父元素基础 cursor 属性的值context-menu…

【深度解析】蓝牙室内定位方案优势介绍

万物互联时代&#xff0c;数据的价值进一步凸显&#xff0c;在海量数据中&#xff0c;位置数据成为万物互联产业中的基础坐标。室内空间结构越来越复杂&#xff0c;人们对位置的实时性和精确度要求不断提高&#xff0c;室内定位的需求也空前高涨。卫星信号对障碍物的穿透性较弱…

git使用教程

一 创建环境 参考 Git 安装配置 | 菜鸟教程 (runoob.com)https://www.runoob.com/git/git-install-setup.html 1.1 配置 $ git config --global user.name "runoob" $ git config --global user.email test@runoob.com 1.2 创建一个新文件夹 在新的文件夹执行(…

额外题目第1天|1365 941 1207 283 189 724 34 922 35 24

1365 暴力解法也能过 class Solution { public:vector<int> smallerNumbersThanCurrent(vector<int>& nums) {vector<int> result(nums.size(), 0);for (int i0; i<nums.size(); i) {int count 0;for (int j0; j<nums.size(); j) {if (nums[j]<…

无涯教程-jQuery - jQuery.getScript( url, callback )方法函数

jQuery.getScript(url&#xff0c;[callback])方法使用HTTP GET请求加载并执行JavaScript文件。 该方法返回XMLHttpRequest对象。 jQuery.getScript( url, [callback] ) - 语法 $.getScript( url, [callback] ) 这是此方法使用的所有参数的描述- url - 包含请求…

Educational Codeforces Round 152 (Rated for Div. 2) B. Monsters

很早想到%K排序,但是就是WA2,心态崩了,昨天晚上差点睡不着觉吐了,感觉自己好笨啊啊啊, 言归正传, 按照正常的思路,样例是可以过的,但是AC不了,例如给出样例3 3 3 1 2 经过自己模拟应该输出1 3 2 ,但是只会输出1 2 3 ,知道症结所在debug,0的先输出,之后输出k-1,k-2…但是怎么实现…

物联网的通信协议

物联网的通信协议 目录 物联网的通信协议一、UART串口通信1.1 串口通信1.2 异步收发1.3 波特率1.4 串口通信协议的数据帧1.5 优缺点1.5.1 优点1.5.2 缺点 二、I^2^C2.1 I^2^C2.2 I^2^C2.3 数据有效性2.4 起始条件S和停止条件P2.5 数据格式2.6 协议数据单元PDU2.7 优缺点2.7.1 优…

Python教程三:Python基本概念

1、Python基本语法 Python中严格区分大小写Python中每一行就是一条语句&#xff0c;每条语句以换行结束每一行语句不建议过长&#xff08;一般不建议超过80个字符&#xff09;一条语句可以多行编写&#xff0c;语句后加\结尾Python是缩进严格的语言&#xff0c;所以在Python中…

RNN架构解析——注意力机制

目录 注意力机制实现 注意力机制 实现

导出为PDF加封面且分页处理dom元素分割

文章目录 正常展示页面导出后效果代码 正常展示页面 导出后效果 代码 组件内 <template><div><div><div class"content" id"content" style"padding: 0px 20px"><div class"item"><divstyle"…

栈粉碎原理分析

栈粉碎原理分析 源代码如下 #include <stdio.h>void function(int a, int b) {char buffer[12];gets(buffer);//long* ret (long *) ((long)buffer28);//*ret *ret 7;return; }void main() {int x;x 0;function(1,2);x 1;printf("%d\n",x); } 由解注释前…

windows C++多线程同步<3>-互斥量

windows C多线程同步&#xff1c;3&#xff1e;-互斥量 概念&#xff0c;如下图&#xff1a; 另外就是互斥对象谁拥有&#xff0c;谁释放 那么一个线程允许多次获取互斥对象吗&#xff1f; 答案是允许&#xff0c;但是申请多次就要释放多次&#xff0c;否则其他线程获取不到互…

「分享」Word文档被锁定无法编辑怎么办?4种方法解决

有没有遇到这种情况&#xff1f;打开Word文档后&#xff0c;发现文档被锁定了&#xff0c;无法输入内容&#xff0c;也无法修改&#xff0c;这很大可能是Word文档被设置了“限制编辑”。 如果Word文档被设置了“限制编辑”&#xff0c;而我们又需要编辑文档&#xff0c;可以用…

等分切割图片的方法

在做数据集的过程中&#xff0c;有时候需要将大图进行切分成小图片&#xff0c;一方面是为了满足训练需要&#xff0c;一方面是为了扩增数据集。 如下图的尺寸为5472x3648,但是我用不着这么大的图片&#xff0c;需要将图9等分 市面上也有等分切割图片的软件或者网站&#xff…

tensorRT多batch动态推理

tensorRT的多batch推理&#xff0c;导出的onnx模型必须是动态batch&#xff0c;只需在导出的时候&#xff0c;设置一个dynamic_axis参数即可。 torch.onnx.export(hybrik_model, dummy_input, "./best_model.onnx", verboseTrue, input_namesinput_names, \output_…

19 QListWidget控件

Tips: 对于列表式数据可以使用QStringList进行左移一块输入。 代码&#xff1a; //listWidget使用 // QListWidgetItem * item new QListWidgetItem("锄禾日当午"); // QListWidgetItem * item2 new QListWidgetItem("汗滴禾下土"); // ui->…

树状数组1

五分钟丝滑动画讲解 | 树状数组_哔哩哔哩_bilibili (23条消息) 树状数组(详细分析应用)&#xff0c;看不懂打死我!_树形数组_鲜果维他命的博客-CSDN博客 注意&#xff1a; 1、树状数组一般的数组一般从下标1开始赋值0作为一个边界 &#xff0c;lowbit&#xff08;0&#…

apple pencil值不值得购买?便宜的电容笔推荐

如今&#xff0c;对ipad使用者而言&#xff0c;苹果原装的Pencil系列无疑是最佳的电容笔。只是不过这款电容笔的售价&#xff0c;实在是太高了&#xff0c;一般的用户都无法入手。因此&#xff0c;在具体的使用过程中&#xff0c;如何选用一种性能优良、价格低廉的电容笔是非常…

Python数据可视化工具——Pyecharts

目录 1 简介绘图前先导包 2 折线图3 饼图4 柱状图/条形图5 散点图6 箱线图7 热力图8 漏斗图9 3D柱状图10 其他&#xff1a;配置项 1 简介 Pyecharts是一款将python与echarts结合的强大的数据可视化工具 Pyecharts是一个用于生成echarts图表的类库。echarts是百度开源的一个数据…