平时我们手写vm.$nextTick,Vue.nextTick都是这里的nextTick方法
src/core/observer/sheduler.ts
export function queueWatcher(watcher: Watcher) {
// ...
nextTick(flushSchedulerQueue)
}
}
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
for (index = 0; index < queue.length; index++) {
// ...
watcher = queue[index]
watcher.run()
// ...
}
}
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替。
例如,当你设置
vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用
src/core/util/next-tick.ts
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
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)
}
}
nextTick 把回调装进一个数组
src/core/util/next-tick.ts
export let isUsingMicroTask = false
const callbacks: Array<Function> = []
let pending = false
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
})
}
}
首先可以看到,回调cb被放到了一个callbacks数组中,但是没有直接把cb放到数组中,而是用箭头函数包了一层,里面使用了try...catch来防止用户传递的回调cb错误导致程序崩溃,后面代码无法执行,这里就会把flushSchedulerQueue放到callbacks中;
timerFunc()是把callbacks中存储的函数在下一次tick中遍历执行,再把callbacks清空,pending为false的时候才会执行timerFunc(),同时在下一次tick循环中会把pending再次变为false,如果每调一次nextTick就调用一次timerFunc,那么flushCallbacks会被重复放进队列中去等待执行
this.$nextTick(cb) 和 this.$nextTick().then(cb)
如果cb不存在,那么就会返回一个promise,this.$nextTick(cb) 和 this.$nextTick().then(cb)有些差别,前者在callbacks数组中按顺序同步执行会先执行,而后者又是放在微任务中执行,所以会后执行,例如:
<body>
<div id="app">
<div ref="name">{{name}}</div>
<button @click="change">change</button>
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data() {
return {
name: 'Cristiano'
}
},
methods: {
change() {
this.name = 'Messi'
this.$nextTick().then(() => {
console.log('没有传cb', this.$refs.name.innerText)
})
this.$nextTick(() => {
console.log('传了cb', this.$refs.name.innerText)
})
}
}
})
</script>
</body>
执行this.$nextTick().then(cb1), _resolve(ctx)被放入cllbacks数组中,然后执行this.$nextTick(cb2),cb2被放入cllbacks数组中,在下一个tick循环中,同步执行cllbacks中的函数,首先_resolve(ctx)执行,那么cb1就要放在本轮循环的微任务队列中等待执行,所以继续执行同步任务cb2,然后取出微任务cb1执行
执行顺序
而我们平时要在数据修改后使用使用$nextTick,才能保证拿到更新后的组件实例或者DOM元素,例如:
<body>
<div id="app">
<div ref="name">{{name}}</div>
<button @click="change">change</button>
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data() {
return {
name: 'Cristiano'
}
},
methods: {
change() {
this.$nextTick().then(() => {
console.log('没有传cb1', this.$refs.name.innerText)
})
this.$nextTick(() => {
console.log('传了cb1', this.$refs.name.innerText)
})
this.name = 'Messi'
this.$nextTick().then(() => {
console.log('没有传cb2', this.$refs.name.innerText)
})
this.$nextTick(() => {
console.log('传了cb2', this.$refs.name.innerText)
})
}
}
})
</script>
</body>
结合这个例子,放入callbacks数组的顺序为change方法中的书写顺序:
1._resolve(ctx)1,
2.传了cb1回调函数,
3.this.name = 'Messi',修改了数据,flushSchedulerQueue被放入,flushSchedulerQueue函数里面有渲染watcher,渲染watcher里面更新页面数据,
4._resolve(ctx)2,
5.传了cb2回调函数;
在下一个tick循环中同步执行callbacks中的函数:
1._resolve(ctx)1,把console.log('没有传cb1', this.$refs.name.innerText)放入微任务队列,
2.执行console.log('传了cb1', this.$refs.name.innerText),
3.执行flushSchedulerQueue,渲染watcher执行,页面数据更新,
4._resolve(ctx)2,把console.log('没有传cb2', this.$refs.name.innerText)放入微任务队列,
5.执行console.log('传了cb2', this.$refs.name.innerText),页面数据已经更新,
6.同步任务执行完了,依次执行微任务队列,console.log('没有传cb1', this.$refs.name.innerText),页面数据已经更新,
7.接着执行微任务队列中console.log('没有传cb2', this.$refs.name.innerText)