基础使用
nextTick,常用来当响应式数据变化后,想要基于更新后的dom进行一些操作。
<body>
<div id="app">
<p id="text">{{ text }}</p>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
text: 'hello',
},
mounted() {
this.text = 'hello world'
this.$nextTick(() => {
// nextTick输出 hello world
console.log('nextTick输出', document.getElementById('text').innerText)
})
// 直接输出 hello
console.log('直接输出', document.getElementById('text').innerText)
}
})
</script>
</body>
为什么不能立即拿到dom呢,因为vue的视图更新是微任务也就是异步的。
当你修改一个响应式数据后,数据的变化并不会立即的变化在dom上,vue会开启一个异步更新队列,在下一个事件更新tick中执行视图更新。nextTick确保在dom更新后,立即执行你的代码。
源码分析
- 在initGloabalApI函数中,将nextTick放到vue对象上。在renderMixin函数中,将nextTick函数绑定到vue的原型上
export function initGlobalAPI(Vue: GlobalAPI) {
Vue.nextTick = nextTick
}
export function renderMixin(Vue: typeof Component) {
Vue.prototype.$nextTick = function (fn: (...args: any[]) => any) {
return nextTick(fn, this)
}
}
- 将回调函数cb推入到callbacks队列中,然后执行timeFunc(), 这个函数会立即开启一个异步更新队列
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
3.通过Promise.resolve().then开启一个微任务,执行flushCallbacks函数
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)
}
4 依次遍历执行nextTick中被推入callbacks的cb
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
- 对于你自己使用的nextTick来说,这个cb是你自己的回调。而对于vue响应式数据变化来说,这个cb是flushSchedulerQueue,用来执行视图更新
/**
* Flush both queues and run the watchers.
*/
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(sortCompareFn)
// 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 (__DEV__ && 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
}
}
}
// 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)
cleanupDeps()
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}