为了深刻介绍响应式系统的内部实现原理,咱们花了一整节的篇幅介绍了数据(包括
data, computed,props
)如何初始化成为响应式对象的过程。有了响应式数据对象的知识,上一节的后半部分咱们还在保留源码结构的基础上构建了一个以data
为数据的响应式系统,而这一节,咱们继续深刻响应式系统内部构建的细节,详细分析Vue
在响应式系统中对data,computed
的处理。node
在构建简易式响应式系统的时候,咱们引出了几个重要的概念,他们都是响应式原理设计的核心,咱们先简单回顾一下:react
Observer
类,实例化一个Observer
类会经过Object.defineProperty
对数据的getter,setter
方法进行改写,在getter
阶段进行依赖的收集,在数据发生更新阶段,触发setter
方法进行依赖的更新watcher
类,实例化watcher
类至关于建立一个依赖,简单的理解是数据在哪里被使用就须要产生了一个依赖。当数据发生改变时,会通知到每一个依赖进行更新,前面提到的渲染wathcer
即是渲染dom
时使用数据产生的依赖。Dep
类,既然watcher
理解为每一个数据须要监听的依赖,那么对这些依赖的收集和通知则须要另外一个类来管理,这个类即是Dep
,Dep
须要作的只有两件事,收集依赖和派发更新依赖。这是响应式系统构建的三个基本核心概念,也是这一节的基础,若是尚未印象,请先回顾上一节对极简风响应式系统的构建。算法
在开始分析data
以前,咱们先抛出几个问题让读者思考,而答案都包含在接下来内容分析中。express
前面已经知道,Dep
是做为管理依赖的容器,那么这个容器在何时产生?也就是实例化Dep
发生在何时?数组
Dep
收集了什么类型的依赖?即watcher
做为依赖的分类有哪些,分别是什么场景,以及区别在哪里?缓存
Observer
这个类具体对getter,setter
方法作了哪些事情?数据结构
手写的watcher
和页面数据渲染监听的watch
若是同时监听到数据的变化,优先级怎么排?dom
有了依赖的收集是否是还有依赖的解除,依赖解除的意义在哪里?异步
带着这几个问题,咱们开始对data
的响应式细节展开分析。async
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
若是遇到属性值为对象时,会为该对象的每一个值收集依赖这句话也很好理解,若是咱们将一个值为基本类型的响应式数据改变成一个对象,此时新增对象里的属性,也须要设置成响应式数据。
通俗的总结一下依赖收集的过程,每一个数据就是一个依赖管理器,而每一个使用数据的地方就是一个依赖。当访问到数据时,会将当前访问的场景做为一个依赖收集到依赖管理器中,同时也会为这个场景的依赖收集拥有的数据。
在分析依赖收集的过程当中,可能会有很多困惑,为何要维护这么多的关系?在数据更新时,这些关系会起到什么做用?带着疑惑,咱们来看看派发更新的过程。 在数据发生改变时,会执行定义好的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
后,会进行依赖的清除计算属性设计的初衷是用于简单运算的,毕竟在模板中放入太多的逻辑会让模板太重且难以维护。在分析computed
时,咱们依旧遵循依赖收集和派发更新两个过程进行分析。
computed
的初始化过程,会遍历computed
的每个属性值,并为每个属性实例化一个computed watcher
,其中{ lazy: true}
是computed watcher
的标志,最终会调用defineComputed
将数据设置为响应式数据,对应源码以下:
function initComputed() {
···
for(var key in computed) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
}
// computed watcher的标志,lazy属性为true
var computedWatcherOptions = { lazy: true };
复制代码
defineComputed
的逻辑和分析data
的逻辑类似,最终调用Object.defineProperty
进行数据拦截。具体的定义以下:
function defineComputed (target,key,userDef) {
// 非服务端渲染会对getter进行缓存
var shouldCache = !isServerRendering();
if (typeof userDef === 'function') {
//
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码
在非服务端渲染的情形,计算属性的计算结果会被缓存,缓存的意义在于,只有在相关响应式数据发生变化时,computed
才会从新求值,其他状况屡次访问计算属性的值都会返回以前计算的结果,这就是缓存的优化,computed
属性有两种写法,一种是函数,另外一种是对象,其中对象的写法须要提供getter
和setter
方法。
当访问到computed
属性时,会触发getter
方法进行依赖收集,看看createComputedGetter
的实现。
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
复制代码
createComputedGetter
返回的函数在执行过程当中会先拿到属性的computed watcher
,dirty
是标志是否已经执行过计算结果,若是执行过则不会执行watcher.evaluate
重复计算,这也是缓存的原理。
Watcher.prototype.evaluate = function evaluate () {
// 对于计算属性而言 evaluate的做用是执行计算回调
this.value = this.get();
this.dirty = false;
};
复制代码
get
方法前面介绍过,会调用实例化watcher
时传递的执行函数,在computer watcher
的场景下,执行函数是计算属性的计算函数,他能够是一个函数,也能够是对象的getter
方法。
列举一个场景避免和
data
的处理脱节,computed
在计算阶段,若是访问到data
数据的属性值,会触发data
数据的getter
方法进行依赖收集,根据前面分析,data
的Dep
收集器会将当前watcher
做为依赖进行收集,而这个watcher
就是computed watcher
,而且会为当前的watcher
添加访问的数据Dep
回到计算执行函数的this.get()
方法,getter
执行完成后一样会进行依赖的清除,原理和目的参考data
阶段的分析。get
执行完毕后会进入watcher.depend
进行依赖的收集。收集过程和data
一致,将当前的computed watcher
做为依赖收集到数据的依赖收集器Dep
中。
这就是computed
依赖收集的完整过程,对比data
的依赖收集,computed
会对运算的结果进行缓存,避免重复执行运算过程。
派发更新的条件是data
中数据发生改变,因此大部分的逻辑和分析data
时一致,咱们作一个总结。
Dep
收集过computed watch
这个依赖,因此会调用dep
的notify
方法,对依赖进行状态更新。computed watcher
和以前介绍的watcher
不一样,它不会马上执行依赖的更新操做,而是经过一个dirty
进行标记。咱们再回头看依赖更新
的代码。Dep.prototype.notify = function() {
···
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
Watcher.prototype.update = function update () {
// 计算属性分支
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
复制代码
因为lazy
属性的存在,update
过程不会执行状态更新的操做,只会将dirty
标记为true
。
data
数据拥有渲染watcher
这个依赖,因此同时会执行updateComponent
进行视图从新渲染,而render
过程当中会访问到计算属性,此时因为this.dirty
值为true
,又会对计算属性从新求值。咱们在上一节的理论基础上深刻分析了Vue
如何利用data,computed
构建响应式系统。响应式系统的核心是利用Object.defineProperty
对数据的getter,setter
进行拦截处理,处理的核心是在访问数据时对数据所在场景的依赖进行收集,在数据发生更改时,通知收集过的依赖进行更新。这一节咱们详细的介绍了data,computed
对响应式的处理,二者处理逻辑存在很大的类似性但却各有的特性。源码中会computed
的计算结果进行缓存,避免了在多个地方使用时频繁重复计算的问题。因为篇幅有限,对于用户自定义的watcher
咱们会放到下一小节分析。文章还留有一个疑惑,依赖收集时若是遇到的数据是数组时应该怎么处理,这些疑惑都会在以后的文章一一解开。