接:
1. 异步更新
假设我们下面一种情况,我们在2s后多次去修改某一个变量的值:
setTimeout(() => {
vm.msg = '1';
vm.msg = '2';
vm.msg = '3';
vm.msg = '4';
}, 2000);
发现页面正常显示了msg的值,但是此时的数据更新导致的页面更新了4次,显然是不太合理的,我们想要的是最终视图只更新一次,所以我们需要将之前的同步更新逻辑修改成异步的。我们首先将watcher放到一个数组中,首先用一个最简单的办法使用setTimeout在一轮任务执行结束后统一进行更新。
class Watcher {
...
update() {
queueWatcher(this);
}
run() {
console.log('数据更新');
this.get()
}
}
const queueIds = new Set();
let queue = [];
function flushQueue() {
if (!queue.length) return;
queue.forEach(watcher => watcher.run())
queueIds.clear();
queue = [];
}
function queueWatcher(watcher) {
const id = watcher.id;
if (!queueIds.has(id)) {
queueIds.add(id);
queue.push(watcher);
setTimeout(flushQueue, 0);
}
}
这也就简单实现了视图的批量更新操作。
2. nextTick
此时的页面会统一进行一次刷新,但是Vue.$nextTick的实现并不单单是用setTimeout实现,nextTick内部同样也是维护了一个事件队列,等同步事件执行完毕后清空,就像我们上面写到的queueWatcher一样,但是内部针对浏览器的api支持程度做了一些兼容和优化。
在异步队列中,微任务的优先级更高,所以优先使用Promise而不是setTimeout,另外还有几个异步的api,它们的优先级顺序分别是:
- Promise(微任务)
- MutationObserver(微任务)
- setImmediate(宏任务)
- setTimeout(宏任务)
const callbacks = []
function flushCallbacks() {
callbacks.forEach(cb => cb())
}
export default function nextTick(cb) {
callbacks.push(cb)
const timerFunc = () => {
flushCallbacks()
}
if (Promise) {
return Promise.resolve().then(flushCallbacks)
}
if (MutationObserver) {
const observer = new MutationObserver(timerFunc)
const textNode = document.createTextNode('1')
observer.observe(textNode, { characterData: true })
textNode.textContent = '2'
return
}
if (setImmediate) {
return setImmediate(timerFunc)
}
setTimeout(timerFunc, 0)
}
然后将之前的setTimeout替换成nextTick看一下效果:
function queueWatcher(watcher) {
const id = watcher.id;
if (!queueIds.has(id)) {
queueIds.add(id);
queue.push(watcher);
+ nextTick(flushQueue, 0)
- setTimeout(flushQueue, 0);
}
}
效果相同,视图也只更新了一次,实现了视图的异步批量更新。
到现在为止的话,我们针对响应式原理已经做了基本功能的实现,后面我们会去对computed和watch进行一下简单的模拟。
代码部分可看本次提交commit