前言

异步更新是 Vue 核心实现之一,在整体流程中充当着 watcher 更新的调度者这一角色。大部分 watcher 更新都会经过它的处理,在适当时机让更新有序的执行。而 nextTick 作为异步更新的核心,也是需要学习的重点。

本文你能学习到:

  • 异步更新的作用
  • nextTick原理
  • 异步更新流程

JS运行机制

在理解异步更新前,需要对JS运行机制有些了解,如果你已经知道这些知识,可以选择跳过这部分内容。

JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

“任务队列”中的任务(task)被分为两类,分别是宏任务(macro task)和微任务(micro task)

宏任务:在一次新的事件循环的过程中,遇到宏任务时,宏任务将被加入任务队列,但需要等到下一次事件循环才会执行。常见的宏任务有 setTimeout、setImmediate、requestAnimationFrame

微任务:当前事件循环的任务队列为空时,微任务队列中的任务就会被依次执行。在执行过程中,如果遇到微任务,微任务被加入到当前事件循环的微任务队列中。简单来说,只要有微任务就会继续执行,而不是放到下一个事件循环才执行。常见的微任务有 MutationObserver、Promise.then

总的来说,在事件循环中,微任务会先于宏任务执行。而在微任务执行完后会进入浏览器更新渲染阶段,所以在更新渲染前使用微任务会比宏任务快一些。

关于事件循环和浏览器渲染可以看下 晨曦时梦见兮 大佬的文章 《深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)》

为什么需要异步更新

既然异步更新是核心之一,首先要知道它的作用是什么,解决了什么问题。

先来看一个很常见的场景:

  1. created(){
  2. this.id = 10
  3. this.list = []
  4. this.info = {}
  5. }

总所周知,Vue 基于数据驱动视图,数据更改会触发 setter 函数,通知 watcher 进行更新。如果像上面的情况,是不是代表需要更新3次,而且在实际开发中的更新可不止那么少。更新过程是需要经过繁杂的操作,例如模板编译、dom diff,频繁进行更新的性能当然很差。

Vue 作为一个优秀的框架,当然不会那么“直男”,来多少就照单全收。Vue 内部实际是将 watcher 加入到一个 queue 数组中,最后再触发 queue 中所有 watcherrun 方法来更新。并且加入 queue 的过程中还会对 watcher 进行去重操作,因为在一个 vue 实例中 data 内定义的数据都是存储同一个 “渲染watcher”,所以以上场景中数据即使更新了3次,最终也只会执行一次更新页面的逻辑。

为了达到这种效果,Vue 使用异步更新,等待所有数据同步修改完成后,再去执行更新逻辑。

nextTick 原理

异步更新内部是最重要的就是 nextTick 方法,它负责将异步任务加入队列和执行异步任务。Vue 也将它暴露出来提供给用户使用。在数据修改完成后,立即获取相关DOM还没那么快更新,使用 nextTick 便可以解决这一问题。

认识 nextTick

官方文档对它的描述:

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

  1. // 修改数据
  2. vm.msg = 'Hello'
  3. // DOM 还没有更新
  4. Vue.nextTick(function () {
  5. // DOM 更新了
  6. })
  7. // 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
  8. Vue.nextTick()
  9. .then(function () {
  10. // DOM 更新了
  11. })

nextTick 使用方法有回调和Promise两种,以上是通过构造函数调用的形式,更常见的是在实例调用 this.$nextTick。它们都是同一个方法。

内部实现

Vue 源码 2.5+ 后,nextTick 的实现单独有一个 JS 文件来维护它,它的源码并不复杂,代码实现不过100行,稍微花点时间就能啃下来。源码位置在 src/core/util/next-tick.js,接下来我们来看一下它的实现,先从入口函数开始:

  1. export function nextTick (cb?: Function, ctx?: Object) {
  2. let _resolve
  3. // 1
  4. callbacks.push(() => {
  5. if (cb) {
  6. try {
  7. cb.call(ctx)
  8. } catch (e) {
  9. handleError(e, ctx, 'nextTick')
  10. }
  11. } else if (_resolve) {
  12. _resolve(ctx)
  13. }
  14. })
  15. // 2
  16. if (!pending) {
  17. pending = true
  18. timerFunc()
  19. }
  20. // $flow-disable-line
  21. // 3
  22. if (!cb && typeof Promise !== 'undefined') {
  23. return new Promise(resolve => {
  24. _resolve = resolve
  25. })
  26. }
  27. }
  1. cb 即传入的回调,它被 push 进一个 callbacks 数组,等待调用。
  2. pending 的作用就是一个锁,防止后续的 nextTick 重复执行 timerFunctimerFunc 内部创建会一个微任务或宏任务,等待所有的 nextTick 同步执行完成后,再去执行 callbacks 内的回调。
  3. 如果没有传入回调,用户可能使用的是 Promise 形式,返回一个 Promise_resolve 被调用时进入到 then

