blog
blog copied to clipboard
Vue的nextTick的是怎么实现的?
Vue.nextTick的作用
在下次DOM更新结束之后执行延迟回调。通常在修改数据之后调用这个方法,便于获取更新之后的DOM。
下面题目的输出是什么
最近在看nextTick相关的文章的时候,看到一些很有意思的题目,可以结合下面的题目了解nextTick源码和watcher源码。
题目一
<template>
<div>
<div ref="test">{{test}}</div>
<button @click="handleClick">click me</button>
</div>
</template>
export default {
data () {
return {
test: 'begin'
};
},
methods () {
handleClick () {
this.test = 'end';
console.log(this.$refs.test.innerText)
}
}
}
题目二
<div id="example">
<div ref="test">{{test}}</div>
<button @click="handleClick">click me</button>
</div>
var vm = new Vue({
el: '#example',
data: {
test: 'begin',
},
methods: {
handleClick() {
this.test = 'end';
console.log('1')
Promise.resolve().then(function promiseFn () {
console.log('promise!')
})
this.$nextTick(function nextTickFn () {
console.log('2')
})
}
}
})
题目三
<div id="example">
<div ref="test">{{test}}</div>
<button @click="handleClick">click me</button>
</div>
var vm = new Vue({
el: '#example',
data: {
test: 'begin',
},
methods: {
handleClick() {
// this.test = 'end'; // 相对于题目二删除了这行代码
console.log('1')
Promise.resolve().then(function promiseFn () { //microTask
console.log('promise!')
})
this.$nextTick(function nextTickFn () {
console.log('2')
})
}
}
})
可以进入blog/vue-demo/nextTick
目录运行上面的示例,
下面是Vue2.6在chrome的输出结果:
# 题目一
begin
# 题目二
1
2
promise
# 题目三
1
promise
2
Vue.nextTick的原理
看了nextTick源码可以发现,nextTick会把回调函数放入微任务队列或者宏任务队列,如果浏览器支持promise或者MutationObserver的话,就把回调函数放入微任务队列,否则就用setImmediate或者setTimeout放入宏任务队列。
在事件循环中,每进行一次事件循环操作称为tick,nextTick的含义是希望在下一次事件循环执行回调函数。
从nextTick的源码来看,如果放入微任务队列的话,是在本次事件循环中的微任务队列尾部插入回调函数;如果放入宏任务队列的话,则是在下一次的事件循环中去执行。
结合nextTick源码看下面的示例
this.$nextTick(function test01 () {
console.log('test01')
})
// 上面的代码做了什么?
// 1. 把函数test01做了一层异常捕获,再放入callbacks数组。现在callbacks: [ test01 ]
// 2. pending设置为true
// 3. 执行timerFunc,将flushCallbacks放入微任务队列或者宏任务队列,flushCallbacks是一个执行callbacks数组的函数。
this.$nextTick(function test02 () {
console.log('test02')
})
// 上面的代码做了什么?
// 1. 把函数test01做了一层异常捕获,再放入callbacks数组。现在callbacks: [ test01, test02 ]
// 2. 因为pending已经为true,就不会执行timerFunc
上面两个nextTick的作用就是把 test01和test02放入callbacks数组,再把flushCallbacks放入微任务队列或者宏任务队列,在事件循环中执行flushCallbacks方法,就是执行callbacks里面的两个回调数组test01和test02,最后通过 callbacks.length = 0
将callbacks置为空数组。
异步更新dom是如何实现的
从题目一的输出,我们也可以看到Vue的dom更新默认是异步的
// 题目一
this.test = 'end'
console.log(this.$refs.test.innerText)
更新test的值,会触发test的setter,在setter里面会触发所有依赖test的观察者,触发每个watcher的update方法,然后在update方法里面执行queueWatcher
。
// https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js#L164
update () {
if (this.lazy) { // lazy是跟computed相关的
this.dirty = true
} else if (this.sync) { // 当数据变化时,是否同步执行回调。默认是异步
this.run()
} else {
queueWatcher(this)
}
}
下面是queueWatcher
的主要源码
// https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js#L164
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) { // 是否当前正在执行queue队列里面的函数
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 > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) { // 当前quene是否为空
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
queueWatcher方法会将watcher放到一个queue中,如果queue中已经有相同id的watcher则不会放入queue,然后将flushSchedulerQueue方法通过nextTick放入callbacks数组中。
flushSchedulerQueue会将queue中的watcher根据id升序排序,id较小的先执行,确保①从父组件到子组件的更新顺序,②确保render watcher后执行,③确保在父组件的watcher中销毁子组件,子组件的watcher会被跳过,因为子组件watcher的active会被置为false。
执行flushSchedulerQueue
会执行所有的watcher的run方法,然后触发updated钩子和activated钩子,最后通过执行resetSchedulerState
,将queue置为空数组。
下面是flushSchedulerQueue
的源码
// https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js#L71
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()
}
// 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)
}
题目解析
在了解watcher源码和nextTick源码之后,可以了解到watcher内部也是使用了nextTick方法,nextTick内部维护了一个callbacks队列,使用flushCallbacks
方法执行所有callbacks中的回调函数。watcher执行内部也维护了一个queue队列,使用flushSchedulerQueue
执行所有watcher的run方法。
掌握了这些知识点之后,我们再来回顾一下最初的题目。
题目一
queue:[test的渲染watcher]
callbacks: [flushSchedulerQueue方法]
微任务队列或者宏任务队列:[flushCallbacks]
题目二
queue:[test的渲染watcher]
callbacks: [flushSchedulerQueue,nextTickFn]
微任务队列或者宏任务队列:[flushCallbacks, promiseFn]
题目三
queue:[]
callbacks: [nextTickFn]
微任务队列或者宏任务队列:[ promiseFn,flushCallbacks]