在Vue源码中,数据的响应式原理是一个重点。此外,vue使用了观察者模式,在源码中体现为
- Observer:发布者
- Dep:依赖,联系发布者与订阅者的纽带
- Watcher:订阅者
其中,Observer是发布者,用于监视数据的读写对应的index.js文件中,实现了数据劫持。而数据劫持的关键,在于使用ES5的Object.defineProperty()重写属性的getter/setter:
- getter中实现依赖收集
所谓依赖,就是vue会在组件渲染的过程中把“接触”(也就是读取数据,触发其get函数)过的数据属性记录为依赖(此为依赖收集),方便后续组件的更新。
// index.js
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// getA发生的时候,Dep.target == DepM
dep.depend() // 这一句为关键
if (childOb) {
childOb.dep.depend()
}
if (Array.isArray(value)) {
dependArray(value)
}
}
return val
},
// dep.js
/**
* Dep类中函数depend,调用Dep.target中存放的Watcher实例的addDep方法
*/
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// watcher.js
/**
* 添加依赖,即订阅Dep,同时让Dep知道该Watcher订阅着它
* @param {Dep} dep
*/
addDep (dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep) // WatcherM.deps.push(DepA) // WatcherM.deps = [DepA, DepB]
if (!this.depIds.has(id)) {
// 收集订阅者Watcher
dep.addSub(this) // DepA.subs.push(WatcherM) // DepA.subs = [WatcherM, WatcherN, WatcherX]
}
}
}
- setter中实现分发更新
当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染(此为分发更新)。
set: function reactiveSetter(newVal) {
const value = val
if (newVal === value) {
return
}
// console.log("newVal = ", newVal)
val = newVal
childOb = observe(newVal)
// 通过dep依赖告知更新
dep.notify() // 这一句为关键
// vm._update at core/instance/index.js
}
// dep.js
notify () {
// 广播,当劫持到数据变更时,通知订阅者Watcher进行update
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
// watcher.js
update () {
if (this.lazy) {
this.dirty = true
} else {
this.run()
}
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value)
) {
// set new value
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
}
如下为Observer/index.js中的代码,是Observer类的构造函数的一个代码片段:
this.value = value
this.dep = new Dep()
// 把当前Observer对象 绑定在value.__ob__上,作为该数据对象已经被Observer观察的标识
// this代指Observer类
def(value, '__ob__', this)
该Observer实例对象被绑定到了vue实例上,并作为属性‘ob‘,我们还可以发现,该Observer实例对象还有成员dep(依赖对象实例),因此我们再到dep.js文件中看看Dep类。
constructor () {
this.id = uid++
/**subs: Watcher[],依赖列表,用于收集订阅者Watcher */
this.subs = []
}
上面是Dep类的构造函数,由于依赖项需要通知Watcher更新组件,那么就需要记录与其绑定的Watcher,这里的成员id是该dep的唯一区分标识,而subs用于记录与其相关的订阅者Watcher。
我们自己简单地写一个数据响应式:
// 订阅者Watcher类
class Watcher {
constructor(name) {
this.name = name
}
// 值变化时调用的方法
update(str) {
console.log(`${this.name}发生了update.`);
this.name = str
console.log(this.name);
}
}
// 发布者Dep类
class Dep {
constructor() {
this.subs = []
}
// 添加Watcher
addSub(watcher) {
this.subs.push(watcher)
}
// 遍历订阅者列表中的所有订阅者
notify(str) {
this.subs.forEach(item => {
// item是订阅者Watcher
item.update(str)
})
}
}
const w1 = new Watcher('张三')
const w2 = new Watcher('李四')
const w3 = new Watcher('王五')
const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
dep.addSub(w3)
dep.notify('赵六')
- 在上面这个简单的栗子中,我们定义了三个订阅者w1, w2, w3,一个发布者dep
- 对于订阅者,我们订阅(监视)的是name属性
- 对于发布者Dep,我们可以向其订阅者列表中添加订阅者w1, w2, w3,dep.addSub()
- 一旦发布者有新内容要发布,就会通知(notify)其订阅者新内容(订阅对象)
- 上面的通知notify也就是遍历订阅者列表,调用每一个订阅者自身的update()更新方法
上面的逻辑初步体现了vue中的数据响应式逻辑,但是还有很多细节,包括Dep何时调用notify()方法,还需要根据源码进一步分析。