本文主要讲述vue的异步更新队列(nextTick) 想要了解nextTick,让我们先来简单了解一下JS 运行机制(Event Loop)
JS 运行机制(Event Loop)
先来一张眼熟的事件循环的经典图
Javascript 只有一个 main thread 主线程和 call-stack调用栈(执行栈),js解析⽅法时,将其中的同步任务按照执⾏顺序排队到调用栈中,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。所有的异步任务放到浏览器所支持的Web Apis中处理,异步任务返回结果之后,将异步事件按照顺序排队到任务队列中等待主线程空闲的时候(前提是调用栈被清空),被读取到栈内等待主线程的执行。
事件循环大致分为以下几个步骤:
(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在一个任务队列。异步任务经过Web Apis处理有了结果,就依次排列到任务队列等待执行。
(3)一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,任务队列结束等待,进入执行栈,开始执行。
(4)主线程不断重复上面的第3步。
异步任务分为宏任务和微任务
本文只列出接下来在nextTick源码中会用到的异步任务
- 宏任务(macro task):setTimeout,setImmediate
- 微任务(micro task):Promise.then,MutationObsever
异步任务执行的顺序
浏览器环境中执行方法时,先将执行栈中的任务清空,再将排列在任务队列的微任务推到执行栈中并清空,之后检查任务队列是否存在宏任务,若存在则取出一个宏任务,执行完成检查是否有微任务,以此循环…
说了这么多,讲道理应该有个小栗子
异步更新队列解析(nextTick)
罗嗦了这么半天,本文的主角(nextTick)该登场了,当当当...
我们都知道Vue在更新 DOM 时是异步执行的,那么到底之怎么回事呢?
官方解释: 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
大概理解就是,DOM的更新应该是在当前线程都执行完在更新,所以就不会出现修改数据DOM立即更新的情况,应该在下一个'tick'中,vue刷新队列,更新DOM。
emmmm... 可能会有点疑惑,下面会一点一点解释一下
先大致说一下数据更新后触发的事件:
当我们在组件中对响应的数据做了修改,就会触发 set方法,最后调用 dep.notify() 方法,notify方法触发update()方法,系统收到更新指示就会触发queueWatcher()方法,queueWatcher方法会将get方法所收集的watcher推到一个异步队列,然后在 nextTick 后执行 flushSchedulerQueue()进行watcher的视图更新。
呈上nextTick源码,解释备注了哦(2.6+)
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
//重置pending,复制一份callback然后将callback重置,遍历执行所有的回调。
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//眼熟的微任务:
//promise.then,MutationObsever
//宏任务:
//setImmediate,setTimeout
//没错这部分是判断浏览器的兼容性的来定义timerFunc()方法的,先走微任务Promise.then,不支持的话走微任务MutationObserver,在不支持走接下来宏任务setImmediate,setTimeout。
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
//nextTick接收传过来的回调函数,然后将回调函数push到一个数组里面,如果没有cb,就会走下面的promise对象。
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
//timerFunc()只执行一次
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
如果看的有点乱,我就再来个小总结吧
nextTick的逻辑大概是这样的:
nextTick把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 isUsingMicroTask条件执行 timerFunc,然后在下一个 tick 执行 flushCallbacks,flushCallbacks 对 callbacks 遍历,然后执行相应的回调函数,最后完成watcher的视图更新。 这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 'tick' 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
this.$nextTick方法
看到这是不是突然想起来眼熟的this.$nextTick
在那么来结合下面的小栗子在理解一下nextTick和手动的
可见同步打印的DOM数据并没有改变,nextTick()的打印的DOM数据是我们刚刚更新的,所以,为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
注:数据更新后想要立即获取操作DOM才会用到this.$nextTick(),不要乱用哦!!