共计 12917 个字符,预计需要花费 33 分钟才能阅读完成。
为了深刻介绍响应式零碎的外部实现原理,咱们花了一整节的篇幅介绍了数据 (包含
data, computed,props
) 如何初始化成为响应式对象的过程。有了响应式数据对象的常识,上一节的后半局部咱们还在保留源码构造的根底上构建了一个以data
为数据的响应式零碎,而这一节,咱们持续深刻响应式零碎外部构建的细节,详细分析Vue
在响应式零碎中对data,computed
的解决。
7.8 相干概念
在构建繁难式响应式零碎的时候,咱们引出了几个重要的概念,他们都是响应式原理设计的外围,咱们先简略回顾一下:
Observer
类,实例化一个Observer
类会通过Object.defineProperty
对数据的getter,setter
办法进行改写,在getter
阶段进行 依赖的收集 , 在数据产生更新阶段,触发setter
办法进行 依赖的更新watcher
类,实例化watcher
类相当于创立一个依赖,简略的了解是数据在哪里被应用就须要产生了一个依赖。当数据产生扭转时,会告诉到每个依赖进行更新,后面提到的渲染wathcer
便是渲染dom
时应用数据产生的依赖。Dep
类,既然watcher
了解为每个数据须要监听的依赖,那么对这些依赖的收集和告诉则须要另一个类来治理,这个类便是Dep
,Dep
须要做的只有两件事,收集依赖和派发更新依赖。
这是响应式零碎构建的三个根本外围概念,也是这一节的根底,如果还没有印象,请先回顾上一节对 极简风响应式零碎的构建。
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
后,会进行依赖的革除
7.10 computed
计算属性设计的初衷是用于简略运算的,毕竟在模板中放入太多的逻辑会让模板过重且难以保护。在剖析 computed
时,咱们仍旧遵循依赖收集和派发更新两个过程进行剖析。
7.10.1 依赖收集
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
会对运算的后果进行缓存,防止反复执行运算过程。
7.10.2 派发更新
派发更新的条件是 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
, 又会对计算属性从新求值。
7.11 小结
咱们在上一节的实践根底上深入分析了 Vue
如何利用 data,computed
构建响应式零碎。响应式零碎的外围是利用 Object.defineProperty
对数据的 getter,setter
进行拦挡解决,解决的外围是在拜访数据时对数据所在场景的依赖进行收集,在数据产生更改时,告诉收集过的依赖进行更新。这一节咱们具体的介绍了 data,computed
对响应式的解决,两者解决逻辑存在很大的相似性但却各有的个性。源码中会 computed
的计算结果进行缓存,防止了在多个中央应用时频繁反复计算的问题。因为篇幅无限,对于用户自定义的 watcher
咱们会放到下一大节剖析。文章还留有一个纳闷,依赖收集时如果遇到的数据是数组时应该怎么解决,这些纳闷都会在之后的文章一一解开。