今天也来写一下vue的响应式原理好让自己更加深刻理解整个过程。
其实大家都知道Vue采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter。在数据变动时发布消息给订阅者,然后触发相应回调。
这里先概括一下整个过程下来是怎样的,然后再一步步看Vue里面的代码是怎么实现这个过程。
在new Vue()里面,有一系列的初始化操作,其中有一个是initState,在initState这个方法里面包含了data,props,computed几个的初始化操作。我们现在先只管data,data的初始化工作是放在initData。initData将data里面的数据进行setter,getter劫持。
如果有多个订阅者想订阅这个属性变化怎么办呢,这个时候就需要一个存放订阅者的队列了。我们希望data里面的每个属性都维护好自己的订阅者队列。这个队列在Vue里面就是Dep啦,在劫持属性setter,getter的时候同时给这个属性添加了一个属于本身的订阅者队列。
看到上面发现观察者(劫持)有了,队列有了,只剩下关键的一点,怎么把订阅者放在队列里面。其实这里面往深层次想一下大概就知道答案肯定是订阅者自己把自己放到队列里面了,因为只有订阅者自己需要订阅哪一个属性。但是这里有一个问题就是队列维护在属性的观察者那里,作用域被隔离了。这里各自为了拿到对方的实例有点绕。概括一下就是,订阅者(Watcher类)实例化的时候会对通过Dep类的一个静态属性target来保存订阅者(Watcher实例)自身,然后对订阅的属性进行求值。我们从上面得知,属性的setter被劫持了,劫持做的东西就是调用队列(Dep实例)的depend方法。所以上面订阅者(Watcher实例)在对属性求值的时候会触发到这个depend方法,depend方法就会把队列(Dep实例)自身实例传回给订阅者(Watcher实例),这样订阅者(Watcher实例)就拿到了属性的订阅队列啦,这样就可以把订阅者(Watcher实例)自己放到队列(Dep实例)里面。到时候如果订阅属性发生变更的时候就可以直接从队列里面把所有订阅者拿出来触发回调了。
1 | /**src/core/instance/init.js **/ |
我们现在进去看看这个initState
1 | /**src/core/instance/state.js **/ |
好了,我们来到了关键的地方initData。
1 | /**src/core/instance/state.js **/ |
这里我们可以关注一下proxy这个方法。initData一开始就是将data挂到实例的_data
字段上面去。按照上面的例子来就是我们可以通过test._data.a
来访问,不过每次都这样访问有点麻烦,所以Vue就帮我们做了一层代理,让我们可以直接test.a
这样来访问。这里的做法很简单就是重新在实例上面重新定义这里这些属性。
重头戏来了,observe
方法。这里我们就略过一些代码直接看最重要的劫持属性getter,setter。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70/**src/core/observer/index.js **/
export class Observer {
....
....
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
....
....
}
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() //为当前属性建立自己的订阅队列
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
//如果对当前属性就行求值的时候发现有订阅者,就执行depend方法把订阅队列放回给订阅者
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
1 | /**src/core/observer/dep.js**/ |
walk
方法遍历data每个属性,执行defineReactive
方法,defineReactive
方法先为这个属性建立一个存放订阅者的队列(Dep实例),这里我们看一下Dep类的实现。劫持属性setter时做的就是执行Dep实例的depend方法。depend方法很简单,可以看上面的实现。这里的关键其实就是Dep.target跟this,看到Dep.target类型我们大概知道这是一个订阅者,但在哪里赋值的话我们先不管。然后就是这个this了,this是指向Dep实例的。这样我们就能顺利地把对订阅队列交给订阅者了。这样只需要调用订阅队列的addSub方法将自己放进去就行了。这里我们在Watcher里面看看实现就行了。
1 | /**src/core/observer/watcher.js**/ |
看Watcher的addDep
方法,这个方法就是将订阅者本身放到队列里面的实现。每一个订阅者都有自己的update方法,属性setter劫持就是调用队列里面每一个订阅者的update方法。
上面就是Vue的响应式原理了。这里比较绕的地方就是队列跟订阅者之间怎么互相传递自己的实例给对方使用而已。我们很简单的概括就是每个数据属性都有一个存放订阅自己的订阅者的队列,但是这个队列作用域与订阅者之间做了一个隔离,所以Vue就用了互相传递的方式将对方传给自己使用。就是通过一个类似全局的属性来存放当前的订阅者,然后属性的队列拿到这个订阅者之后将自己传入到这个订阅者里面,最后订阅者拿到了队列就又把自己放入队列中。
其实上面说了这么多,大家都发现这里面还缺乏了一个很关键的环境。就是怎么把订阅者跟Dep.target
关联起来呢。其实就很简单就是new Watcher的时候对订阅的属性进行求值,求值的过程当作顺便把Dep.target
关联到自身。求值的时候调用了Watcher类的get
方法,get
方法里面pushTarget(this)
就是做这件事情的。
最后给出一个大家都看过的例子,然后用这个例子结合来最后说明一下整个情况。1
2
3
4
5
6
7const test = new Vue({
data:{
a:1
}
})
new Watcher(test, "a", (value)=>console.log(value))
test.a = 2
initData阶段: 为a生成一个订阅者队列,对a的getter,setter进行劫持。getter做的事情就是将自己的订阅者队列暴露给外部的订阅者
new Watcher阶段:将实例复制给Dep.target,然后对a进行求值操作,触发getter方法,然后把实例放进去上面暴露出来的订阅者队列,将匿名回调方法放在update方法里面以进行统一调用
test.a = 2阶段:触发setter方法,调用队列的notify方法,通知队列里面所有订阅者数据发生了变化,然后循环队列里面的订阅者,执行他们的update方法实现回调。
这一篇就说明一下Vue响应式的基本原理。