Vue源码解读-数据的双向绑定简陋版(仅用于原理说明)

本文通过简化Vue源码,从复杂的Vue源码中抽取数据双向绑定部分的核心进行简化,以便于理解Vue是如何实现数据双向绑定的,其中使用了
ES5的Object.defineProperty()
发布-订阅模式

监听数据变化

  • 对data进行改造,所有属性设置set&get,用于在属性获取或者设置时,添加逻辑
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
// Dep用于订阅者的存储和收集,将在下面实现
import Dep from 'Dep'
// Observer类用于给data属性添加set&get方法
export default class Observer{
constructor(value){
this.value = value
this.walk(value)
}
walk(value){
Object.keys(value).forEach(key => this.convert(key, value[key]))
}
convert(key, val){
defineReactive(this.value, key, val)
}
}
export function defineReactive(obj, key, val){
var dep = new Dep()
// 给当前属性的值添加监听
var chlidOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=> {
console.log('get value')
// 如果Dep类存在target属性,将其添加到dep实例的subs数组中
// target指向一个Watcher实例,每个Watcher都是一个订阅者
// Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
// 此处的问题是:并不是每次Dep.target有值时都需要添加到订阅者管理员中去管理,需要对订阅者去重,不影响整体思路,不去管它
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set: (newVal) => {
console.log('new value seted')
if(val === newVal) return
val = newVal
// 对新值进行监听
chlidOb = observe(newVal)
// 通知所有订阅者,数值被改变了
dep.notify()
}
})
}
export function observe(value){
// 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
if(!value || typeof value !== 'object'){
return
}
return new Observer(value)
}

管理订阅者

  • 对订阅者进行收集、存储和通知
1
2
3
4
5
6
7
8
9
10
11
12
export default class Dep{
constructor(){
this.subs = []
}
addSub(sub){
this.subs.push(sub)
}
notify(){
// 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
this.subs.forEach((sub) => sub.update())
}
}

订阅者

  • 每个订阅者都是对某条数据的订阅
  • 订阅者维护着每一次更新之前的数据,将其和更新之后的数据进行对比,如果发生了变化,则执行相应的业务逻辑,并更新订阅者中维护的数据的值
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
import Dep from 'Dep'
export default class Watcher{
constructor(vm, expOrFn, cb){
this.vm = vm // 被订阅的数据一定来自于当前Vue实例
this.cb = cb // 当数据更新时想要做的事情
this.expOrFn = expOrFn // 被订阅的数据
this.val = this.get() // 维护更新之前的数据
}
// 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
update(){
this.run()
}
run(){
const val = this.get()
if(val !== this.val){
this.val = val;
this.cb.call(this.vm)
}
}
get(){
// 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
Dep.target = this
const val = this.vm._data[this.expOrFn]
// 置空,用于下一个Watcher使用
Dep.target = null
return val;
}
}

Vue

  • 将数据代理到Vue实例上,真实数据存储于实例的_data属性中
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
import Observer, {observe} from 'Observer'
import Watcher from 'Watcher'
export default class Vue{
constructor(options = {}){
// 简化了$options的处理
this.$options = options
// 简化了对data的处理
let data = this._data = this.$options.data
// 将所有data最外层属性代理到Vue实例上
Object.keys(data).forEach(key => this._proxy(key))
// 监听数据
observe(data)
}
// 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者
$watch(expOrFn, cb){
new Watcher(this, expOrFn, cb)
}
_proxy(key){
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: () => this._data[key],
set: (val) => {
this._data[key] = val
}
})
}
}

调用这个极简版演示数据双向绑定原理的Vue

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
import Vue from './Vue';
let demo = new Vue({
data: {
'a': {
'ab': {
'c': 'C'
}
},
'b': {
'bb': 'BB'
},
'c': 'C'
}
});
demo.$watch('c', () => console.log('c is changed'))
// get value
demo.c = 'CCC'
// new value seted
// get value
// c is changed
demo.c = 'DDD'
// new value seted
// get value
// c is changed
demo.a
// get value
demo.a.ab = {
'd': 'D'
}
// get value
// get value
// new value seted
console.log(demo.a.ab)
// get value
// get value
// {get d: (), set d: ()}
demo.a.ab.d = 'DD'
// get value
// get value
// new value seted
console.log(demo.a.ab);
// get value
// get value
// {get d: (), set d: ()}