为了深刻介绍响应式零碎的外部实现原理,咱们花了一整节的篇幅介绍了数据(包含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)}// observefunction 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属性为truevar 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
咱们会放到下一大节剖析。文章还留有一个纳闷,依赖收集时如果遇到的数据是数组时应该怎么解决,这些纳闷都会在之后的文章一一解开。