vue异步更新Dom 原理
如:
我们修改一个数据,之后立即获取其修改后的值,但结果却不是我们想要的,获取的却是以前的值,而不是最新的
<template>
<div>
<div id="text">{{ message }}</div>
<button @click="d1">
changeData
</button>
</div>
</template>
<script>
export default {
data() {
return {
message: "11",
};
},
methods: {
d1() {
this.message = "22;
const textContent = document.getElementById("d1").textContent;
// textContent 是 11 而不是22
// $nextTick 回调中,是最新的
this.$nextTick(() => {
const textContent = document.getElementById("text").textContent;
、、 textContent 的值是 22
});
},
},
};
</script>
此时就牵涉到了 vue异步更新DOM的原理
当set检测到 数据发生变化,就会Dep里找对应的依赖Watcher,向Watcher 发送消息,Watcher 执行对应的更新函数。
看一下源码:
class Watcher {
// 只展示需要讲的,即update
update() {
queueWatcher(this);
}
}
const queue = [];
function queueWatcher(watcher: Watcher) {
queue.push(watcher);
nextTick(flushSchedulerQueue);
}
function flushSchedulerQueue() {
let watcher, id;
queue.sort((a, b) => a.id - b.id);
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
// 更新DOM
watcher.run();
}
}
(1) 通过执行update,触发queueWatcher 函数,此函数 是将此依赖 Watcher 放进queue(实际就是一个异步队列)
(2) 之后触发nextTick 函数,将默认的回调传进去(flushSchedulerQueue,这个函数其实就是更新视图的方法,在里面会判断依赖是否存在,排除重复的依赖,还会排序,先渲染父节点,再渲染子节点,避免不必要的节点渲染:如:父节点 v-if已经设置为false,则子节点就不用渲染了)
在nextTick函数中,会对回调函数添加到 一个叫callbacks 这个数组里面,这个数组就是专门用来存储回调函数的
const callbacks = [];
let timerFunc;
function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
callbacks.push(() => {
cb.call(ctx);
});
timerFunc();
}
(3)在nextTick 函数中 触发 timerFunc()
此方法,就是根据浏览器的兼容性问题,判断浏览器是否支持 promise、MutationObserve、setImmediate,如果以上都不支持,则执行一个定时器,根据兼容的情况,创建一个异步方法,来执行更新操作(flushcallBack)
let timerFunc;
if (typeof Promise !== "undefined") {
timerFunc = () => {
Promise.resolve().then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
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);
};
} else if (typeof setImmediate !== "undefined") {
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// 清空回调队列,同时执行所有回调函数
function flushCallbacks() {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
flushCallback ()清空callback队列、同时依次所有执行回调函数。
2.this.$nextTick 的原理
实际上就是调用了nextTick 方法
本质如下:
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this);
};
通过调用nextTick 这个方法,在callback中添加回调函数,在异步队列中,根据进入先后,执行对应函数,应用 更新视图的方法(flushSchedulerQueue)先进入队列中,则先执行,执行后就生成了新的dom, this.$nextTick 传的回调后执行,此时就能获得更新后的DOM元素。
总结:
vue异步更新dom的原理
1.修改数据时,会先将所有的依赖都放在 queue(异步队列中)
2.调用nextTick方法,将所有回调函数都添加在callbacks 这个数组中,执行timerFunc,根据浏览器的兼容性,来创建一个异步任务
3.异步完成后,执行对应的回调,对依赖进行排序,然后执行run()方法,对dom进行更新