继续往下走看看 timerFunc 的实现:

  1. // Here we have async deferring wrappers using microtasks.
  2. // In 2.5 we used (macro) tasks (in combination with microtasks).
  3. // However, it has subtle problems when state is changed right before repaint
  4. // (e.g. #6813, out-in transitions).
  5. // Also, using (macro) tasks in event handler would cause some weird behaviors
  6. // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
  7. // So we now use microtasks everywhere, again.
  8. // A major drawback of this tradeoff is that there are some scenarios
  9. // where microtasks have too high a priority and fire in between supposedly
  10. // sequential events (e.g. #4521, #6690, which have workarounds)
  11. // or even between bubbling of the same event (#6566).
  12. let timerFunc
  13. // The nextTick behavior leverages the microtask queue, which can be accessed
  14. // via either native Promise.then or MutationObserver.
  15. // MutationObserver has wider support, however it is seriously bugged in
  16. // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  17. // completely stops working after triggering a few times... so, if native
  18. // Promise is available, we will use it:
  19. /* istanbul ignore next, $flow-disable-line */
  20. if (typeof Promise !== 'undefined' && isNative(Promise)) {
  21. const p = Promise.resolve()
  22. timerFunc = () => {
  23. p.then(flushCallbacks)
  24. // In problematic UIWebViews, Promise.then doesn't completely break, but
  25. // it can get stuck in a weird state where callbacks are pushed into the
  26. // microtask queue but the queue isn't being flushed, until the browser
  27. // needs to do some other work, e.g. handle a timer. Therefore we can
  28. // "force" the microtask queue to be flushed by adding an empty timer.
  29. if (isIOS) setTimeout(noop)
  30. }
  31. isUsingMicroTask = true
  32. } else if (!isIE && typeof MutationObserver !== 'undefined' && (
  33. isNative(MutationObserver) ||
  34. // PhantomJS and iOS 7.x
  35. MutationObserver.toString() === '[object MutationObserverConstructor]'
  36. )) {
  37. // Use MutationObserver where native Promise is not available,
  38. // e.g. PhantomJS, iOS7, Android 4.4
  39. // (#6466 MutationObserver is unreliable in IE11)
  40. let counter = 1
  41. const observer = new MutationObserver(flushCallbacks)
  42. const textNode = document.createTextNode(String(counter))
  43. observer.observe(textNode, {
  44. characterData: true
  45. })
  46. timerFunc = () => {
  47. counter = (counter + 1) % 2
  48. textNode.data = String(counter)
  49. }
  50. isUsingMicroTask = true
  51. } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  52. // Fallback to setImmediate.
  53. // Technically it leverages the (macro) task queue,
  54. // but it is still a better choice than setTimeout.
  55. timerFunc = () => {
  56. setImmediate(flushCallbacks)
  57. }
  58. } else {
  59. // Fallback to setTimeout.
  60. timerFunc = () => {
  61. setTimeout(flushCallbacks, 0)
  62. }
  63. }

上面的代码并不复杂,主要通过一些兼容判断来创建合适的 timerFunc,最优先肯定是微任务,其次再到宏任务。优先级为 promise.then > MutationObserver > setImmediate > setTimeout。(源码中的英文说明也很重要,它们能帮助我们理解设计的意义)

我们会发现无论哪种情况创建的 timerFunc,最终都会执行一个 flushCallbacks 的函数。

  1. const callbacks = []
  2. let pending = false
  3. function flushCallbacks () {
  4. pending = false
  5. const copies = callbacks.slice(0)
  6. callbacks.length = 0
  7. for (let i = 0; i < copies.length; i++) {
  8. copies[i]()
  9. }
  10. }

flushCallbacks 里做的事情 so easy,它负责执行 callbacks 里的回调。

好了,nextTick 的源码就那么多,现在已经知道它的实现,下面再结合异步更新流程,让我们对它更充分的理解吧。

异步更新流程

数据被改变时,触发 watcher.update

  1. // 源码位置:src/core/observer/watcher.js
  2. update () {
  3. /* istanbul ignore else */
  4. if (this.lazy) {
  5. this.dirty = true
  6. } else if (this.sync) {
  7. this.run()
  8. } else {
  9. queueWatcher(this) // this 为当前的实例 watcher
  10. }
  11. }

