xingbofeng.github.io
xingbofeng.github.io copied to clipboard
Vue源码学习笔记之Dep和Watcher
之前在解析Vue
源码的过程中专门提到了Observe
模块的变异方法与相应式原理。但是对于相应式的通知视图更新这一块儿只是专门提了通过Dep
与Watcher
这样一个发布-订阅模式来进行通知。
恰好最近刚刚学习了发布-订阅模式的原理,正巧利用这样一段时间来进行Vue的专门的发布-订阅模块的源码学习。
先看一下Vue
文档对于深入相应式原理的剖析:
在初始化的时候,首先通过 Object.defineProperty
改写 getter/setter
为 Data
注入观察者能力,在数据被调用的时候,getter
函数触发,调用方(会为调用方创建一个 Watcher
)将会被加入到数据的订阅者序列,当数据被改写的时候,setter
函数触发,变更将会通知到订阅者(Watcher
)序列中,并由 Watcher
触发 re-render
,后续的事情就是通过 render function code
生成虚拟 DOM
,进行 diff
比对,将不同反应到真实的 DOM
中。
发布-订阅模式
最近在阅读《JavaScript设计模式与开发实践》是专门对发布-订阅模式
这一块进行了精读,精读部分内容可见我写的精读部分的文档:发布-订阅模式
一个简单的发布-订阅模式
的实现主要是以下三点内容:
- 指定好发布者;
- 发布者有一个缓存列表,里面存放了回调函数,以便发布后通知订阅者;
- 发布消息的时候遍历缓存列表,依次触发订阅者的回调;
以下为一个售楼处-短信通知的简版发布订阅模式的实现:
var event = {
clientList: [],
listen: function(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
// 订阅的消息添加进缓存列表
this.clientList[key].push(fn);
},
trigger: function() {
var key = Array.prototype.shift.call(arguments);
var fns = this.clientList[key];
if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments); // (2) // arguments 是 trigger 时带上的参数
}
}
};
// **再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能**
var installEvent = function(obj) {
for (var i in event) {
obj[i] = event[i];
}
};
var salesOffices = {}; // 定义发布者(售楼处)
installEvent(salesOffices);
// 小明订阅消息
salesOffices.listen('squareMeter88', function(price) {
console.log('价格= ' + price);
});
// 小红订阅消息
salesOffices.listen('squareMeter100', function(price) {
console.log('价格= ' + price);
});
salesOffices.trigger('squareMeter88', 2000000); // 输出:2000000
salesOffices.trigger('squareMeter100', 3000000); // 输出:3000000
我们在这里使用了一个全局的Event
对象来代理我们所有的发布-订阅模型
。
Dep模块
Dep模块的位置在src/core/observer/dep.js
,主要作用是收集订阅者的容器。
看以下代码:
/* @flow */
// 首先引入watcher模块,用于通知观察者
import type Watcher from './watcher'
import { remove } from '../util/index'
// 闭包定义一个唯一的ID,这里在上边也说了,我们需要保持每次都要通知到指定的通知者,因此用唯一ID标示
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher; // 一个订阅者
id: number; // 定义Dep的唯一id作为标示
subs: Array<Watcher>; // 维护一个观察者队列,一旦数据发生改变通知所有观察者
constructor () {
this.id = uid++ // 定义发布者的唯一ID
this.subs = [] // 观察者队列
}
// 添加观察者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除观察者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// Dep.target变量存的是一个订阅者对象
// 一旦其发布者发布过数据,通知订阅者!
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知订阅者,数据更新啦(调用订阅者的update方法)
notify () {
// stabilize the subscriber list first
// 保证是一个Array,这里就是订阅者的队列
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// targetStack定义一个栈,用于收集依赖
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
其中Dep.target.addDep(this)
在Watcher
模块,作用为添加依赖:
addDep (dep: Dep) {
const 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
定义了发布者的模型,在整个应用中使用唯一的id
对其实例进行标识。 -
Dep
的订阅者独自形成一个订阅者队列subs
。Dep
通过addSub
与removeSub
方法添加和移除订阅者。 -
Dep
通过notify
通知订阅者数据更新。这个更新对于对象来说是通过setter
完成,对于数组,因为其length
属性不可configurable
并且不可enumerable
以及writable
。因此Vue
使用变异方法
更新数据以确保能正常notify
。 -
当数据的
getter
触发后,会收集依赖,但也不是所有的触发方式都会收集依赖,只有通过watcher
触发的getter
会收集依赖,而所谓的被收集的依赖就是当前watcher
,DOM
中的数据必须通过watcher
来绑定,只通过watcher
来读取。
如何收集依赖?
看以下代码:
new Vue({
template: 'computed',
data: {
raw: 1
},
computed: {
model: function() {
return this.raw + 1;
},
},
watch: {
model: function() {
console.log('the computed');
},
},
});
在计算属性处理完成后,会发现在vm
下挂载了一个key
为model
的属性。
vm.model = function() {
return this.raw + 1;
}
在vm
挂载的过程中就已经触发了一次getter
便收集了一次依赖!
收集依赖的理解
-
Dep
其实是dependence
依赖的缩写,举个例子,我们的一个模板{{ a + b }}
,我们会说他的依赖有a
和b
,其实就是依赖了data
的a
和b
属性,更精确的说是依赖了a
属性中闭包的dep实例
和b
属性中闭包的那个dep实例
。 -
详细来说:我们的这个
{{ a + b }}
在DOM
里最终会被a + b
表达式的真实值所取代,所以存在一个求出这个a+b
的表达式的过程,求值的过程就会自然的分别触发a
和b
的getter
,而在getter
中,我们看到执行了dep.depend()
,这个函数实际上会做dep.addSub(Dep.target)
,即在dep的订阅者数组中存放了Dep.target
,让Dep.target
订阅dep
。 -
那
Dep.target
是什么?他就是我们后面介绍的Watcher
实例,为什么要放在Dep.target
里呢?是因为getter
函数并不能传参,dep
可以通过闭包的形式放进去,那watcher
可就不行了,watcher
内部存放了a + b
这个表达式,也是由watcher
计算a + b
的值,在计算前他会把自己放在一个公开的地方(Dep.target
),然后计算a + b
,从而触发表达式中所有遇到的依赖的getter
,这些getter
执行过程中会把Dep.target
加到自己的订阅列表中。等整个表达式计算成功,Dep.target
又恢复为null
.这样就成功的让watcher
分发到了对应的依赖的订阅者列表中,订阅到了自己的所有依赖。
还是不理解Dep?看Watcher
在Vue 2.4.2
版本中,Watcher
模块位于src/core/observer/watcher.js
。
Watcher
可以先暂时理解为房产中介用户买房子找中介,中介帮忙找房主,房主卖房子找中介,中介帮房主把房子卖给用户。
setter
触发消息到Watcher
,watcher
帮忙告诉Directive
更新DOM
,DOM
中修改了数据也会通知给Watcher
,Watcher
帮忙修改数据。
先来看Watcher
类构造器:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
// lazy watcher是在计算属性里用到的,Vue在初始化时会封装你的计算属性的getter,
// 并在里面闭包了一个新创建的lazy watcher
// 而指令bind函数中创建的那个并不是lazy watcher,即使这个指令是绑定到一个计算属性上的,请注意区分
// lazy不会像一般的指令的watcher那样在这个watcher构造函数里计算初始值(this.value)
// 而计算属性的getter里写有了逻辑,如果他的lazy watcher的dirty是false,
// 就拿出之前计算过的值返回给你(dirty的意思表示是数据的依赖有变化,你需要重新计算)
// 否则就会使用Watcher.prototype.evaluate完成求值,
// 一旦指定lazy为true,那么这个数据就肯定是dirty的
// 因此初始化时,是从没有计算过的,数据是undefined,并非正确的值,因此肯定需要计算,所以this.dirty = this.lazy
this.dirty = this.lazy // for lazy watchers
// 用deps存储当前的依赖,而新一轮的依赖收集过程中收集到的依赖则会放到newDeps中
// 之所以要用一个新的数组存放新的依赖是因为当依赖变动之后,
// 比如由依赖a和b变成依赖a和c
// 那么需要把原先的依赖订阅清除掉,也就是从b的subs数组中移除当前watcher,因为我已经不想监听b的变动
// 所以我需要比对deps和newDeps,找出那些不再依赖的dep,然后dep.removeSub(当前watcher),这一步在afterGet中完成
this.deps = []
this.newDeps = []
// 这两个set是用来提升比对过程的效率,不用set的话,判断deps中的一个dep是否在newDeps中的复杂度是O(n)
// 改用set来判断的话,就是O(1)
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
// 对于计算属性computed来说,就会进入到这里
this.getter = expOrFn
} else {
// 使用parsePath初始化getter
// 把expression解析为一个对象,对象的get/set属性存放了获取/设置的函数
// 比如hello解析的get函数为function(scope) {return scope.hello;}
this.getter = parsePath(expOrFn)
// 表达式解析错误的错误处理
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// 如果设定lazy,不会立刻调用
this.value = this.lazy
? undefined
: this.get()
}
getter
在vm
初始化时getter
。
get () {
pushTarget(this) // 做依赖收集
let value
const vm = this.vm
try {
// 调用其getter方法,初始化就调用
value = this.getter.call(vm, vm)
} catch (e) {
// 表达式错误
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value) // 如果深度订阅,递归观察对象变化
}
// watcher的值计算完成后,新的依赖将被设置,旧的依赖会被删除,依赖收集完成。
popTarget()
this.cleanupDeps()
}
return value
}
traverse
表示深度订阅,设置VM.$watch
第三个参数为{ deep: true }
。
const seenObjects = new Set()
function traverse (val: any) {
seenObjects.clear()
_traverse(val, seenObjects)
}
function _traverse (val: any, seen: ISet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
return
}
// 如果当前值有Observer
if (val.__ob__) {
// 拿到当前值的Observer的订阅者管理员的id
const depId = val.__ob__.dep.id
// 如果seen中已经有这个id了(已经被订阅),直接返回
if (seen.has(depId)) {
return
}
// 否则添加到seen中(订阅它)
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)
}
}
更新数据
update () {
/* istanbul ignore else */
// lazy模式下,标记下当前是脏的就可以了
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 同步模式下直接更新
this.run()
} else {
// 否则异步更新(暂时不展开,可见scheduler模块)
queueWatcher(this)
}
}
更深的理解?
-
Vue
实例初始化过程中,将所有计算属性包装为lazy watcher
; - 首次访问计算属性时,
watcher
为dirty
,此时开始计算此watcher
的值(dirty
表示数据是脏的,必须计算一次); - 计算开始之前,此
watcher
将被设置为依赖目标(Dep.target.addDep(this)
),开始依赖收集; - 计算
watcher
值的过程中,被访问到属性的getter
中会是检查是否存在依赖目标,若存在依赖目标就创建依赖关系; -
watcher
的值计算完成后,新的依赖将被设置,旧的依赖会被删除,依赖收集完成。 - 当依赖属性更新时,会通知自身的依赖目标,
watcher
被设置为dirty
(提醒watcher
又该更新了); - 再次访问该计算属性,重复计算及依赖收集步骤。