简单应用

我们先来看一个简单的应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<input id="input" type="text" v-model="text">
<div id="text">输入的值为:{{text}}</div>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
text: 'hello world'
}
})
</script>

上面的示例具有的功能就是初始时,'hello world’字符串会显示在input输入框中和div文本中,当手动输入值后,div文本的值也相应的改变。

我们来简单理一下实现思路:

1、input输入框以及div文本和data中的数据进行绑定

2、input输入框内容变化时,data中的对应数据同步变化,即 view => model

3、data中数据变化时,对应的div文本内容同步变化,即 model => view

原理介绍

Vue.js是通过数据劫持以及结合发布者-订阅者来实现双向绑定的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。

双向数据绑定,简单点来说分为三个部分:

1、Observer:观察者,这里的主要工作是递归地监听对象上的所有属性,在属性值改变的时候,触发相应的watcher。
2、Watcher:订阅者,当监听的数据值修改时,执行响应的回调函数(Vue里面的更新模板内容)。
3、Dep:订阅管理器,连接Observer和Watcher的桥梁,每一个Observer对应一个Dep,它内部维护一个数组,保存与该Observer相关的Watcher。

DEMO实现双向绑定

下面我们来一步步的实现双向数据绑定。

第一部分是Observer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Observer(obj, key, value) {
var dep = new Dep();
if (Object.prototype.toString.call(value) == '[object Object]') {
Object.keys(value).forEach(function(key) {
new Observer(value, key, value[key])
})
};

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
if (Dep.target) {
dep.addSub(Dep.target);
};
return value;
},
set: function(newVal) {
value = newVal;
dep.notify();
}
})
}

递归的为对象obj的每个属性添加getter和setter。在getter中,我们把watcher添加到dep中。在setter中,触发watcher执行回调。

第二部分是Watcher:

1
2
3
4
5
6
7
8
function Watcher(fn) {
this.update = function() {
Dep.target = this;
fn();
Dep.target = null;
}
this.update();
}

fn是数据变化后要执行的回调函数,一般是获取数据渲染模板。默认执行一遍update方法是为了在渲染模板过程中,调用数据对象的getter时建立两者之间的关系。因为同一时刻只有一个watcher处于激活状态,把当前watcher绑定在Dep.target(方便在Observer内获取)。回调结束后,销毁Dep.target。

第三部分是Dep:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Dep() {
this.subs = [];

this.addSub = function (watcher) {
this.subs.push(watcher);
}

this.notify = function() {
this.subs.forEach(function(watcher) {
watcher.update();
});
}
}

内部一个存放watcher的数组subs。addSub用于向数组中添加watcher(getter时)。notify用于触发watcher的更新(setter时)。

以上我们就完成了简易的双向绑定的功能,我们用一下看是不是能达到上面简单应用同样的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<input id="input" type="text" v-model="text">
<div id="text">输入的值为:{{text}}</div>
</div>
<script type="text/javascript">
var obj = {
text: 'hello world'
}
Object.keys(obj).forEach(function(key){
new Observer(obj, key, obj[key])
});
new Watcher(function(){
document.querySelector("#text").innerHTML = "输入的值为:" + obj.text;
})
document.querySelector("#input").addEventListener('input', function(e) {
obj.text = e.target.value;
})
</script>

当然上面这是最简单的双向绑定功能,Vue中还实现了对数组、对象的双向绑定,下面我们来看看Vue中的实现。

Vue中的双向绑定

看Vue的实现源码前,我们先来看下下面这张图,经典的Vue双向绑定原理示意图(图片来自于网络):

Vue双向绑定示意图

简单解析如下:

1、实现一个数据监听器Obverser,对data中的数据进行监听,若有变化,通知相应的订阅者。

2、实现一个指令解析器Compile,对于每个元素上的指令进行解析,根据指令替换数据,更新视图。

3、实现一个Watcher,用来连接Obverser和Compile, 并为每个属性绑定相应的订阅者,当数据发生变化时,执行相应的回调函数,从而更新视图。

Vue中的Observer:

首先是Observer对象,源码位置src/core/observer/index.js

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
export class Observer {
value: any;
dep: Dep;
vmCount: number;

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 添加__ob__来标示value有对应的Observer
def(value, '__ob__', this)
if (Array.isArray(value)) { // 处理数组
if (hasProto) { // 实现是'__proto__' in {}
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else { // 处理对象
this.walk(value)
}
}

// 给对象每个属性添加getter/setters
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 重点
}
}

// 循环观察数组的每一项
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // 重点
}
}
}

整体上,value分为对象或数组两种情况来处理。这里我们先来看看defineReactive和observe这两个比较重要的函数。

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
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
}

// cater for pre-defined getter/setters
// 保存对象属性上自有的getter和setter
const getter = property && property.get
const setter = property && property.set
// 如果属性上之前没有定义getter,并且没有传入初始val值,就把属性原有的值赋值给val
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 () {
// 给属性设置getter
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 给每个属性创建一个dep
dep.depend()
if (childOb) {
childOb.dep.depend()
// 如果是数组,就递归创建
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 给属性设置setter
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()
}
})
}

defineReactive这个方法里面,是具体的为对象的属性添加getter、setter的地方。它会为每个值创建一个dep,如果用户为这个值传入getter和setter,则暂时保存。之后通过Object.defineProperty,重新添加装饰器。在getter中,dep.depend其实做了两件事,一是向Dep.target内部的deps添加dep,二是将Dep.target添加到dep内部的subs,也就是建立它们之间的联系。在setter中,如果新旧值相同,直接返回,不同则调用dep.notify来更新与之相关的watcher。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果不是对象就跳过
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 如果已有observer,就直接返回,上面讲到过会用`__ob__`属性来记录
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 如果没有,就创建一个
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