调用 queueWatcher,将 watcher 加入队列

  1. // 源码位置:src/core/observer/scheduler.js
  2. const queue = []
  3. let has = {}
  4. let waiting = false
  5. let flushing = false
  6. let index = 0
  7. export function queueWatcher (watcher: Watcher) {
  8. const id = watcher.id
  9. // 1
  10. if (has[id] == null) {
  11. has[id] = true
  12. // 2
  13. if (!flushing) {
  14. queue.push(watcher)
  15. } else {
  16. // if already flushing, splice the watcher based on its id
  17. // if already past its id, it will be run next immediately.
  18. let i = queue.length - 1
  19. while (i > index && queue[i].id > watcher.id) {
  20. i--
  21. }
  22. queue.splice(i + 1, 0, watcher)
  23. }
  24. // queue the flush
  25. // 3
  26. if (!waiting) {
  27. waiting = true
  28. nextTick(flushSchedulerQueue)
  29. }
  30. }
  31. }
  1. 每个 watcher 都有自己的 id,当 has 没有记录到对应的 watcher,即第一次进入逻辑,否则是重复的 watcher, 则不会进入。这一步就是实现 watcher 去重的点。
  2. watcher 加入到队列中,等待执行
  3. waiting 的作用是防止 nextTick 重复执行

flushSchedulerQueue 作为回调传入 nextTick 异步执行。

  1. function flushSchedulerQueue () {
  2. currentFlushTimestamp = getNow()
  3. flushing = true
  4. let watcher, id
  5. // Sort queue before flush.
  6. // This ensures that:
  7. // 1. Components are updated from parent to child. (because parent is always
  8. // created before the child)
  9. // 2. A component's user watchers are run before its render watcher (because
  10. // user watchers are created before the render watcher)
  11. // 3. If a component is destroyed during a parent component's watcher run,
  12. // its watchers can be skipped.
  13. queue.sort((a, b) => a.id - b.id)
  14. // do not cache length because more watchers might be pushed
  15. // as we run existing watchers
  16. for (index = 0; index < queue.length; index++) {
  17. watcher = queue[index]
  18. if (watcher.before) {
  19. watcher.before()
  20. }
  21. id = watcher.id
  22. has[id] = null
  23. watcher.run()
  24. }
  25. // keep copies of post queues before resetting state
  26. const activatedQueue = activatedChildren.slice()
  27. const updatedQueue = queue.slice()
  28. resetSchedulerState()
  29. // call component updated and activated hooks
  30. callActivatedHooks(activatedQueue)
  31. callUpdatedHooks(updatedQueue)
  32. }

flushSchedulerQueue 内将刚刚加入 queuewatcher 逐个 run 更新。resetSchedulerState 重置状态,等待下一轮的异步更新。

  1. function resetSchedulerState () {
  2. index = queue.length = activatedChildren.length = 0
  3. has = {}
  4. if (process.env.NODE_ENV !== 'production') {
  5. circular = {}
  6. }
  7. waiting = flushing = false
  8. }

要注意此时 flushSchedulerQueue 还未执行,它只是作为回调传入而已。因为用户可能也会调用 nextTick 方法。这种情况下,callbacks 里的内容为 ["flushSchedulerQueue", "用户的nextTick回调"],当所有同步任务执行完成,才开始执行 callbacks 里面的回调。

由此可见,最先执行的是页面更新的逻辑,其次再到用户的 nextTick 回调执行。这也是为什么我们能在 nextTick 中获取到更新后DOM的原因。

总结

异步更新机制使用微任务或宏任务,基于事件循环运行,在 Vue 中对性能起着至关重要的作用,它对重复冗余的 watcher 进行过滤。而 nextTick 根据不同的环境,使用优先级最高的异步任务。这样做的好处是等待所有的状态同步更新完毕后,再一次性渲染页面。用户创建的 nextTick 运行页面更新之后,因此能够获取更新后的DOM。

往期 Vue 源码相关文章:

