# 双向数据绑定
双向数据绑定其实就是模型(Model)改变后同步到视图中(View),视图(View)中的数据改变时同步到模型(Model)中。确保视图与模型中的数据一致。
# 常用的实现方式
发布者-订阅模式(backbone.js) 利用发布订阅设计模式,更新数据方法一般都是函数调用。例如:
vm.set('property', value)
。脏值检查(angular.js) 通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。最简单的就是
setInterval()
轮询检查数据。不过一般都是通过指定事件触发才进行数据检查。例如:DOM 事件(输入框 input 回调)、XHR 响应事件...等等数据劫持(vue.js) 采用数据劫持加上发布订阅模式。例如:通过
Object.defineProperty()
劫持各个属性的setter
和getter
,数据更新时同通知订阅者,调用更新视图函数。
# 数据劫持实现方法
先附上一个简易版流程图。
- Observer:发布者,使用
Object.defineProperty()
进行数据劫持,数据有变换时通知Dep
。 - Compile:解析 HTML 模板,将自定义指令转化为对于的视图更新函数
Watcher
,同时初始化视图。 - Watcher:订阅者,模板解析后的视图
View
更新函数。 - Dep:一个消息队列,会通知多个订阅者
Watcher
,因为模型上的一个属性会被视图的很多地方同时绑定,也就是一对多。
将Observer
、Compile
、Watcher
、Dep
实现后组合就是MVVM
了。
这里采用 ES5 的方式进行实现,后面会提供一个 ES6 版本和 Proxy 版本。
WARNING
如果不了解Object.defineProperty()
,建议去查阅一下,这是一个 ES5 的内置方法。
# 类的基本实现
先实现Observer
、Compile
、Watcher
、Dep
的基本逻辑。
Observer
类的基本实现,简单拦截一个对象中的所有数据。
function Observer(obj) {
var _this = this
Object.keys(obj).forEach(function (key) {
var val = obj[key]
// 深度劫持,对象中的对象
// 注意是new Observer(),否则Oberver内部的this是指向window
if (typeof val === 'object') new Observer(val)
_this.defineProp(obj, key, val)
})
}
// 拦截单个数据
Observer.prototype.defineProp = function(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举,可以for in
configurable: true, // 描述符可以改变,也可以在对象中删除该属性
get: function() {
return val
},
set: function(v) {
console.log('劫持一下')
val = v
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Compile
类的基本实现,对HTML模板进行解析,根据不同的指令处理不同的逻辑。
// [el] HTML元素
function Compile(el) {
// 创建文档碎片,提高DOM操作效率
var fragment = document.createDocumentFragment(el)
var child
while (child = el.firstChild) {
// 加入后,会移除之前的节点
fragment.appendChild(child)
}
this.decode(fragment)
// 添加的不是fragment本身,而是其子孙节点
el.appendChild(fragment)
}
// 解析元素节点和文本节点
// [node] 元素节点
Compile.prototype.decode = function(node) {
var _this = this
// {{}} v-text v-model
// 使用childNodes,不使用children
Array.prototype.forEach.call(node.childNodes, function(child) {
switch (child.nodeType) {
case 1:
// 元素节点 <p v-text="x">...<p>
_this.decodeElement(node)
// 子节点也需要解析,递归
_this.decode(node)
break;
case 3:
// 文本节点 {{ content }}
_this.decodeText(node)
break;
}
})
}
// 解析元素节点的属性 v-text v-model
Compile.prototype.decodeElement = function(node) {
// TODO:实现逻辑
}
// 解析文本节点的内容 {{ content }}
Compile.prototype.decodeText = function(node) {
// TODO:实现逻辑
}
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
Watcher
类的基本实现,一个订阅者。
// [prop] 对应的属性
// [callback] 某个节点的视图更新函数
function Watcher(prop, callback) {
this.prop = prop
this.callback = callback // 保存视图更新函数
}
// 视图更新
Watcher.prototype.update = function() {
this.callback && this.callback()
}
2
3
4
5
6
7
8
9
10
11
Dep
类的基本实现,一个队列,可以增加订阅者和通知所有订阅者。
function Dep() {
// [watcher1, watcher2...]
this.queue = []
}
// 新增订阅者
// [subject] 一个Watcher实例
Dep.prototype.add = function(subject) {
this.queue.push(subject)
}
// 通知所有订阅者 -> 调用一组视图更新函数
Dep.prototype.notify = function() {
this.queue.forEach(function(subject) {
subject.update()
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 类的完善
经常会使用到val = data.prop1.prop2
或data.prop1.prop2 = val
,为了方便,就写两个公共函数。
// [obj] 对象
// [stringProp] 属性字符串
function GetData(obj, stringProp) {
var props = stringProp.split('.')
while(props.length > 0) {
obj = obj[props.shift()]
}
return obj
}
// [obj] 对象
// [stringProp] 属性字符串
// [value] 值
function SetData(obj, stringProp, value) {
var props = stringProp.split('.')
while(props.length > 1) {
obj = obj[props.shift()]
}
obj[props[0]] = value
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Observer
类的完善,一个属性对应一个队列,{{ content }}
属性改变就通知队列中所有的订阅者。因为视图很多地方会绑定同一个属性。这样一来就可以通知绑定该数据的具体地方了。例如:<input v-model="content"/><p>{\{ content }}</p>
。
// [obj] 需要监听的对象
function Observer(obj) {
var _this = this
Object.keys(obj).forEach(function (key) {
var val = obj[key]
// 深度劫持,对象中的对象
if (typeof val === 'object') Observer(val)
_this.defineProp(obj, key, obj[val])
})
}
// 拦截单个数据
// [obj] 对象
// [key] 需要监听的属性
// [val] 该属性的值
Observer.prototype.defineProp = function(obj, key, val) {
// 一个属性,一个队列
// 因为视图很多地方会绑定同一个属性,例如:<input v-model="content"><p>{{ content }}
// TODO:需要思考,如何把对应的Watcher实例加进去
var dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举,可以for in
configurable: true, // 描述符可以改变,也可以在对象中删除该属性
get: function() {
return val
},
set: function(v) {
if (v !== val) {
val = v
// 数据更新了,通知dep
// dep通知watcher,调用视图更新函数
dep.notify()
}
}
})
}
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
Watcher
类的完善。
// [data] 数据模型
// [prop] 对应的属性
// [callback] 某个节点的视图更新函数
function Watcher(data, prop, callback) {
this.data = data
this.prop = prop
this.callback = callback // 保存视图更新函数
this.value = this.getVal()
// TODO:需要思考,如何将该实例添加到对应的队列dep中
}
Watcher.prototype.getVal = function() {
return GetData(this.data, this.prop)
}
// 视图更新
Watcher.prototype.update = function() {
// 对比一下新旧值,判断是否更新
var newVal = this.getVal()
if (this.callback && this.value !== newVal) {
// 顺便更新一下内部的value
this.value = newVal
this.callback()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Compile
类的完善。
- 需要解析元素节点
<p v-text="x"></p>
,初始化内容,同时删除属性v-text
。还需要创建一个订阅者,订阅该属性,更新视图。 - 需要解析元素节点
<input v-model="x" />
,初始化内容,同时删除属性v-model
,监听input
事件更新数据模型。还需要创建一个订阅者,订阅该属性,更新视图。 - 需要解析文本节点
{{ content }}
,初始化内容。还需要创建一个订阅者,订阅该属性,更新视图。
// [el] HTML元素
// [data] 模型数据
function Compile(el, data) {
// 模型数据
this.data = data
// 创建文档碎片,提高DOM操作效率
var fragment = document.createDocumentFragment()
var child
while (child = el.firstChild) {
// 加入后,会移出之前的节点
fragment.appendChild(child)
}
// 进行编译
this.decode(fragment)
// 添加的不是fragment本身,而是其子孙节点
el.appendChild(fragment)
}
// 解析元素节点和文本节点
// [node] 元素节点
Compile.prototype.decode = function(node) {
var _this = this
// {{}} v-text v-model
// 使用childNodes,不使用children
Array.prototype.forEach.call(node.childNodes, function(child) {
switch (child.nodeType) {
case 1:
// 元素节点 <p v-text="x">...<p>
_this.decodeElement(child)
// 子节点也需要解析,递归
_this.decode(child)
break;
default:
// 文本节点 {{ content }}
_this.decodeText(child)
break;
}
})
}
// 解析元素节点的属性 v-text v-model
// [node] 元素节点
Compile.prototype.decodeElement = function(node) {
var _this = this
Array.prototype.forEach.call(node.attributes, function(attr) {
// v-xxx="vvv"
// v-xxx
var attrName = attr.name
// vvv
var attrValue = attr.value
// 视图更新函数
var callback = undefined
switch(attrName) {
case 'v-text':
// 视图初始化
node.textContent = GetData(_this.data, attrValue)
// 视图更新函数
callback = function() {
node.textContent = GetData(_this.data, attrValue)
}
// 在元素上移除 v-text="xx"
node.removeAttribute('v-text')
break;
case 'v-model':
// 视图初始化
node.setAttribute('value', GetData(_this.data, attrValue))
// 视图更新函数
callback = function() {
node.setAttribute('value', GetData(_this.data, attrValue))
}
// 同时监听input事件,双向绑定
node.addEventListener('input', function(e) {
SetData(_this.data, attrValue, e.target.value)
})
// 在元素上移除 v-text="xx"
node.removeAttribute('v-model')
break;
}
// 如果视图更新函数不为空,则创建一个订阅者
if (callback) new Watcher(_this.data, attrValue, callback)
})
}
// 解析文本节点的内容 xx{{ content1 }}xx{{ context2 }}xx
// [node] 元素节点
Compile.prototype.decodeText = function(node) {
var _this = this
// 是否初始化
var initial = false
// 原始模板数据
var templete = node.textContent
// 视图更新函数
var callback = function() {
// 替换文本内容
node.textContent = templete.replace(/{{[^{}]+}}/g, function(t) {
var prop = t.match(/[^{}\s]+/)[0]
// 如果第一次初始化,需要创建一个订阅者
if (!initial) new Watcher(_this.data, prop, callback)
return GetData(_this.data, prop)
})
initial = true
}
// 调用一下,初始化模板,创建订阅者
callback()
}
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
Dep
类基本不变,就省略。
# 如何将对应的Watcher加入到对应的Dep中?
Observer.prototype.defineProp = function(obj, key, val) {
var dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
// 如果缓存存在,则添加进Dep中。*请结合Watcher中的注释理解。
if (Dep.Cache) dep.add(Dep.Cache)
return val
},
set: function(v) {
if (v !== val) {
val = v
dep.notify()
}
}
})
}
// ...
function Watcher(data, prop, callback) {
this.data = data
this.prop = prop
this.callback = callback
// 将当前实例,保存起来
Dep.Cache = this
// getVal其实就是GetData,也是data.prop1,必然会触发Object.defineProperty中的get方法
// 在get方法里面做一些文章就可以添加进入dep了。
this.value = this.getVal()
// 添加完后,清楚缓存
Dep.Cache = undefined
}
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
# 完成MVVM
根据上面的流程来,以下方式就可以完成双向数据绑定的类了。
function MVVM(el, data) {
this.$data = data
new Observer(data)
new Compile(el, data)
}
2
3
4
5
写个简单的例子测试一下:
<div id="app">
name: {{ name }}; age: {{ age }}
<input v-model="name"/>
<div v-text="address.country"></div>
</div>
2
3
4
5
new MVVM(document.getElementById('app'), {
name: "YXP",
age: "23",
address: {
country: '中国1'
}
})
2
3
4
5
6
7
← 位置 Promise/A+ →