Vue中的nextTick作用原理
1.Vue的异步更新策略
Vue采用了一种称为异步更新策略的机制。这意味着当数据发生变化时,Vue不会立即更新DOM,而是将更新任务放入一个队列中,等待下一个事件循环迭代时再进行更新。也就是说:当数据发生变化,Vue会开启异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新。
下面看一个例子:
<div id="root">{{message}}</div>
<script type="text/javascript">
const vm = new Vue({
el: "#root",
data() {
return {
message: "修改前的值",
};
},
mounted() {
this.message = "修改后的值";
let root = document.querySelector("#root");
console.log(root.textContent); //这时候打印的还是“修改前的值”
},
methods: {},
});
</script>
如上,页面一开始进去,在mounted钩子中修改root元素的内容,立马打印页面中root内部的值,这时候数据更新了但页面是没更新的。所以打印出来的还是修改前的值。

界面上的修改后的值是在这轮时间事件循环结束(肯定在console.log(root.textContent)之后)才更新到视图的。
我们可以验证一下上述说法----------------视图肯定是在console.log(root.textContent)之后才完成,至于什么时候执行,那就和事件循环内部情况有关了。因为视图的更新必须等待事件循环迭代时再进行更新。
mounted() {
this.message = "修改后的值";
let root = document.querySelector("#root");
console.log(root.textContent); //这时候打印的还是“修改前的值”
debugger;
},
如上,添加了一个debugger,它可以在此处终止程序的运行。我们看看刚初始的界面:

这说明在进行debugger时候页面的视图还没有更新。
如果改为以下写法,在setTimeout(这一块和事件循环有关,后续我会讲到)中去进行debugger,结果就不一样了。
mounted() {
this.message = "修改后的值";
let root = document.querySelector("#root");
console.log(root.textContent); //这时候打印的还是“修改前的值”
setInterval(() => {
debugger;
}, 0);
},
初始界面如下,会发现这时候页面的视图已经更新:

2.Vue为什么会有异步更新这种机制
如果是同步更新dom元素,每次数据一更新就要更新视图,而更新视图需要经过很多步骤,会有回流重绘操作,会造成性能的浪费。
3.nextTick
3.1 nextTick的作用
由于 Vue 的异步更新策略,当我们在数据发生变化后立即访问或修改DOM 元素时,可能会遇到数据已经改变但 DOM 尚未更新的情况。这时,我们可以使用nextTick方法来延迟执行后面代码,确保在 DOM 更新后执行。
nextTick方法接受一个回调函数作为参数,这个回调函数会在DOM 更新完成后被调用。这样,我们可以在回调函数中安全地访问和修改 DOM 元素。
3.2 nextTick的用法
还是以1.1为例,我们可以把代码改成下面这样子:
mounted() {
this.message = "修改后的值";
let root = document.querySelector("#root");
console.log(root.textContent); //这时候打印的还是“修改前的值”
this.$nextTick(() => {
console.log(root.textContent); //这时候打印的是“修改后的值”
});
},
这时候会发现nextTick内部执行的操作是建立在页面视图已经更新好的基础上了:

3.3 nextTick的原理
源码位置:/src/core/util/next-tick.js:
如下callbacks表示异步操作队列。我们会往callbacks中新增回调函数,新增回调函数之后会执行timerFunc函数,而pending用于标识同一时间只能执行一次。
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// cb callbacks
callbacks.push(() => {
if (cb) {
// cb try-catch
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
// timerFunc
if (!pending) {
pending = true;
timerFunc();
}
// nextTick Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
下面分析一下timerFunc函数。它是根据当前环境支持什么方法才决定调用哪个,分别有Promise.then、 MutationObserver、setImmediate、setTimeout。通过上面的任意一种方法,进行优雅降级操作。代码如下:
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 1 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]'
)) {
// 2 MutationObserver
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)){
// 3 setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 4 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
无论是微任务还是宏任务,都会放到flushCallbacks使用。这里将callbacks里面的函数复制一份,同时将callbacks置空,依次执行callbacks中的函数:
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
总结一下,大致过程就是
- 将回调函数放入到
callbacks等待执行 - 将执行任务放到微任务和宏任务队列中
- 当事件循环到了微任务和宏任务队列,执行函数一次执行
callbacks中的回调


7582

被折叠的 条评论
为什么被折叠?