Vue你不得不知道的异步更新机制和nextTick原理的更多相关文章

  1. Vue异步更新机制以及$nextTick原理

    相信很多人会好奇Vue内部的更新机制,或者平时工作中遇到的一些奇怪的问题需要使用$nextTick来解决,今天我们就来聊一聊Vue中的异步更新机制以及$nextTick原理 Vue的异步更新 可能你还 ...

  2. 使用AsyncTask异步更新UI界面及原理分析

    概述: AsyncTask是在Android SDK 1.5之后推出的一个方便编写后台线程与UI线程交互的辅助类.AsyncTask的内部实现是一个线程池,所有提交的异步任务都会在这个线程池中的工作线 ...

  3. 【转】从Vue.js源码看异步更新DOM策略及nextTick

    在使用vue.js的时候,有时候因为一些特定的业务场景,不得不去操作DOM,比如这样: <template> <div> <div ref="test" ...

  4. Vue初学者可能不知道的坑

    1.setTimeout/ setInterval 场景一 :this指向改变无法用this访问vue实例 mounted(){ setTimeout( function () { //setInte ...

  5. 一文读懂架构师都不知道的isinstance检查机制

    起步 通过内建方法 isinstance(object, classinfo) 可以判断一个对象是否是某个类的实例.但你是否想过关于鸭子协议的对象是如何进行判断的呢? 比如 list 类的父类是继 o ...

  6. Vue 源码解读(4)—— 异步更新

    前言 上一篇的 Vue 源码解读(3)-- 响应式原理 说到通过 Object.defineProperty 为对象的每个 key 设置 getter.setter,从而拦截对数据的访问和设置. 当对 ...

  7. Android异步消息处理机制(多线程)

    当我们需要执行一些耗时操作,比如说发起一条网络请求时,考虑到网速等其他原因,服务器未必会立刻响应我们的请求,如果不将这类操作放在子线程里去执行,就会导致主线程被阻塞住,从而影响用户对软件的正常使用. ...

  8. Android开发:图文分析 Handler通信机制 的工作原理

    前言 在Android开发的多线程应用场景中,Handler机制十分常用 下面,将图文详解 Handler机制 的工作原理 目录 1. 定义 一套 Android 消息传递机制 2. 作用 在多线程的 ...

  9. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

随机推荐

  1. Java中容易遗漏的小知识点( 一 )(为了和小白一样马上要考试的兄弟准备的,希望小白和大家高过不挂)

    笔者csdn博客同文地址:https://blog.csdn.net/weixin_45791445/article/details/106597515 我是小康小白,一个平平无奇的Java小白.热爱 ...

  2. Spark读取Hbase中的数据

    大家可能都知道很熟悉Spark的两种常见的数据读取方式(存放到RDD中):(1).调用parallelize函数直接从集合中获取数据,并存入RDD中:Java版本如下: JavaRDD<Inte ...

  3. 装cnpm

    npm install -g cnpm --registry=https://registry.npm.taobao.org 然后配置环境变量

  4. 通用!Python保存一个对象的方式

    参考资料: https://kite.com/python/answers/how-to-save-a-dictionary-to-a-file-in-python 通过如下的代码,可以将Python ...

  5. Ngnix 配置文件快速入门

    转自https://www.cnblogs.com/knowledgesea/p/5175711.html 其实也没什么好说的,我想大部分人也不会在意nginx的实现原理啥的.服务器要部署的时候,把n ...

  6. int与Integer的区别(基本类型与复杂类型的对比)转

    基本类型,或者叫做内置类型,是JAVA中不同于类的特殊类型. Java中的简单类型从概念上分为四种:实数.整数.字符.布尔值.但是有一点需要说明的是,Java里面只有八种原始类型,其列表如下: 实数: ...

  7. 想学好Python,你必须了解Python中的35个关键词

    每种编程语言都会有一些特殊的单词,称为关键词.对待关键词的基本要求是,你在命名的时候要避免与之重复.本文将介绍一下Python中的关键词.关键词不是内置函数或者内置对象类型,虽然在命名的时候同样也最好 ...

  8. .Net Core 中GC的工作原理

    前言 .NET 中GC管理你服务的内存分配和释放,GC是运行公共语言运行时(CLR Common Language Runtime)中,GC可以帮助开发人员有效的分配内存和和释放内存,大多数情况下是不 ...

  9. 如何在项目开发中应用好“Deadline 是第一生产力”?

    我想也许你早就听说过"Deadline是第一生产力"这句话,哪怕以前没听说过,我相信看完本文后,再也不会忘记这句话,甚至时不时还要感慨一句:"Deadline是第一生产力 ...

  10. 全网最深分析SpringBoot MVC自动配置失效的原因

    前言 本来没有计划这一篇文章的,只是在看完SpringBoot核心原理后,突然想到之前开发中遇到的MVC自动失效的问题,虽然网上有很多文章以及官方文档都说明了原因,但还是想亲自看一看,本以为很简单的事 ...