本文基于 VUE 2.6.11 源码。Vue 3.0 的实现略有变化,但思路仍可借鉴2.6。文章末尾会讲一下3.0相关变化。
关于nextTick,我一直有很多疑惑:
-
这个函数是怎么做到在dom更新之后执行回调的?
-
响应式数据修改中,有一个将所有修改归并,并统一修改的现象,如何实现的?
有响应式数据a,b a = '修改a'; nextTick(打印a、b对应的dom) b = '修改b'; // 控制台打印出的a、b对应DOM均为修改后的状态,这又是怎么做到的? 复制代码
我们一一解答:
问题1:
这个事说起来挺有意思,和调用顺序有关,如果调用顺序恰当,确实能在dom更新前先执行回调,比如:
data() {
return {
content: 'before'
}
},
mounted() {
this.test()
},
methods: {
test() {
this.nextTick(() => {
console.log(this.$refs.box.innerHTML) // 在修改响应式数据前调用
})
this.content = 'after'
this.nextTick(() => {
console.log(this.$refs.box.innerHTML)
})
}
}
// 打印结果为:
// before
// after
复制代码
好,现在我们明白了这东西和顺序有关,那就需要某种数据结构来保存调用顺序。
咱们去看源码:github.com/vuejs/vue/b…
按照 ①—> ⑥ ,的顺序阅读注释
let pending = false // 防止timerFunc函数被重复执行
const callbacks = [] // ① 这个是保留nextTick(callback)中回调函数的数组
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)
}
})
if (!pending) {
pending = true // 因为timerFunc是异步调用,不做控制的话timerFunc函数可能被重复调用,从这里我们也能看出,timerFunc在异步队列中等待时,callbacks数组会不断积累调用nextTick时传入的函数。
timerFunc() // ③ 这个函数用于触发异步调用,把flushCallbacks函数丢进异步队列,代码看④
}
// 下面的代码忽略,和我们要关注的问题无关
// $flow-disable-line
// if (!cb && typeof Promise !== 'undefined') {
// return new Promise(resolve => {
// _resolve = resolve
// })
// }
}
timerFunc = () => { // ④,这个函数负责把flushCallbacks推入异步调用栈,你不用管它是用setTimeout还是promise.then或MutationObserver或setImmediate
setImmediate(flushCallbacks) // 看下面??
}
function flushCallbacks () { // ⑤ 把数组callbacks中的函数取出,依次调用
pending = false // 下面会把callbacks复制一份并清空,因此无需防止重复调用timerFunc
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]() // ⑥ 执行callbacks中保存的函数!
}
}
复制代码
好,现在我们知道这个 **按调用顺序保存回调函数 **是怎么实现的了。
但此时会引出一个新问题,看下面??????
修改响应式数据,是怎么和nextTick关联的?我明明没调用啊!
这个和响应式原理有关,我们去看看源码:
我们都知道,vue的响应式原理是类似于发布订阅模式的,每一个响应式数据,都有对应的**’事件中心’**,其中会有一堆观察者watcher来注册,等待数据变化时的通知,那么我们模拟一下:
this.content = 'after'
会触一次通知,此时watcher.update()
被调用。
update
函数又调用了另一个函数queueWatcher
。
queueWatcher
执行了这一行代码 nextTick(flushSchedulerQueue)
。flushSchedulerQueue
负责执行收集到的所有watcher。
OK,找到在哪里调用nextTick了!
源码如下:
/**
* Subscriber interface.
* Will be called when a dependency changes.会在依赖变化时被调用
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)// 调用调用调用调用调用调用调用调用调用
}
}
复制代码
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher) // ⚠️⚠️⚠️把所有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) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 这就是调用nextTick的地方
}
}
}
复制代码
问题2:响应式数据修改中,有一个将所有修改归并,并统一修改的现象,如何实现的?
收集时用一个队列存储watcher,修改时清空watcher。
收集依赖于函数queueWatcher
,就在上一个问题末尾处??????????????????,看带⚠️的注释。
连续执行依赖于函数提到的flushSchedulerQueue
函数,我们看下源码:
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
// ⚠️一次性执行所有收集到的watcher⚠️
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 (process.env.NODE_ENV !== 'production' && 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)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
复制代码
小结
源码分析到此结束,响应式原理的源码不在这里继续深入,再写就偏题了。
有兴趣的可在vue2.6源码中搜索以下内容,搭配相关文章,自行探索:
export function defineReactive // src/core/observer/index.js,用于构件响应式对象
// src/core/observer/index.js
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify() // setter 拦截赋值,触发一次通知
}
// src/core/observer/dep.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 调用update函数,和上文形成闭环
}
}
复制代码
在Vue3.0中,NextTick的实现有变化吗?
有!摘录一段源码。可以看到,使用promise.then()
存储&连接各个回调函数,保证调用顺序。或者直接返回一个promise,让使用者利用 async / await
进行控制,这很原生,妙啊~
// https://github.com/vuejs/vue-next/blob/44996d1a0a2de1bc6b3abfac6b2b8b3c969d4e01/packages/runtime-core/src/scheduler.ts#L42
export function nextTick(
this: ComponentPublicInstance | void,
fn?: () => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
复制代码
关于nextTick是基于宏任务或是微任务
这个是和运行环境相关的。在源码中,具体的实现被封装为函数timerFunc
后才使用。所以,代码层面,nextTick函数也不关心timerFunc的实现究竟是微任务还是宏任务。在 Vue 3 中,2.6的实现方式已被放弃,统一使用promise.then(),构件链状结构实现。
感兴趣的可阅读2.6.11源码:github.com/vuejs/vue/b…