背景
当我们修改属性时,会重新渲染更新页面,每一次修改对应一次页面更新,如果某一次操作中进行了多次修改属性,那就有可能产生多次页面更新。而我们想要的结果是在修改了多次属性后,只需进行一次页面更新,并且在更新属性后,可以访问到最新的dom元素vm.$el。
收集需要更新的watcher
当属性发生改变时会调用watcher的update()方法进行页面更新,因此我们可以在这里开始做批处理。收集watcher时做去重处理,不需要重复的watcher。
update(){
// 批处理更新
queueWatcher(this);
}
每次调用update方法都会将watcher加入一个队列中存储,相当于将每次修改属性的操作都存储了起来
let has = {};
let queue = [];
let pending = false;
function flushSchedularQueue(){
console.log('更新页面');
queue.forEach(watcher => {
watcher.run();
})
has = {};
queue = [];
pending = false;
}
export function queueWatcher(watcher){
let id = watcher.id;
if(!has[id]){
queue.push(watcher);
has[id] = true;
// 多次调用queuewatcher,如果watcher不是同一个,就会调用多次nextTick,所以加入pending控制一下,只调用一次nextTick,其他watcher只需加入列表queue
if(!pending){
pending = true;
// 异步更新页面
nextTick(flushSchedularQueue);
}
}
}
nextTick异步调用回调函数
利用nextTick来更新页面,同时将nextTick挂载在vue原型链上抛出去,这样用户也能使用nextTick,就能利用nextTick来执行页面更新完毕后的操作。nextTick的原理:将回调函数存入一个列表中,通常第一个回调函数就是更新页面的逻辑,而后面的回调函数往往是用户定义的逻辑,同步将回调函数加入队列,最后异步执行队列里的所有回调函数。vue2里面在这里( Promise.resolve().then(flushCallbacks) )对执行异步操作其实有一个优雅降价的写法:promise > mutationObserver > setImmdiate > setTimeout,而vue3则是直接使用promise。
let callbacks = [];
let waiting = false;
function flushCallbacks(){
// 第一次cb渲染watcher更新操作
// 第二次后面cb是用户传入的回调
callbacks.forEach(cb => {
cb();
})
callbacks = [];
waiting = false;
}
export function nextTick(cb){
callbacks.push(cb); // 默认的cb是渲染逻辑 用户的逻辑放到渲染逻辑之后,就可以保证用户获取到渲染后的内容
// 批处理,只有首次会开定时器,后续只需加入callbacks列表中
if(!waiting){
waiting = true;
Promise.resolve().then(flushCallbacks);
}
}
Vue.prototype.$nextTick = nextTick;
更新数组
将当前数组和watcher关联起来,递归里层数组,让里层数组也收集watcher。
function defineReactive(obj, key, value){
let childOb = observe(value); // 递归每一层,监听每一层对象
let dep = new Dep()
Object.defineProperty(obj, key, {
get(){
if(Dep.target){
dep.depend();
if(childOb){ // 如果对数组取值 会将当前的watcher和数组关联
childOb.dep.depend();
if(Array.isArray(value)){
dependArray(value);
}
}
}
return value;
},
set(newValue){
if(value === newValue) return;
observe(newValue);
value = newValue;
dep.notify();
}
})
}
// 让里层数组收集外层数组的依赖,这样修改里层数组也可以更新视图
function dependArray(value){
for(let i=0; i< value.length; i++){
let current = value[i];
current._ob_ && current._ob_.dep.depend(); // 让里层和外层都是收集同一个watcher
if(Array.isArray(current)){
dependArray(current);
}
}
}
当数组内容发生改变时,通知更新视图。
methodList.forEach(method => {
newArray[method] = function(...args){
let result = Array.prototype[method].apply(this, args);
let inserted = null;
const ob = this._ob_;
switch(method){
case 'splice':
inserted = args.slice(2);
break;
case 'unshift':
case 'push':
inserted = args;
break
default:
break;
}
if(inserted){
ob.observeArray(inserted);
}
// 通知更新
ob.dep.notify();
return result
}
})
效果
这样多次修改message数据,最终也只是更新一次页面,同时也可以利用$nextTick访问到更新页面后的内容。
const vm = new Vue({
el: '#app',
data: {
message: 'hello',
people: {
name: 'lily',
},
bookList: ['哈利波特', '鲤鱼历险记', '老人与海'],
}
})
setTimeout(() => {
console.log('一秒后');
vm._data.message = 'hahaha';
vm._data.message = '什么玩意';
vm._data.message = '笑死';
vm._data.message = '啊这';
vm.$nextTick(() => {
console.log(vm.$el);
})
}, 1000);
理解
批处理更新其实就是将多次渲染只执行一次渲染,有点像防抖但又不是防抖,他是巧妙的利用watcher、同步代码和异步代码来实现的,更新操作放在异步代码里,利用同步代码收集所有watcher,所以只有watcher收集完毕才会去执行异步代码更新页面。数组更新方面有点绕,比较难理解。