7.9 data
7.9.1 问题思考
在开始分析data
之前,我们先抛出几个问题让读者思考,而答案都包含在接下来内容分析中。
前面已经知道,
Dep
是作为管理依赖的容器,那么这个容器在什么时候产生?也就是实例化Dep
发生在什么时候?Dep
收集了什么类型的依赖?即watcher
作为依赖的分类有哪些,分别是什么场景,以及区别在哪里?Observer
这个类具体对getter,setter
方法做了哪些事情?- 手写的
watcher
和页面数据渲染监听的watch
如果同时监听到数据的变化,优先级怎么排? - 有了依赖的收集是不是还有依赖的解除,依赖解除的意义在哪里?
带着这几个问题,我们开始对data
的响应式细节展开分析。
7.9.2 依赖收集
data
在初始化阶段会实例化一个Observer
类,这个类的定义如下(忽略数组类型的data
):
// initData
function initData(data) {
···
observe(data, true)
}
// observe
function observe(value, asRootData) {
···
ob = new Observer(value);
return ob
}
// 观察者类,对象只要设置成拥有观察属性,则对象下的所有属性都会重写getter和setter方法,而getter,setting方法会进行依赖的收集和派发更新
var Observer = function Observer (value) {
···
// 将__ob__属性设置成不可枚举属性。外部无法通过遍历获取。
def(value, '__ob__', this);
// 数组处理
if (Array.isArray(value)) {
···
} else {
// 对象处理
this.walk(value);
}
};
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable, // 是否可枚举
writable: true,
configurable: true
});
}
Observer
会为data
添加一个__ob__
属性, __ob__
属性是作为响应式对象的标志,同时def
方法确保了该属性是不可枚举属性,即外界无法通过遍历获取该属性值。除了标志响应式对象外,Observer
类还调用了原型上的walk
方法,遍历对象上每个属性进行getter,setter
的改写。
Observer.prototype.walk = function walk (obj) {
// 获取对象所有属性,遍历调用defineReactive###1进行改写
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive###1(obj, keys[i]);
}
};
defineReactive###1
是响应式构建的核心,它会先实例化一个Dep
类,即为每个数据都创建一个依赖的管理,之后利用Object.defineProperty
重写getter,setter
方法。这里我们只分析依赖收集的代码。
function defineReactive###1 (obj,key,val,customSetter,shallow) {
// 每个数据实例化一个Dep类,创建一个依赖的管理
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
// 属性必须满足可配置
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
// 这一部分的逻辑是针对深层次的对象,如果对象的属性是一个对象,则会递归调用实例化Observe类,让其属性值也转换为响应式对象
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,s
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
// 为当前watcher添加dep数据
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {}
});
}
主要看getter
的逻辑,我们知道当data
中属性值被访问时,会被getter
函数拦截,根据我们旧有的知识体系可以知道,实例挂载前会创建一个渲染watcher
。
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
与此同时,updateComponent
的逻辑会执行实例的挂载,在这个过程中,模板会被优先解析为render
函数,而render
函数转换成Vnode
时,会访问到定义的data
数据,这个时候会触发gettter
进行依赖收集。而此时数据收集的依赖就是这个渲染watcher
本身。
代码中依赖收集阶段会做下面几件事:
- 为当前的
watcher
(该场景下是渲染watcher
)添加拥有的数据。 - 为当前的数据收集需要监听的依赖
如何理解这两点?我们先看代码中的实现。getter
阶段会执行dep.depend()
,这是Dep
这个类定义在原型上的方法。
dep.depend();
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.target
为当前执行的watcher
,在渲染阶段,Dep.target
为组件挂载时实例化的渲染watcher
,因此depend
方法又会调用当前watcher
的addDep
方法为watcher
添加依赖的数据。
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
// newDepIds和newDeps记录watcher拥有的数据
this.newDepIds.add(id);
this.newDeps.push(dep);
// 避免重复添加同一个data收集器
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
其中newDepIds
是具有唯一成员是Set
数据结构,newDeps
是数组,他们用来记录当前watcher
所拥有的数据,这一过程会进行逻辑判断,避免同一数据添加多次。
addSub
为每个数据依赖收集器添加需要被监听的watcher
。
Dep.prototype.addSub = function addSub (sub) {
//将当前watcher添加到数据依赖收集器中
this.subs.push(sub);
};
getter
如果遇到属性值为对象时,会为该对象的每个值收集依赖
这句话也很好理解,如果我们将一个值为基本类型的响应式数据改变成一个对象,此时新增对象里的属性,也需要设置成响应式数据。
- 遇到属性值为数组时,进行特殊处理,这点放到后面讲。
通俗的总结一下依赖收集的过程,每个数据就是一个依赖管理器,而每个使用数据的地方就是一个依赖。当访问到数据时,会将当前访问的场景作为一个依赖收集到依赖管理器中,同时也会为这个场景的依赖收集拥有的数据。
7.9.3 派发更新
在分析依赖收集的过程中,可能会有不少困惑,为什么要维护这么多的关系?在数据更新时,这些关系会起到什么作用?带着疑惑,我们来看看派发更新的过程。在数据发生改变时,会执行定义好的setter
方法,我们先看源码。
Object.defineProperty(obj,key, {
···
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
// 新值和旧值相等时,跳出操作
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
···
// 新值为对象时,会为新对象进行依赖收集过程
childOb = !shallow && observe(newVal);
dep.notify();
}
})
派发更新阶段会做以下几件事:
- 判断数据更改前后是否一致,如果数据相等则不进行任何派发更新操作。
- 新值为对象时,会对该值的属性进行依赖收集过程。
- 通知该数据收集的
watcher
依赖,遍历每个watcher
进行数据更新,这个阶段是调用该数据依赖收集器的dep.notify
方法进行更新的派发。Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
if (!config.async) {
// 根据依赖的id进行排序
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
// 遍历每个依赖,进行更新数据操作。
subs[i].update();
}
};
- 更新时会将每个
watcher
推到队列中,等待下一个tick
到来时取出每个watcher
进行run
操作Watcher.prototype.update = function update () {
···
queueWatcher(this);
};
queueWatcher
方法的调用,会将数据所收集的依赖依次推到queue
数组中,数组会在下一个事件循环'tick'
中根据缓冲结果进行视图更新。而在执行视图更新过程中,难免会因为数据的改变而在渲染模板上添加新的依赖,这样又会执行queueWatcher
的过程。所以需要有一个标志位来记录是否处于异步更新过程的队列中。这个标志位为flushing
,当处于异步更新过程时,新增的watcher
会插入到queue
中。function queueWatcher (watcher) {
var id = watcher.id;
// 保证同一个watcher只执行一次
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
···
nextTick(flushSchedulerQueue);
}
}
nextTick
的原理和实现先不讲,概括来说,nextTick
会缓冲多个数据处理过程,等到下一个事件循环tick
中再去执行DOM
操作,它的原理,本质是利用事件循环的微任务队列实现异步更新。
当下一个tick
到来时,会执行flushSchedulerQueue
方法,它会拿到收集的queue
数组(这是一个watcher
的集合),并对数组依赖进行排序。为什么进行排序呢?源码中解释了三点:
- 组件创建是先父后子,所以组件的更新也是先父后子,因此需要保证父的渲染
watcher
优先于子的渲染watcher
更新。- 用户自定义的
watcher
,称为user watcher
。user watcher
和render watcher
执行也有先后,由于user watchers
比render watcher
要先创建,所以user watcher
要优先执行。- 如果一个组件在父组件的
watcher
执行阶段被销毁,那么它对应的watcher
执行都可以被跳过。
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// 对queue的watcher进行排序
queue.sort(function (a, b) { return a.id - b.id; });
// 循环执行queue.length,为了确保由于渲染时添加新的依赖导致queue的长度不断改变。
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
// 如果watcher定义了before的配置,则优先执行before方法
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
// 重置恢复状态,清空队列
resetSchedulerState();
// 视图改变后,调用其他钩子
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
flushSchedulerQueue
阶段,重要的过程可以总结为四点:
- 对
queue
中的watcher
进行排序,原因上面已经总结。- 遍历
watcher
,如果当前watcher
有before
配置,则执行before
方法,对应前面的渲染watcher
:在渲染watcher
实例化时,我们传递了before
函数,即在下个tick
更新视图前,会调用beforeUpdate
生命周期钩子。- 执行
watcher.run
进行修改的操作。- 重置恢复状态,这个阶段会将一些流程控制的状态变量恢复为初始值,并清空记录
watcher
的队列。
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
重点看看watcher.run()
的操作。
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if ( value !== this.value || isObject(value) || this.deep ) {
// 设置新值
var oldValue = this.value;
this.value = value;
// 针对user watcher,暂时不分析
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);
}
}
}
};
首先会执行watcher.prototype.get
的方法,得到数据变化后的当前值,之后会对新值做判断,如果判断满足条件,则执行cb
,cb
为实例化watcher
时传入的回调。
在分析get
方法前,回头看看watcher
构造函数的几个属性定义
var watcher = function Watcher(
vm, // 组件实例
expOrFn, // 执行函数
cb, // 回调
options, // 配置
isRenderWatcher // 是否为渲染watcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
// lazy为计算属性标志,当watcher为计算watcher时,不会理解执行get方法进行求值
this.value = this.lazy
? undefined
: this.get();
}
方法get
的定义如下:
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
···
} finally {
···
// 把Dep.target恢复到上一个状态,依赖收集过程完成
popTarget();
this.cleanupDeps();
}
return value
};
get
方法会执行this.getter
进行求值,在当前渲染watcher
的条件下,getter
会执行视图更新的操作。这一阶段会重新渲染页面组件
new Watcher(vm, updateComponent, noop, { before: () => {} }, true);
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
执行完getter
方法后,最后一步会进行依赖的清除,也就是cleanupDeps
的过程。
关于依赖清除的作用,我们列举一个场景: 我们经常会使用
v-if
来进行模板的切换,切换过程中会执行不同的模板渲染,如果A模板监听a数据,B模板监听b数据,当渲染模板B时,如果不进行旧依赖的清除,在B模板的场景下,a数据的变化同样会引起依赖的重新渲染更新,这会造成性能的浪费。因此旧依赖的清除在优化阶段是有必要。
// 依赖清除的过程
Watcher.prototype.cleanupDeps = function cleanupDeps () {
var i = this.deps.length;
while (i--) {
var dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
var tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
};
把上面分析的总结成依赖派发更新的最后两个点
- 执行
run
操作会执行getter
方法,也就是重新计算新值,针对渲染watcher
而言,会重新执行updateComponent
进行视图更新 - 重新计算
getter
后,会进行依赖的清除