/* @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其实是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。
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)
}
}
之前在解析
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设计模式与开发实践》是专门对
发布-订阅模式
这一块进行了精读,精读部分内容可见我写的精读部分的文档:发布-订阅模式一个简单的
发布-订阅模式
的实现主要是以下三点内容:以下为一个售楼处-短信通知的简版发布订阅模式的实现:
我们在这里使用了一个全局的
Event
对象来代理我们所有的发布-订阅模型
。Dep模块
Dep模块的位置在
src/core/observer/dep.js
,主要作用是收集订阅者的容器。看以下代码:
其中
Dep.target.addDep(this)
在Watcher
模块,作用为添加依赖:Dep
定义了发布者的模型,在整个应用中使用唯一的id
对其实例进行标识。Dep
的订阅者独自形成一个订阅者队列subs
。Dep
通过addSub
与removeSub
方法添加和移除订阅者。Dep
通过notify
通知订阅者数据更新。这个更新对于对象来说是通过setter
完成,对于数组,因为其length
属性不可configurable
并且不可enumerable
以及writable
。因此Vue
使用变异方法
更新数据以确保能正常notify
。当数据的
getter
触发后,会收集依赖,但也不是所有的触发方式都会收集依赖,只有通过watcher
触发的getter
会收集依赖,而所谓的被收集的依赖就是当前watcher
,DOM
中的数据必须通过watcher
来绑定,只通过watcher
来读取。如何收集依赖?
看以下代码:
在计算属性处理完成后,会发现在
vm
下挂载了一个key
为model
的属性。在
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
类构造器:getter
在
vm
初始化时getter
。traverse
表示深度订阅,设置VM.$watch
第三个参数为{ deep: true }
。更新数据
更深的理解?
Vue
实例初始化过程中,将所有计算属性包装为lazy watcher
;watcher
为dirty
,此时开始计算此watcher
的值(dirty
表示数据是脏的,必须计算一次);watcher
将被设置为依赖目标(Dep.target.addDep(this)
),开始依赖收集;watcher
值的过程中,被访问到属性的getter
中会是检查是否存在依赖目标,若存在依赖目标就创建依赖关系;watcher
的值计算完成后,新的依赖将被设置,旧的依赖会被删除,依赖收集完成。watcher
被设置为dirty
(提醒watcher
又该更新了);