概要
官网说明:将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
应用
首先看下开发中是如何应用的
<template>
<div>{{ tips }}</div>
</template>
methods: {
upateDom1() {
this.tips = '第一种用法'
console.log(this.tips) // => DOM未更新
// 参数 -> 回调函数
this.$nextTick(function() {
console.log(this.tips) // => DOM已更新
})
},
async updateDom2 () {
this.tips = '换一种用法'
console.log(this.tips) // => DOM未更新
/*因为 this.$nextTick() 返回的是 Promise,所以可以使用 async/await 语法 */
await this.$nextTick()
console.log(this.tips) // => DOM已更新
}
}
源码解析
Vue版本: 2.6.14
源码路径:/src/core/util/next-tick.js
初始化,先准备一个存储回调函数的队列,再准备一个执行函数
// 回调函数队列
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]()
}
}
大概分析函数
nextTick都做了啥
- 回调函数
cb不为空推进队列callbackspending为false,执行函数timerFunc下一段代码分析它- 最后一段 if 就可以解释为啥可以用语法
async/await
/**
* @param {Function} cb 回调函数
* @param {Object} ctx 上下文
*/
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)
}
})
// pending = false 执行callbacks里的函数
if (!pending) {
pending = true
// 执行被包装后的函数
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
💡 Tips:提一嘴,使用的时候没传过
ctx这玩意儿啊,其实是封装了一层,如下:
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
如下代码可以看出
timerFunc的最终目的是为了执行flushCallbacks细节上,对于处理一些环境/兼容等问题而做的策略优先级:
Promise->MutationObserver->setImmediate->setTimeout
let timerFunc
/** 优先使用 promise */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// 在有问题的UIWebViews中,Promise.then 会有一个奇怪的状态,回调被推到微任务队列,
// 但队列不会被更新,直到浏览器需要做一些其他的操作,例如处理计时器,所以我们可以通过添加一个空定时器来强制触发
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// 这里通过 MutationObserver 的特性来触发回调函数的执行
// MutationObserver 详情:https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
} 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)) {
// 回退到 setImmediate.
// 技术角度,它利用了宏任务队列,但它仍然比 setTimeout 更好
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 回退到 setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
那么this.$nextTick(cb) 的 cb 内又是怎么知道 this.tips = '第一种用法' 这段DOM已经更新了呢,其实这段代码已经按照执行顺序先一步推进 nextTick 的队列,简单的说 data 里的数据变更也是需要排队的
如下:flushSchedulerQueue 主要是对DOM更新处理
源码路径:/src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
if (!waiting) {
waiting = true
// 更新DOM的处理 作为回调入参,
nextTick(flushSchedulerQueue)
}
}
}
扯到 watcher 还很多东西可以讲,但这是 nextTick 的专题,俺今天能理解它就够了
官网说明:异步更新队列