文章首发于:github.com/USTB-musion…javascript
前段时间在写项目时对nextTick的使用有一些疑惑。在查阅各类资料以后,在这里总结一下Vue.js异步更新的策略以及nextTick的用途和原理。若有总结错误的地方,欢迎指出!html
本文将从如下3点进行总结:java
<template>
<div>
<div ref="message">{{message}}</div>
<button @click="handleClick">点击</button>
</div>
</template>
复制代码
export default {
data () {
return {
message: 'begin'
};
},
methods () {
handleClick () {
this.message = 'end';
console.log(this.$refs.message.innerText); //打印“begin”
}
}
}
复制代码
打印出来的结果是“begin”,咱们在点击事件里明明将message赋值为“end”,而获取真实DOM节点的innerHTML却没有获得预期中的“begin”,为何?ios
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
复制代码
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 10000; i++) {
this.number++;
}
}
}
}
复制代码
在点击click事件以后,number会被遍历增长10000次。在Vue.js响应式系统中,能够看一下个人前一篇文章Vue.js的响应式系统原理。咱们知道Vue.js会经历“setter->Dep->Watcher->patch->视图”这几个流程。。git
根据以往的理解,每次number被+1的时候,都会触发number的setter按照上边的流程最后来修改真实的DOM,而后DOM被更新了10000次,想一想都刺激!看一下官网的描述:Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据改变。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做上很是重要显然。github
为了方便理解Vue.js异步更新策略和nextTick,先介绍如下JS的运行机制,参考阮一峰老师的JavaScript 运行机制详解:再谈Event Loop。摘取的关键部分以下:JS是单线程的,意思就是同一时间只能作一件事情。它是基于事件轮询的,具体能够分为如下几个步骤:数组
(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。app
(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。异步
(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。async
(4)主线程不断重复上面的第三步。
Vue.js在修改数据的时候,不会立马修改数据,而是要等同一事件轮询的数据都更新完以后,再统一进行视图更新。 知乎上的例子:
//改变数据
vm.message = 'changed'
//想要当即使用更新后的DOM。这样不行,由于设置message后DOM尚未更新
console.log(vm.$el.textContent) // 并不会获得'changed'
//这样能够,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
console.log(vm.$el.textContent) //能够获得'changed'
})
复制代码
图解:
nextTick在官网当中的定义:
在下次 DOM 更新循环结束以后执行延迟回调。在修改数据以后当即使用这个方法,获取更新后的 DOM。
如下用setTimeout来模拟nextTick,先定义一个callbacks来存储nextTick,在下一个tick处理回调函数以前,全部的cb都会存储到这个callbacks数组当中。pending是一个标记位,表明等待的状态。接着setTimeout 会在 task 中建立一个事件 flushCallbacks ,flushCallbacks 则会在执行时将 callbacks 中的全部 cb 依次执行。
// 存储nextTick
let callbacks = [];
let pending = false;
function nextTick (cb) {
callbacks.push(cb);
if (!pending) {
// 表明等待状态的标志位
pending = true;
setTimeout(flushCallbacks, 0);
}
}
function flushCallbacks () {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
复制代码
真实的代码比这儿复杂的多,在Vue.js源码当中,nextTick定义在一个单独的文件中来维护,在src/core/util/next-tick.js中:
/* @flow */
/* globals MessageChannel */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
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]()
}
}
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
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)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
加上注释以后:
/** * Defer a task to execute it asynchronously. */
/* 延迟一个任务使其异步执行,在下一个tick时执行,一个当即执行函数,返回一个function 这个函数的做用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完之后以此执行直到执行到timerFunc 目的是延迟到当前调用栈执行完之后执行 */
export const nextTick = (function () {
/*存放异步执行的回调*/
const callbacks = []
/*一个标记位,若是已经有timerFunc被推送到任务队列中去则不须要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc
/*下一个tick时的回调*/
function nextTickHandler () {
/*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不须要在push多个回调到callbacks时将timerFunc屡次推入任务队列或者主线程*/
pending = false
/*执行全部callback*/
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 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 if */
/* 这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试获得timerFunc的方法 优先使用Promise,在Promise不存在的状况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,因此优先使用。 若是上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。 参考:https://www.zhihu.com/question/55364497 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
/*使用Promise*/
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
// 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)
}
} else if (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 IE11, iOS7, Android 4.4
/*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
// fallback to setTimeout
/* istanbul ignore next */
/*使用setTimeout将回调推入任务队列尾部*/
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
/* 推送到队列中下一个tick时执行 cb 回调函数 ctx 上下文 */
return function queueNextTick (cb?: Function, ctx?: Object) {
let _resolve
/*cb存到callbacks中*/
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
复制代码
关键在于timeFunc(),该函数起到延迟执行的做用。 从上面的介绍,能够得知timeFunc()一共有三种实现方式。
nextTick的用途
应用场景:须要在视图更新以后,基于新的视图进行操做。
看一个例子: 点击show按钮使得原来v-show:false的input输入框显示,并获取焦点:
<div id="app">
<input ref="input" v-show="inputShow">
<button @click="show">show</button>
</div>
复制代码
new Vue({
el: "#app",
data() {
return {
inputShow: false
}
},
methods: {
show() {
this.inputShow = true
this.$nextTick(() => {
this.$refs.input.focus()
})
}
}
})
复制代码