一、nextTick 小测试
<template>
<div id="app">
<p ref="name">{{ name }}</p>
<button @click="handleClick">修改name</button>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
name: '11111111'
}
},
mounted() {
console.log('mounted', this.$refs.name.innerText)
},
methods: {
handleClick () {
this.$nextTick(() => console.log('nextTick1', this.$refs.name.innerText))
this.name = '22222222'
console.log('sync log', this.$refs.name.innerText)
this.$nextTick(() => console.log('nextTick2', this.$refs.name.innerText))
this.$nextTick().then(()=>{
console.log('nextTick3', this.$refs.name.innerText')
})
}
}
}
</script>
请问上述代码中,当点击按钮“修改 name”时,'nextTick1','sync log','nextTick2'对应的 this.$refs.name.innerText 分别会输出什么?注意,这里打印的是 DOM 的 innerText ~
二、nextTick 源码实现
源码位于 core/util/next-tick 中。可以将其分为 4 个部分来看
1. 全局变量
callbacks 队列、pending 状态
const callbacks = []
let pending = false
2. flushCallbacks
遍历 callbacks 执行每个 cb
function flushCallbacks () {
pending = false // 注意这里,一旦执行,pending马上被重置为false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0
copies[i]() // 执行每个cb
}
}
3. nextTick 的异步实现
根据执行环境的支持程度采用不同的异步实现策略
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
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)
}
}
这里用个真实案例加深对 MutationObserver 的理解。毕竟比起其他三种异步方案,这个应该是大家最陌生的
const observer = new MutationObserver(() => console.log('观测到文本节点变化'))
const textNode = document.createTextNode(String(1))
observer.observe(textNode, {
characterData: true
})
console.log('script start')
setTimeout(() => console.log('timeout1'))
textNode.data = String(2)
console.log('script end')
4. nextTick 方法实现
cb、Promise 方式
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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
理解 pending 有什么用,如何运作?
案例 1,同一轮 Tick 中执行 2 次$nextTick,timerFunc只会被执行一次
三、Vue 组件的异步更新
Vue 的异步更新 DOM 其实也是使用 nextTick 来实现的,跟我们平时使用的$nextTick 其实是同一个
回顾 当我们改变一个属性值会发生什么
this.msg='new val' --> 新值存在变化? --> 是否是引用类型 --> dep.notify() --> 遍历 subs --> 调用 watcher.update() --> queueWatcher() --> watcher 推到 queue 队列中 --> nexttick 调用 flushSchedulerQueue -->排序 watcher 让父在子前-->调用 watcher.run --> watcher.get -->pushTarget() -->调用 vm.update(vm.render())-->执行 render -->patch 到 dom 上 -->popTarget() -->结束
1. queueWatcher 做了什么?
const queue: Array<Watcher> = []
function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
2. flushSchedulerQueue 做了什么?
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 排序保证先父后子执行更新,保证userWatcher在渲染Watcher前
queue.sort((a, b) => a.id - b.id)
// 遍历所有的需要派发更新的Watcher执行更新
for (index = 0
watcher = queue[index]
id = watcher.id
has[id] = null
// 真正执行派发更新,render -> update -> patch
watcher.run()
}
}
相信经过上文对 nextTick 源码的剖析,我们已经揭开它神秘的面纱了。
-
- mounted 11111111
-
- sync log 11111111
-
- nextTick1 11111111
-
- nextTick2 22222222
-
- nextTick3 22222222
注意:其虽然是放在$nextTick 的回调中,在下一个 tick 执行,但是他的位置是在 this.name = '22222222'的前。也就是说,他的 cb 会比 App 组件的派发更新(flushSchedulerQueue)更先进入队列,当 nextTick1 打印时,App 组件还未派发更新,所以拿到的还是旧的 DOM 值。