上篇文章vue生命周期中我们说过一个句话,那就是mounted中并不会保证所有子组件都被挂载完成后再触发,因此当你希望视图完全渲染完成后再做某些事情时,请在mounted中使用$nextTick。那么$nextTick到底是干嘛用的,为什么能解决我们以上的问题。下面我们来好好了解了解$nextTick。但在此之前,如果不懂js事件循环的人,请先去看下什么是js事件循环,因为要理解$nextTick,必须得先理解js事件循环
下面我们先简单得回顾下什么是事件循环(具体理解请看这 js事件循环)
js处理异步主要有微任务(microTask)和 宏任务 (macroTask),而从开始执行一个宏任务–>执行完这个宏任务中所有同步代码—>清空当前微任务队列中所有微任务—> UI渲染 。 这便是完成了一个事件循环(Tick), 然后开始执行下一个宏任务(相当于下一轮循环)。
vue实现dom更新是异步完成的,我们可以从下面这个例子中就能看的出
<body>
<div id="app">
<p ref="dom">{
{
message}}</p>
<button @click="changeValue">改变值</button>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
changeValue () {
this.message = 'hello zhangShan'
console.log(this.$refs.dom.innerText)
}
}
})
</script>
输出值为
hello world
从上图中,我们可以看出,我们改变了message后,立马去输出p标签的text值,发现还是原来的值。这就很明显了,vue的dom更新,并不是同步的。而是异步的,所以在输出时,实际dom还并没有更新。
那么,为什么要设计成异步的,其实很好理解,如果是同步的,当我们频繁的去改变状态值时,是不是会频繁的导致我们的dom更新啊。这很显然是不行的。
<body>
<div id="app">
<p ref="dom">{
{
message}}</p>
<button @click="changeValue">改变值</button>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
changeValue () {
this.message = 'hello zhangShan'
this.message = 'hello liShi'
this.message = 'hello wangWu'
this.message = 'hello chenLiu'
console.log(this.$refs.dom.innerText)
}
}
})
</script>
像上图这样,如果vue同步更新的话,将会造成4次dom更新。故vue是异步dom更新的,且更新原理如下(借用官网的话):
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
这句话大部分地方其实都很好理解,我也不做过多的说明,我只说明下这句话中(在下一个的事件循环"tick"中,vue刷新队列并执行实际工作),按理的理解,这个下一个事件循环"tick"其实是个泛指,他并不是指下一个事件循环,才去刷新队列。实际刷新队列是有可能在本次事件循环的微任务中刷新的,也可能是在下一个事件循环中刷新的。这取决于代码当前执行的环境,如若当前执行环境支持promise,那么nextTick内部实际会用Promise去执行,那么队列刷新就会在本次事件循环的微任务中去执行。
也就是说,如果当前环境支持promise,那么nextTick内部会使用promise.then去执行,否则,如果支持mutationObserver,那么会用mutationObserver(什么是mutationObserver),不过mutationObserver在vue2.5以后被弃用了。如果这两种都不支持,才会使用setImmediate,MessageChannel(vue2.5以后才有),或者setTimeout(按顺序,支持哪个优先用哪个)。
这也就是vue的降级策略
优先选择微任务microtask(promise和mutationObserver),不支持的情况下,才不得不降级选用宏任务macrotask(setImmediate, MessageChannel, setTimeout)。
那么,为什么优先选择微任务呢
详情请看 js事件循环。看完后就会明白,在微任务中更新队列是会比在宏任务中更新少一次UI渲染的。
下面我们来证实下我们的猜想,请看下面一段代码
<body>
<div id="app">
<p ref="dom">{
{
message}}</p>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
message: 'hello world'
},
mounted() {
// 第一步
this.message = 'aaa'
// 第二步
setTimeout(() => {
console.log('222')
})
// 第三步
Promise.resolve().then((res) => {
console.log('333')
})
// 第四步
this.$nextTick(() => {
console.log('444')
console.log(this.$refs.dom)
})
// 第五步
Promise.resolve().then((res) => {
console.log('555')
})
}
})
</script>
<body>
<div id="app">
<p ref="dom">{
{
message}}</p>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
message: 'hello world'
},
mounted() {
// 第二步
setTimeout(() => {
console.log('222')
})
// 第三步
Promise.resolve().then((res) => {
console.log('333')
})
// 第一步
this.message = 'aaa'
// 第四步
this.$nextTick(() => {
console.log('444')
console.log(this.$refs.dom)
})
// 第五步
Promise.resolve().then((res) => {
console.log('555')
})
}
})
</script>
看上面代码,这次我们数据更新放到第三步和第四步直接去执行,再看输出结果
大家发现没有,这个时候,第三步的微任务是优先执行了的。是不是说明了,nextTick中的callback啥时候执行,取决于数据是在什么时候发生了改变的啊。那么为什么会这样呢。这我们就要从nextTick源码来看看到底是怎么回事了。我们先来看看源码
下面的源码借鉴于nextTick源码实现
首先,我们知道(响应式原理请自行查看MVVM响应式原理或者vue源码解析),当响应式数据发生变化时,是不是会触发它的setter,从而通知Dep去调用相关的watch对象,从而触发watch的update函数进行视图更新。那我们先看看update函数做了啥
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步则执行run直接渲染视图*/
this.run()
} else {
/*异步推送到观察者队列中,下一个tick时调用。*/
queueWatcher(this)
}
}
update中是不是调用了一个queueWatcher方法啊(我们先将update的调用称作第一步,将queueWatcher函数的调用称作第二步,后面用的上),我们再看这个方法做了什么
/*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
/*获取watcher的id*/
const id = watcher.id
/*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
if (has[id] == null) {
has[id] = true
if (!flushing) {
/*如果没有flush掉,直接push到队列中即可*/
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 >= 0 && queue[i].id > watcher.id) {
i--
}
queue.splice(Math.max(i, index) + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
可以看出,queueWatcher方法内部主要做的就是将watcher push到了queue队列当中。
同时当waiting为false时,调用了一次 nextTick方法, 同时传入了一个参数 flushSchedulerQueue,其实这个参数,就是具体的队列更新函数,也就是说更新dom操作就是在这里面做的。而这个waiting状态的作用,很明显是为了保证nextTick(flushSchedulerQueue)只会执行一次。后续再通过this.xxx改变数据,只会加入将相关的watcher加入到队列中,而不会再次执行nextTick(flushSchedulerQueue)。
现在我们将nextTick(flushSchedulerQueue) 称作第三步
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()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${
watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
我们再来看看nextTick内部,做了些啥
/**
1. 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
})
}
}
})()
在这个函数内,我们可以看到
这个时候,我们继续看queueNextTick,这里做了什么啊
大家发现没有,这个pending其实就是解开我们问题的关键啊,为什么这么说呢。我们先看timerFunc内做了啥,再回过头来解释
那么timerFunc做啥了
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)
}
}
可以看出,timerFunc内部定义了一些异步函数,视当前执行环境的不同,timerFunc内部执行的异步函数不同,他内部可能是promise, 可能是mutationObserver, 可能是setTimeout。(我们当前例子是在chrome浏览器下,timerFunc内部是Promise无疑)。但可以看出,不管内部是什么异步函数,它都在异步的回调中执行了nextTickHandler,而nextTickHandler是决定我们调用this.$nextTick(() => {})时,内部回调函数啥时候执行的关键。故可以得出结论,timerFunc内部的异步函数的回调啥时候执行,我们this.$nextTick()内的回调就啥时候执行
好,到了这一步,我们就可以来重新梳理下,代码是怎么走的啦。
mounted() {
// 第一步
this.message = 'aaa'
// 第二步
setTimeout(() => {
console.log('222')
})
// 第三步
Promise.resolve().then((res) => {
console.log('333')
})
// 第四步
this.$nextTick(() => {
console.log('444')
console.log(this.$refs.dom)
})
// 第五步
Promise.resolve().then((res) => {
console.log('555')
})
}
mounted() {
// 第一步
this.message = 'aaa'
// 第二步
setTimeout(() => {
console.log('222')
})
// 第三步
Promise.resolve().then((res) => {
console.log('333')
})
// 第四步
this.$nextTick(() => {
console.log('444')
console.log(this.$refs.dom)
})
// 第五步
Promise.resolve().then((res) => {
console.log('555')
})
}
这也就是所有的一个执行过程了,讲的应该都很明了了。
整体逻辑可能会有点绕,但是认真看,相信你一定能够看的懂得。看不懂,请私信我,或者直接评论,我会一一解答得。