上一篇有提到事件循环,及完整的事件循环(Event loop)过程解析,趁热打铁,理解一下vue中的nextTick()。
function nextTick(callback?: () => void): Promise<void>
官方文档中的解释是:“在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用nextTick(),获取更新后的 DOM。”
简单来说,nextTick(),是将回调函数延迟执行,在下一次DOM更新数据后调用,当数据更新并在DOM中渲染后,自动执行该函数。
看下官方示例:
<script>
import { nextTick } from 'vue'
export default {
data() {
return {
count: 0
}
},
methods: {
async increment() {
this.count++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
}
}
</script>
<template>
<button id="counter" @click="increment">{
{ count }}</button>
</template>
nextTick源码理解
简单理解一下之后,我们看下源码吧,在src/core/instance/render.js中 将nextTick定义到vue原型链上,这里的 this是指当前组件的this。
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
在src/core/util/next-tick.js中,通过export暴露出了 nextTick 函数。
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()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我们可以看到,nextTick接收两个参数,第一个是函数cb(即 我们要延迟执行的函数),第二个是this上下文。
在函数体内,callbacks是一个数组,用来存储所有需要执行的回调函数。判断cb存在,就把cb存到callbacks数组中,同时把cb的上下文指向组件的this。cb不存在,就把_resolve函数放到callbacks数组中。
然后判断pending的值,pending用来控制状态,判断是否有正在执行的回调函数。当pending为false,表示没有回调函数在执行;此时将pending设为true,然后执行timerFunc函数;
判断当cb不存在并且浏览器支持Promise时,返回一个Promise。也就是说当没有回调函数时,可以通过this.$nextTick().then(cb)的方式进行调用。
理解下来,这里的 timerFunc 应该是 nextTick 实现的关键函数了,我们看下 timerFunc 的源码:
let timerFunc
// nextTick行为利用了微任务队列,可以通过本机Promise访问该队列。
// 使MutationObserver有更广泛的支持,但它被严重窃听,
// 当在触摸事件中触发时,iOS中的UIWebView>=9.3.3。触发几次后完全停止工作…
// tips:源码中是一大段英文,方便理解,我直接翻译成中文,很明显,这里是交代了一下使用场景;
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// 在有问题的UIWebViews中,Promise.then()不会完全断裂,但是它可能会陷入一种奇怪的状态,
// 回调被推入微任务队列,但队列不会被刷新,直到浏览器需要做一些其他工作,例如处理计时器。
// 因此,我们可以通过添加空计时器“强制”刷新微任务队列
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 在原生Promise不可用的情况下使用MutationObserver,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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)) {
// 回退到setImmediate.
// 技术上,它利用(宏)任务队列,但它仍然是比setTimeout更好的选择
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 回退到setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
简单理解一下:if-else-if是对nextTicket 做兼容处理:判断系统是否支持promise,支持,用promise做延时处理;不支持,判断是否支持MutationObserver,setImmediate 和 setTimeout。
区别在于:promise 和 MutationObserver是微任务,setImmediate和setTimeout是宏任务,执行的顺序有差别,微任务的执行顺序早于宏任务;
我们看到,以上所有的延时中都执行了flushCallbacks函数,那我们看看flushCallbacks的源码:
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
执行flushCallbacks(),会把pending设为false,把callbacks数组复制一份到copies数组中,并将callbacks数组清空,然后对copies数组循环,并以此执行数组中每一项( 即执行在nextTick() 时存到callbacks数组中的cb())。
MutationObserver
我们看到,当不支持promise时,会用MutationObserver,字面意思来看是“变动观察器”,那我们来看看这是个啥吧。
MDN上的解释:MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。
简单理解为:MutationObserver 监听DOM变动,DOM 发生任何变动,MutationObserver 就会收到通知,所以vue可以用MutationObserver 来监听DOM 是否更新完毕。
引用MDN上的示例,来看一下MutationObserver的使用:
// 选择需要观察变动的节点
const targetNode = document.getElementById('some-id');
// 观察器的配置(需要观察什么变动)
// attributes: 属性的变动。
// childList: 子节点的变动。
// characterData: 节点内容或节点文本的变动。
// subtree: 所有后代节点的变动。
const config = { attributes: true, childList: true, characterData: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
// 之后,可停止观察
observer.disconnect();
observe()用来观察DOM节点变化,通过其回调函数接收通知。接收2个参数,第一个参数是要观察的DOM元素,第二个是要观察的变动类型。调用方式为observer.observe(dom, options)
我们看一个简单的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="http://cdn.bootcss.com/jquery/3.1.0/jquery.js"></script>
</head>
<body>
<div id='target'>
<p></p>
</div>
</body>
<script>
let targetNode = document.getElementById('target');
let pNode = targetNode.getElementsByTagName('p')[0];
let config = { attributes: true, childList: true, subtree: true }
let i = 0
let observe = new MutationObserver((mutations, observe) => {
i++
console.log('pNode', pNode, i)
});
observe.observe(target, config);
pNode.appendChild(document.createTextNode('哈哈哈'));
pNode.appendChild(document.createTextNode('1'));
pNode.appendChild(document.createTextNode('2'));
pNode.appendChild(document.createTextNode('3'));
pNode.setAttribute('class', 'test')
</script>
</html>
执行结果如下:
我们可以看到,我们总共加了四个文本节点到dom上,控制台上只打印了一次,也就是说MutationObserver只执行了一次,也就是说MutationObserver是等页面上所有dom完成后,再执行;
这样的话,我们就能理解nextTicket中MutationObserver的用法了,即通过对文本节点的操作来触发MutationObserver从而使flushCallbacks执行;
总结一下:
1. nextTick是Vue提供的⼀个全局API,由于vue的异步更新策略导致我们对数据的修改不会⽴刻体现在dom 变化上,如果想要⽴即获取更新后的dom状态,就需要使⽤这个⽅法;