前言

异步更新是 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 和浏览器渲染、帧动画、空闲回调(动图演示)》

为什么需要异步更新

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

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

created(){
this.id = 10
this.list = []
this.info = {}
}

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

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

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

nextTick 原理

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

认识 nextTick

官方文档对它的描述:

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

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

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

内部实现

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

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

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

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc // The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

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

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

const callbacks = []
let pending = false function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

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

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

异步更新流程

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

// 源码位置:src/core/observer/watcher.js
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) // this 为当前的实例 watcher
}
}

调用 queueWatcher,将 watcher 加入队列

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

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

function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id // Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
} // keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice() resetSchedulerState() // call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
}

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

function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}

要注意此时 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. MQ系列(0)——MQ简介

    mq简介 mq 就是消息队列(Message Queue).想必大家对队列的数据结构已经很熟悉了,消息队列可以简单理解为:把要传输的数据放在队列中,mq 就是存放和发送消息的这么一个队列中间件.在消息 ...

  2. 2019-02-02 Python学习——生成器杨辉三角,迭代器与可迭代对象的区别

    练习 杨辉三角定义如下: 1 / \ 1 1 / \ / \ 1 2 1 / \ / \ / \ 1 3 3 1 / \ / \ / \ / \ 1 4 6 4 1 / \ / \ / \ / \ / ...

  3. 龙芯团队完成CoreCLR MIPS64移植,在github开源

    国产龙芯的软件生态之中.NET不会缺席,毕竟 C# 与 .NetCore/Mono 也是全球几大主流的编程语言和运行平台之一,最近一段时间听到太多的鼓吹政务领域不支持.NET, 大家都明白这是某些人为 ...

  4. 9、ssh的集成方式1

    集成方式1:核心 我们没有创建applicationContext-action.xml配置文件,在该配置文件里面让Spring去管理我们的AddUserAction,但是AddUserAction的 ...

  5. struct2面试准备

    二 工作流程1.客户端浏览器发出HTTP请求.2.根据web.xml配置,该请求被FilterDispatcher接收3.根据struts.xml配置,找到需要调用的Action类和方法, 并通过Io ...

  6. Python实用笔记 (24)面向对象高级编程——使用@property

    这显然不合逻辑.为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数: clas ...

  7. Nginx配置upstream并且实现负载均衡

    感谢看过这一些列博文和评论的小伙伴, 我把自己所看到的学到的拿到这里来分享是想和大家一起学习进步, 想听听园友给出的意见, 也是对自己学习过程的一个总结. 技术无止境, 我们仍需努力! 1,话不多说, ...

  8. 痞子衡嵌入式:kFlashFile v1.0 - 一个基于Flash的掉电数据存取方案

    大家好,我是痞子衡,是正经搞技术的痞子.今天给大家带来的是痞子衡的个人小项目 - kFlashFile. 痞子衡最近在参与一个基于 i.MXRT1170 的项目,项目有个需求,需要在 Flash 里实 ...

  9. springboot 整合retry(重试机制)

    当我们调用一个接口可能由于网络等原因造成第一次失败,再去尝试就成功了,这就是重试机制,spring支持重试机制,并且在Spring Cloud中可以与Hystaix结合使用,可以避免访问到已经不正常的 ...

  10. Java 从入门到进阶之路(二十八)

    在之前的文章我们都是通过 Java 在内存中应用,本章开始我们来看一下 Java 在系统文件(硬盘)上的操作. 系统文件就是我们电脑中的文件,简单来说就是像 Windows 系统中 C D E 等各类 ...