问题:如代码所示,我们在多次更新data属性,会不会触发多次dom diff计算和渲染,在更新data属性到dom渲染过程发生了什么事?
<template>
<div>
<div>{{title}}</div>
<div @click="refresh">更新</div>
</div>
</template>
<script>
export default {
data() {
return {
title: ''
};
},
methods: {
refresh() {
this.title = '测试数据1';
this.title = '测试数据2';
this.title = '测试数据3';
this.title = '测试数据4';
this.title = '测试数据5';
this.title = '测试数据6';
}
}
};
</script>
一、了解Observer、Dep、Watcher
1、vue组件初始化
vue组件初始化的时候会执行_init函数,从而做了一些初始化处理,在处理不同阶段中调用不同的vue生命周期函数,例:breforeCreate、created等
Vue.prototype._init = function (options) {
var vm = this;
initEvents(vm);
// 将render函数转化为vnode
initRender(vm);
// 调用beforeCreate生命周期函数
callHook(vm, 'beforeCreate');
initInjections(vm);
// 初始化处理props、methods、data、computed、watch
initState(vm);
initProvide(vm);
// 调用created生命周期函数
callHook(vm, 'created');
....
// 挂载组件到DOM树
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
2、Observer
initState → initData → observe → new Observer(data) → Observer.walk(data) → defineReactive$$1(data, key) → Object.defineProperty(data, key, {get, set})
通过调用链可以发现在initData开始,由Observer开始对data每个key值进行set、get的拦截监听(利用Object.defineProperty),同时可以发现在defineReactive$$1中对每个key值的拦截监听都会创建一个Dep对象,在get的时候调用dep.depend(),在set的时候调用dep.notify()
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
// 对data的key进行set、get拦截
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
dep.depend();
},
set: function reactiveSetter (newVal) {
dep.notify();
}
}
}
3、Dep
Dep对象主要将watcher对象维护在自己的属性中,同时也将自己加入到watcher中,在notify时候会调用watcher的update方法
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
// 将watcher加入到dep的subs数组中
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
// 将dep加入到watcher的deps数组中、同时
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
// 遍历dep中watcher、调用他们update方法
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
4、Watcher
vm.$mount → mountComponent → new Watcher(vm, updateComponent)
由调用链在创建Watcher对象后,会将自己压入栈顶,等待触发data 属性getter时候,将dep和watcher对象互相绑定
// 挂载组件
function mountComponent (
vm,
el,
hydrating
) {
updateComponent = function () {
// vm.render()执行render函数生成新节点
vm._update(vm._render(), hydrating);
};
// 创建Watcher对象
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true);
}
/************* Watcher函数 **********************/
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
// 存储的dep数组
this.deps = [];
// 存储的dep id
this.depIds = new _Set();
// 存储调用者传过来的updateComponent
this.expression = expOrFn.toString();
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
}
this.value = this.lazy ? undefined : this.get();
}
// watcher的get方法
Watcher.prototype.get = function get () {
// 将当前watcher压栈
pushTarget(this);
value = this.getter.call(vm, vm);
// 将当前watcher出栈
popTarget();
}
// 添加dep到Watcher对象属性中
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
// 将watcher对象添加到dep对象中
dep.addSub(this);
}
}
};
// Watcher对象update方法
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
// 懒加载
this.dirty = true;
} else if (this.sync) {
// 同步组件
this.run();
} else {
// watcher队列
queueWatcher(this);
}
};
// Watcher对象run方法
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
二、vue组件中数据更新会发生什么事情?
由于上面我们同步了一些关键信息,通过断点结合,可以看到整个调用过程
1、数据更新,我们首先会触发Data的set拦截,同时调用dep的notify方法。
2、遍历dep对象中的watcher数组,执行watcher的update方法。
3、在watcher的update函数中调用queueWatcher方法,并将自己作为入参。
4、在queueWatcher函数中,判断has map是否有当前watcher id,没有则将watcher存入queue队列,记录watcher id到has map中,调用nextTick方法;有则跳过这一步,等待异步刷新队列执行(异步刷新队列:flushSchedulerQueue)。
5、在nextTick中,将flushSchedulerQueue push进callbacks数组,执行timerFunc方法。
6、timerFunc中等待浏览器的微任务/宏任务回调时候遍历执行callbacks数组中的异步刷新队列方法flushSchedulerQueue。
7、在flushSchedulerQueue中,调用watcher的run方法。
8、在watcher的run中,调用watcher的get方法。
9、在watcher的get中,调用watcher的getter属性方法。
10、在Watcher对象初始化时候,getter就是mountComponent时候传入updateComponent方法。
11、执行updateComponent方法,会调用组件_update方法,传入当前组件_render函数返回值。
12、在_render函数中,获取当前组件data中的值(当前最新的值)。
13、在_update中,调用patch方法,传入新旧Vnode。
14、在patch中,调用patchVnode方法,开始diff比较虚拟dom树。