最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。
前言
在上篇专栏中详解介绍了数据的监听过程。那么当监听到数据发生变化后又是如何通知视图更新,在 Vue 中是采用发布者订阅者这个设计模式来实现这个功能。其中发布者就是数据,订阅者就是 Watcher,另外还使用 Dep 来专门收集并管理订阅者,而订阅者还分为三种类型:render Watcher 、computed Watcher、user Watcher。本文主要介绍这三种订阅者是如何被收集。
一、render Watcher 渲染订阅者的收集
在这篇专栏中介绍过,当读取数据时会触发 getter 函数,在 getter 函数中收集订阅者。
那什么时候读取数据呢?还记着在这篇专栏中介绍过,在 Vue 的挂载过程中执行 mountComponent
函数有一段比较重要的逻辑,代码如下:
var updateComponent;
updateComponent = function() {
vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {
before: function before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */ );
执行 new Watcher
实例化 Watcher 类 ,得到一个实例化对象就是订阅者,称为 Watcher。先来看一下 Watcher 构造函数。
因为在 Vue 中 Watcher 分为 render Watcher 、computed Watcher、user Watcher 三种类型,所以在构造函数中要定义很多实例对象来实现这几种 Watcher 。
在这里先精简一下 Watcher 构造函数,只保留跟 render Watcher 和 Dep 相关的实例对象。
var uid = 0;
var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
if (options) {
this.deep = !!options.deep;
this.lazy = !!options.lazy;
this.before = options.before;
} else {
this.deep = this.lazy = false;
}
this.cb = cb;
this.id = ++uid;
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
}
this.value = this.lazy ? undefined : this.get();
};
- 参数
vm
:Vue 实例化对象。 - 参数
expOrFn
:要监听的数据,可为一个字符串表示要观察的数据路径,也可为一个函数结果返回要观察的数据。 - 参数
cb
:回调函数,当监听的数据发生变化时调用。 - 参数
options
:一些配置选项。 - 参数
isRenderWatcher
:为true
表示创建的 Watcher 是渲染 Watcher。
this.deps
和 this.newDeps
表示 Watcher 持有的 Dep 的数组集合,Dep 是用来专门收集并管理订阅者。那么这里为何需要有 2 个 Dep 的数组集合呢,稍后介绍。
this.depIds
和 this.newDepIds
分别代表 this.deps
和 this.newDeps
中 Dep 的标识符 id
的 Set 集合,Set 是 ES6 的数据结构,它类似于数组,但是成员的值都是唯一的,没有重复。
最后执行 this.value = this.lazy ? undefined : this.get()
,因在此场景中的 Watcher 构造函数的参数 options
里面没有 lazy
这个属性,故 this.lazy
为 false
,会执行 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) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
先执行 pushTarget(this)
,来看一下 pushTarget
函数,
Dep.target = null;
var targetStack = [];
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
在 pushTarget
函数中,Dep.target
是 Dep 静态属性,执行 Dep.target = target
把当前 Watcher 赋值到 Dep.target
,这样保证了同一时间只能有一个 Watcher 被收集。执行 targetStack.push(target)
把当前 Watcher 添加到 targetStack
数组中,其作用是为了恢复这个 Watcher。
在 popTarget
函数中,执行 targetStack.pop()
把当前 Watcher 移除出 targetStack
数组,说明当前 Watcher 已经收集完毕,执行 Dep.target = targetStack[targetStack.length - 1]
把上一个未被收集的 Watcher 重新赋值给Dep.target
。
targetStack
数组就像一个栈,保证 Watcher 的收集顺序,至于为什么这么做,后续遇到相关场景再介绍。
在 try 语句中执行 value = this.getter.call(vm, vm)
,先来看一下 this.getter
是什么,
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
}
可以看出 this.getter
的值是 Watcher 构造函数的参数 expOrFn
,那么 this.getter
的值是 function() { vm._update(vm._render(), hydrating); };
那么执行 this.getter.call(vm, vm)
,相当执行 vm._update(vm._render(), hydrating)
,则会先执行 vm._render()
。
回顾这篇专栏中介绍执行 vm._render()
生成 vnode 的过程中会去读取 data 中的数据,会触发数据的 getter 函数,在其中收集订阅者。而 getter 函数是在 defineReactive
函数中定义。
function defineReactive(obj, key, val, customSetter, shallow) {
var dep = new Dep();
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter(newVal) {
}
});
}
先执行 var dep = new Dep()
实例化一个 Dep 类,并赋值给常量 dep
。
在 getter 函数中 ,执行 if (Dep.target)
,此时 Dep.target
有值,值是当前 Wacther。故执行 dep.depend()
, dep.depend
是 Dep 的实例方法。先来看一下 Dep 构造函数
var uid = 0;
var Dep = function Dep() {
this.id = uid++;
this.subs = [];
};
Dep 构造函数非常简单,id
实例对象是 Dep 的标识符,每创建一个 Dep 就会自增加 1,subs
实例对象是个收集订阅者的容器。
再来看一下 dep.depend
实例方法。
Dep.prototype.depend = function depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
Dep.target
存在,故执行 Dep.target.addDep(this)
,addDep
是 Wachter 的一个实例方法,到这里可以得知 Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的,所以本文会在介绍 Wachter 中穿插介绍 Dep 。
来看一下 addDep
实例方法。
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)) {
dep.addSub(this);
}
}
}
此时参数 dep
是当前要订阅的发布者中的创建 Dep ,执行 var id = dep.id
,把 Dep 的实例对象 id
赋值给常量 id
,实例对象 id
是 Dep 的一个标识符,每个 Dep 的 id
都不相同。
执行 if (!this.newDepIds.has(id))
,其中 this.newDepIds
是 Dep 的标识符 id
的集合,是个 Set 数据结构, 故用 has
来判断 id
在this.newDepIds
中是不是已存在。
若不存在,执行 this.newDepIds.add(id)
将 id
添加到this.newDepIds
中。执行 this.newDeps.push(dep)
,其中 this.newDeps
是 Dep 的集合,把当前要订阅的发布者中的创建 Dep 添加到 this.newDeps
中。
执行 if (!this.depIds.has(id))
判断 id
在this.depIds
中是不是已存在,其中 this.depIds
也是一个 Dep 的标识符 id
的集合,和 this.newDepIds
有什么区别,放在后面介绍。若不存在,执行 dep.addSub(this)
,又回到 Dep 的实例方法addSub
中,此时this
是当前 Watcher 。
来看一下 Dep 的实例方法 addSub
。
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
}
执行 this.subs.push(sub)
来收集订阅者,其中参数 sub
是 Watcher ,也就是订阅者,this.subs
是收集订阅者的容器。
以上已经完成了一个订阅者收集的过程。那么到这里就结束了么,其实并没有,因为上述逻辑是在 try 语句中执行的,无论执行是否成功,最后还要执行 finally 语句里面逻辑。
finally {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
因在此场景中的 Watcher 构造函数的参数 options
里面没有 deep
这个属性,所以 this.deep
为 false
,不会执行 if 里面的代码。直接执行 popTarget()
,popTarget
函数的作用是把上一个未被收集的 Watcher 重新赋值给Dep.target
,具体实现在前面已介绍过。
那 Vue 为什么要这么设计,举个例子来说明。假设在渲染过程中触发了对订阅者A的收集,但在收集过程中又触发了对订阅者B的收集。而在 Vue 中为了保证通知订阅者执行更新的顺序,同一时间同一地点只能收集一个订阅者,所以很巧妙的利用栈的工作原理,实现了先对订阅者B的收集,当对订阅者B的收集完毕,再去收集订阅者A。
至于怎么利用栈的工作原理,在 Vue 中创建一个数组 targetStack
来存储那些 被触发收集 但 未被收集 完毕 的订阅者,利用 pushTarget
函数模拟入栈,利用 popTarget
函数模拟出栈,保证发布者收集的订阅者的顺序。
执行 this.cleanupDeps()
,来看一下 cleanupDeps
这个实例方法
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;
}
cleanupDeps
实例方法的作用是移除清除无用的发布者,也就是通知发布者移除订阅者。先来看一下实现逻辑,
考虑到 Vue 是数据驱动的,所以每次数据变化都会重新 render,那么 vm._render()
方法又会再次执行,并再次触发数据的 getter 进行收集订阅者,所以 Wathcer 在构造函数中会初始化 2 个 Dep 的数组集合,也就是订阅了那些发布者的集合,this.newDeps
表示新订阅的发布者集合,而 this.deps
表示上一次订阅的发布者集合。
会首先遍历 this.deps
,如果其中 Dep 的实例对象 id
在 this.newDepIds
中不存在,则执行 dep.removeSub(this)
, 移除订阅者对这个发布者的订阅。然后把 this.newDepIds
和 this.depIds
交换,this.newDeps
和 this.deps
交换,并把 this.newDepIds
和 this.newDeps
清空。
那为什么要清除订阅者,假设个场景来解释一下,在一个页面中,用 v-if
来控制模块A和模块B,当满足 v-if
的值为 true
,渲染模块A,会读取模块A中的数据,触发模块A中的数据收集这个页面的渲染 Watcher(订阅者)。如果这时 v-if
的值变为 false
,渲染模块B,会读取模块B中的数据,触发模块B中的数据收集这个页面的渲染 Watcher(订阅者)。那么此时修改模块A的数据,也会通知这个订阅者执行更新。虽然模块A的数据改变了,但是模块A已经不在页面展示,这样的更新显然是浪费的。
如果一个订阅者每次被收集时,把订阅者中记录的新订阅的发布者集合和上一次订阅的发布者集合进行比对,通知那些没被再次订阅的发布者调用 dep.removeSub
实例方法,清除这个订阅者。这样更新就不会造成任何浪费。
最后来看一下 dep.removeSub
实例方法
Dep.prototype.removeSub = function removeSub(sub) {
remove(this.subs, sub);
};
function remove(arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
dep.removeSub
实例方法很简单,调用 remove
把 sub
从 this.subs
这个订阅者容器中移除掉,sub
就是一个要移除的订阅者。
综上所述,在页面渲染过程中,在挂载阶段会创建一个渲染 Watcher (订阅者),在 Watcher 构造函数中最后执行 get
实例方法,在其中会调用 vm._render()
方法生成 vnode ,在其过程中会去读取 data 中的数据,就会触发数据的 getter 函数,在其中调用 Dep 的实例方法 depend
,在其中又调用 Watcher 的实例方法 addDep
先把发布者的收集器 Dep 存储在 Watcher 的 newDepIds
中,然后调用 Dep 的实例方法 addSub
收集这个订阅者。最后调用 Watcher 的实例方法 cleanupDeps
清除无用的发布者,在其中遍历上次订阅的发布者集合 deps
,通过这次订阅的发布者的标识 id
集合 newDepIds
来判断上次订阅的发布者是否在这次订阅的发布者集合 newDeps
中,若不在,调用 Dep 的实例方法 removeSub
去这个发布者中移除这个订阅者。清除完毕后,将 newDeps
和 deps
互换后,还有 newDepIds
和 depIds
互换后,再把 newDeps
和 newDepIds
清空,这就完成了订阅者的收集流程。
二、user Watcher 用户自定义订阅者的收集
用户自定义订阅者的收集流程和渲染订阅者的收集流程基本相似,只是用户自定义订阅者可以实现一些额外的功能,例如深度监听、立即回调、取消监听功能,故收集流程还是有些差异。下面来介绍一下 用户自定义订阅者的收集流程。
1、创建用户自定义订阅者的内部逻辑
用户自定义订阅者是通过选项 watch 或 vm.$watch
定义的 Watcher。先来看一下通过选项 watch 创建用户自定义订阅者的内部逻辑。
watch 选项是在 initState
函数中执行以下代码初始化。
function initState(vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
其中 opts.watch !== nativeWatch
这,是因为 Firefox 中有一个“监视”功能对象的原型 var nativeWatch = ({}).watch;
,所以要排除一下。再来看一下initWatch
函数。
function initWatch(vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
先回想官方文档中 watch 选项的用法
watch: {
//用法一
a: function(val, oldVal) {
},
//用法二
b: 'someMethod',
//用法三
c: {
handler: function(val, oldVal) { /* ... */ },
deep: true
},
//用法四
d: {
handler: 'someMethod',
immediate: true
},
//用法五
e: [
'handle1',
function handle2(val, oldVal) { /* ... */ },
{
handler: function handle3(val, oldVal) { /* ... */ },
/* ... */
}
],
}
回到 initWatch
函数中,遍历 watch 那么其每项 handler
可以是一个函数、一个对象、一个数组。如果 handler
是数组时要再遍历一遍,将每项调用 createWatcher
函数处理,若不是也要调用 createWatcher
函数处理。来看一下 createWatcher
函数。
function createWatcher(vm, expOrFn, handler, options) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
在 createWatcher
函数中最后执行 vm.$watch(expOrFn, handler, options)
,可知 user Watcher 是用 vm.$watch
实例方法创建的。
先来看一下 vm.$watch
实例方法的用法:vm.$watch( expOrFn, callback, [options] )
,其返回值是一个取消观察函数,用来停止触发回调。
expOrFn
要监听的数据,可为一个字符串表示要观察的数据路径,也可为一个函数结果返回要观察的数据。callback
当要观察的数据发生变化时的回调,可以是一个函数也可以是个对象。options
额外选项,如:deep
表示要对数据的值进行深度观察,immediate
表示立即用观察的数据的值触发回调函数。
那么在 createWatcher
函数中 handler
是回调函数的意思,再来看以下逻辑就很容易理解了
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
当 handler
是对象时,去对象的 handler 属性获取 handler
,再执行 if (typeof handler === 'string')
判断 handler
是不是字符串,若是字符串,那么 handler
是选项 methods 中的一个方法。
下面介绍一下 Vue 中怎么实现 vm.$watch
实例方法。
Vue.prototype.$watch = function(expOrFn, cb, options) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm,
("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn() {
watcher.teardown();
}
}
执行 if (isPlainObject(cb))
判断参数 cb
是否为对象,若是对象相当用选项 Watch 创建 user Watcher,故要执行 createWatcher(vm, expOrFn, cb, options)
创建 user Watcher并返回。
执行 options = options || {}; options.user = true;
处理一下参数 options
,并加上属性 user
并设置为 true
。
执行 var watcher = new Watcher(vm, expOrFn, cb, options)
,实例化一个 Watcher 类。
来看一下 Watcher 构造函数。先精简一下,只保留跟 user Watcher 和 Dep 相关的实例对象。
var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
vm._watchers.push(this);
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
} else {
this.deep = this.user = false;
}
this.cb = cb;
this.id = ++uid;
this.active = true;
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
this.expression = expOrFn.toString();
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
);
}
}
this.value = this.lazy ? undefined : this.get();
}
执行 vm._watchers.push(this)
缓存 Watcher,vm._watchers
是一个 Watcher 的集合。
此时参数 options
有值为 {user : true}
,执行 if 里面的逻辑 this.user = !!options.user
,故 Watcher 的实例对象 user
为 true
,这是 user Watcher 的标志。
参数 expOrFn
的含义是要监听的数据,也就要订阅的发布者,其值为函数时已经在渲染订阅者的介绍中讲过了,这里来介绍一下其值是字符串时,是怎么处理的。
当参数 expOrFn
是字符串时是代表数据路径,要用 parsePath
方法来解析获取数据,来看一下 parsePath
方法。
var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]"));
function parsePath(path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('.');
return function(obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) {
return
}
obj = obj[segments[i]];
}
return obj
}
}
例如参数 path
是 a.b.c
, 那么变量 segments
是 [a,b,c]
,最后返回一个函数,赋值给 this.getter
。
最后执行 this.value = this.lazy ? undefined : this.get()
,因在此场景中的 Watcher 构造函数的参数 options
里面没有 lazy
这个属性,所以 this.lazy
为 false
,会执行 this.get()
,在 get
实例方法中会执行value = this.getter.call(vm, vm)
。在场景中 this.getter
是由 parsePath
函数生成的,其值如下所示
function(obj) {
for (var i = 0; i < [a, b, c].length; i++) {
if (!obj) {
return
}
obj = obj[[a, b, c][i]];
}
return obj
}
那么参数 obj
为 vm
,即 Vue 类的实例化对象 this。遍历 [a, b, c]
循环执行 obj = obj[[a, b, c][i]]
,步骤如下所示
obj = this.a
obj = this.a.b
obj = this.a.b.c
那么最后 obj
的值为 this.a.b.c
并返回,又因在遍历中有去获取this.a
、this.a.b
、this.a.b.c
这些数据的值,会触发数据的描述符属性的 getter,在其中收集用户自定义的订阅者。其收集逻辑跟收集渲染订阅者的一样,就不重复介绍了。可以看到收集用户自定义订阅者的核心流程和收集渲染订阅者基本一样。
2、立即回调的实现
回到 vm.$watch
实例方法中,有以下这段逻辑。
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm,
("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
当参数 options
中有属性 immediate
且值为 true
,执行 cb.call(vm, watcher.value)
,这就是立即回调的实现逻辑。其中 watcher.value
就是通过 Watcher 的 get
实例方法求出来的要监听的数据的值。
3、取消监听功能的实现
回到 vm.$watch
实例方法中,有以下这段逻辑。
return function unwatchFn() {
watcher.teardown();
}
其中 watcher.teardown()
是实现取消监听功能的关键,teardown
是 Watcher 的实例方法,来看一下这个方法。
Watcher.prototype.teardown = function teardown() {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
var i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
};
取消监听功能,换句话来说就是移除该订阅者,故在 teardown
实例方法中做了两件事:
-
从全局 Wacther 集合
this.vm._watchers
中移除该订阅者。执行
this.active
,若this.active
的值为true
代表这个订阅者未被移除。执行
if (!this.vm._isBeingDestroyed)
,vm._isBeingDestroyed
是 Vue 实例是否被销毁的标志。为true
表示实例被销毁。若为false
,执行remove(this.vm._watchers, this)
,其中this.vm._watchers
是用 Vue 实例对象_watchers
来保存当前 Vue 实例下有多少个订阅者的集合,remove
函数作用是从数组中删除指定项。function remove(arr, item) { if (arr.length) { var index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1) } } }
-
去该订阅者订阅的发布者中移除该订阅者。
this.deps
是 Watcher 订阅者用来保存订阅了那些发布者的集合,遍历this.deps
执行this.deps[i].removeSub(this)
通知每个发布者调用 Dep 的实例方法removeSub
移除该订阅者。
4、深度监听的实现
在 Watcher 的实例方法 get
中实现深度监听功能,其主要逻辑如下
if (this.deep) {
traverse(value);
}
若参数 options
的属性 deep
的值为 true
,那么 this.deep
的值为 true
则执行 traverse(value)
,traverse
函数是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发子对象的 getter 函数,在其中收集订阅者,从而实现了深度监听,来看一下 traverse
函数。
var seenObjects = new Set();
function traverse(val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse(val, seen) {
var i, keys;
var isA = Array.isArray(val);
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
var depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return
}
seen.add(depId);
}
if (isA) {
i = val.length;
while (i--) {
_traverse(val[i], seen);
}
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) {
_traverse(val[keys[i]], seen);
}
}
}
执行 if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode)
,若 val
不是数组且不是对象,或者 val
是个冻结的对象,或者 VNode 类实例化对象。符合以上几种情况,因为 val
此时已经没有子对象或者子对象不值得遍历下去来触发其子对象的收集订阅者,同时也通过获取 val
来触发自身收集订阅者。
执行 if (val.__ob__)
, 判断 val
是否被监听过,若是,则执行 var depId = val.__ob__.dep.id
,获取子对象的订阅者收集器 Dep 的标识 dep.id
,那为什么能通过 val._ob_
来获取到 dep.id
,这是在监听数据时,会执行 observe(value)
,又在其中执行 new Observer(value)
,在其中执行 this.dep = new Dep(); def(value, '__ob__', this)
这样就把 dep
赋值到数据的 _ob_
属性上,具体介绍可以看这篇专栏。
执行 if (seen.has(depId))
判断参数 seen
是否有 depId
,有直接 return ,没有执行 seen.add(depId)
添加到参数 seen
。此逻辑是个优化,避免重复遍历子对象触发收集订阅者。参数 seen
的值是通过 var seenObjects = new Set()
赋值,其值是个 Set 数据结构。
执行if (isA)
判断 val
是否是数组,若是数组,就循环数据,将数组中的每一项都递归调用_traverse(val[i], seen)
,同时也通过获取 val[i]
来触发自身收集订阅者。
若是对象,执行 keys = Object.keys(val)
获取 val
的键集合keys
,然后循环 keys
中所有的key,递归调用 _traverse(val[keys[i]], seen)
,同时也通过获取 val[keys[i]]
来触发自身收集订阅者。
等 _traverse
递归调用完毕后,执行 seenObjects.clear()
,清除在此过程中保存的 dep.id
。
三、computed Watcher 计算属性订阅者的收集
计算属性订阅者的收集和其它订阅者收集的流程不一样,另外计算属性还实现了缓存功能。
计算属性 computed
是在 Vue 实例初始化中的 initState
函数中,执行了 if (opts.computed) initComputed(vm, opts.computed)
来初始化的,来看一下 initComputed
函数。
var computedWatcherOptions = {
lazy: true
};
function initComputed(vm, computed) {
var watchers = vm._computedWatchers = Object.create(null);
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
if (!isSSR) {
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
}
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}
执行 var watchers = vm._computedWatchers = Object.create(null)
创建一个空对象 vm._computedWatchers
来存储计算属性 Watcher 。
遍历选项 computed ,将每个计算属性赋值给 userDef
,执行 var getter = typeof userDef === 'function' ? userDef : userDef.get;
,因为在官网文档中介绍计算属性 computed 有两种用法
computed: {
// 仅读取
aDouble: function() {
return this.a * 2
},
// 读取和设置
aPlus: {
get: function() {
return this.a + 1
},
set: function(v) {
this.a = v - 1
}
}
}
所以先判断一下 userDef
是函数的话,直接赋值给 getter
,若不是函数那就是对象把 userDef.get
赋值给 getter
。这里的 getter
将作为 Watcher 构造函数的参数 expOrFn
的值,也称为计算属性的表达式。
执行 if (!isSSR)
在不是服务器渲染的场景下在执行 watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
,创建计算属性的 Watcher 并添加到 vm._computedWatchers
中。这里要注意 Watcher 构造函数的参数 cb
是个空值 noop,参数 options
的值是 { lazy: true }
。还是来看一下 Watcher 构造函数,先精简一下,只保留跟 computed Watcher 和 Dep 相关的实例对象。
var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
vm._watchers.push(this);
if (options) {
this.lazy = !!options.lazy;
}
this.cb = cb;
this.id = ++uid$2;
this.active = true;
this.dirty = this.lazy;
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.value = this.lazy ? undefined : this.get();
};
来看一下最后一句代码 this.value = this.lazy ? undefined : this.get()
,上面提到过参数 options
的值是 { lazy: true }
,故 this.lazy
的值 true
,那么执行不到 this.get()
,在收集渲染订阅者和用户自定义者的流程中,都是在 get
实例方法中触发数据的 getter 函数进行收集的。那么计算数据订阅者是在哪里收集?
回到 initComputed
函数中,执行 if (!(key in vm))
判断计算属性的 key 值有没有在 Vue 中定义过。若没有,则执行 defineComputed(vm, key, userDef)
。若有,判断计算属性的 key 是否已经被选项 data 或者选项 prop 的 key 所占用,如果是的话则在开发环境报相应的警告。下面来看一下 defineComputed
函数。
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function defineComputed(target, key, userDef) {
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);
}
defineComputed
函数是利用 Object.defineProperty
给计算属性添加 getter 和 setter,setter 通常是计算属性是一个对象时,并且拥有 set
方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分。
执行var shouldCache = !isServerRendering();
判断是不是在服务端渲染,此场景显然不是,故 shouldCache
为 false
,若 userDef
是个函数,执行 createComputedGetter(key)
,若 userDef
是个对象,userDef.cache
是个废弃的选项,在 Vue2中默认为 true
,故还是执行 createComputedGetter(key)
。来看一下 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
函数返回一个 getter 函数。每当使用计算属性时会触发这个 getter 函数。
执行 var watcher = this._computedWatchers && this._computedWatchers[key]
,其中 this._computedWatchers
时存储每个计算属性创建的 Wacther 的对象集合,其 key 就是计算属性。找到对应的 Wacther 赋值给常量 watcher
。
若 watcher
存在,执行 if (watcher.dirty)
,dirty
这个 Watcher 实例对象,是在 Watcher 构造函数中执行 this.dirty = this.lazy
赋值的。所以第一次执行 getter 函数时,watcher.dirty
为 true
,则执行 watcher.evaluate()
。来看一下 evaluate
这个 Watcher 实例方法。
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
}
执行 this.value = this.get()
,调用 this.get()
, 在 get
这个 Wacther 的实例方法中会执行计算属性的表达式,在执行中会访问表达式中的数据,就会触发数据的 getter 函数,开始收集计算属性订阅者,这里的收集流程就和渲染订阅者的收集流程一模一样。this.get()
执行完会返回一个值,就是计算属性的表达式计算结果赋值给 this.value
,最后把 this.dirty
置为 false
,当下次再使用计算属性时,由于 this.dirty
为 false
,就不会执行 watcher.evaluate()
,直接返回 watcher.value
,这就是计算属性的缓存功能的实现逻辑了。当计算属性的表达式中的数据发生变化后,会通知计算属性 Watcher 把 this.dirty
置为 true
,当再次使用计算属性时,会执行 watcher.evaluate()
,重新执行计算属性的表达式,返回计算属性的新值,这个会在后续专栏详细介绍。
执行 if (Dep.target) { watcher.depend()}
。因为在执行 this.get()
中计算属性订阅者收集完毕后,执行 popTarget()
把上一个订阅者(渲染订阅者)出栈,然后赋值给 Dep.target
。所以此时 Dep.target
是渲染订阅者。
来看一下 watcher.depend
实例方法
Watcher.prototype.depend = function depend() {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
这里用一个实例来介绍一下,比如计算属性 a
是如下这样定义的。
<template>
<div>{{a}}</div>
</template>
<script>
export default {
data(){
return{
b:1,
c:2
}
},
computed:{
a:function(){
return this.b + this.c
}
}
}
</script>
那么此时渲染模板时读取到 a
这个计算属性时是如何收集渲染订阅者。是构成这个计算属性的值的 this.b
和 this.c
去收集这个渲染订阅者。这是因为计算属性 a
是由 this.b
或 this.c
发生变化引起的变化。
由于 this.b
或 this.c
先收集了计算属性订阅者,故在计算属性订阅者的 this.deps
包含者 this.b
或 this.c
的依赖收集器。遍历执行 this.deps[i].depend()
,触发this.b
和 this.c
收集渲染订阅者。
四、后续
本文详细介绍了三种类型的订阅者的收集流程,下一篇专栏准备介绍发布者发生变化后,如何通知订阅者执行更新。另外 Watcher 还要很多实例对象和实例用法也将会在其中穿插介绍。尽情期待。谢谢。