observe这个方法用于观察一个对象,返回与对象相关的Observer对象,如果没有则为value创建一个对应的Observer。

好的,我们再回到Observer,如果传入的是对象,我们就调用walk,该方法就是遍历对象,对每个值执行defineReactive。

对于传入的对象是数组的情况,其实会有一些特殊的处理,因为数组本身只引用了一个地址,所以对数组进行push、splice、sort等操作,我们是无法监听的。所以,Vue中改写value的__proto__(如果有),或在value上重新定义这些方法。augment在环境支持__proto__时是protoAugment,不支持时是copyAugment。

1
2
3
4
5
6
7
8
9
10
11
// augment在环境支持__proto__时
function protoAugment (target, src: Object) {
target.__proto__ = src
}
// augment在环境不支持__proto__时
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}

augment在环境支持__proto__时,就很简单,调用protoAugment其实就是执行了value.__proto__ = arrayMethodsaugment在环境支持__proto__时,调用copyAugment中循环把arrayMethods上的arrayKeys方法添加到value上。

那这里我们就要看看arrayMethods方法了。arrayMethods其实是改写了数组方法的新对象。arrayKeysarrayMethods中的方法列表。

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
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 是push、unshift、splice时,重新观察数组,因为这三个方法都是像数组中添加新的元素
if (inserted) ob.observeArray(inserted)
// 通知变化
ob.dep.notify()
return result
})
})

实际上还是调用数组相应的方法来操作value,只不过操作之后,添加了相关watcher的更新。调用pushunshiftsplice三个方法参数大于2时,要重新调用ob.observeArray,因为这三种情况都是像数组中添加新的元素,所以需要重新观察每个子元素。最后在通知变化。

Vue中的Observer就讲到这里了。实际上还有两个函数setdel没有讲解,其实就是在添加或删除数组元素、对象属性时进行getter、setter的绑定以及通知变化,具体可以去看源码。

Vue中的Dep:

看完Vue中的Observer,然后我们来看看Vue中Dep,源码位置:src/core/observer/dep.js

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
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = []
}

// 添加订阅者
addSub (sub: Watcher) {
this.subs.push(sub)
}

// 移除订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}

// 添加到订阅管理器
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

// 通知变化
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
// 遍历所有的订阅者,通知更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

Dep类就比较简单,内部有一个id和一个subs,id用于作为dep对象的唯一标识,subs就是保存watcher的数组。相比于上面我们自己实现的demo应用,这里多了removeSub和depend。removeSub是从数组中移除某个watcher,depend是调用了watcher的addDep。

好,Vue中的Dep只能说这么多了。

Vue中的Watcher:

最后我们再来看看Vue中的Watcher,源码位置:src/core/observer/watcher.js

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// 注,我删除了源码中一些不太重要或与双向绑定关系不太大的逻辑,删除的代码用// ... 表示
let uid = 0
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
this.cb = cb
this.id = ++uid
// ...
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}

get () {
pushTarget(this)
let value
const vm = this.vm
// ...
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}

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)
}
}
}

cleanupDeps () {
// ...
}

update () {
// 更新三种模式吧,lazy延迟更新,sync同步更新直接执行,默认异步更新添加到处理队列
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

run () {
// 触发更新,在这里调用cb函数
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

evaluate () {
// ...
}

depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}

teardown () {
// ...
}
}

创建Watcher对象时,有两个比较重要的参数,一个是expOrFn,一个是cb。

在Watcher创建时,会调用this.get,里面会执行根据expOrFn解析出来的getter。在这个getter中,我们或渲染页面,或获取某个数据的值。总之,会调用相关data的getter,来建立数据的双向绑定。

当相关的数据改变时,会调用watcher的update方法,进而调用run方法。我们看到,run中还会调用this.get来获取修改之后的value值。

其实Watcher有两种主要用途:一种是更新模板,另一种就是监听某个值的变化。

模板更新的情况:在Vue声明周期挂载元素时,我们是通过创建Watcher对象,然后调用updateComponent来更新渲染模板的。

1
vm._watcher = new Watcher(vm, updateComponent, noop)

在创建Watcher会调用this.get,也就是这里的updateComponent。在render的过程中,会调用data的getter方法,以此来建立数据的双向绑定,当数据改变时,会重新触发updateComponent。

数据监听的情况:另一个用途就是我们的computed、watch等,即监听数据的变化来执行响应的操作。此时this.get返回的是要监听数据的值。初始化过程中,调用this.get会拿到初始值保存为this.value,监听的数据改变后,会再次调用this.get并拿到修改之后的值,将旧值和新值传给cb并执行响应的回调。

好,Vue中的Watcher就说这么多了。其实上面注释的代码中还有cleanupDeps清除依赖逻辑、teardown销毁Watcher逻辑等,留给大家自己去看源码吧。

总结一下

Vue中双向绑定,简单来说就是Observer、Watcher、Dep三部分。下面我们再梳理一下整个过程:

  • 首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;
  • 然后在编译的时候在该属性的数组dep中添加订阅者,Vue中的v-model会添加一个订阅者,**也会,v-bind也会;
  • 最后修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。

相关