基于MVVM的双向数据绑定是这两年的技术热点,各种新框架层出不穷,前端页面的很多动作再也不用依赖jQuery的面条式操作DOM节点了,这里就简单实现一个双向数据绑定,消除神秘感。

实现双向绑定的方法有三种:

  • 发布/订阅模式,比如 backbone
  • 脏检查,比如angular,(现在angular啥样就不知道了)
  • 以Vue为代表的数据劫持 (基于Object.defineProperty(),缺陷:IE8不支持)

这里就用Object.defineProperty()来实现一个小demo,哪怕IE8不支持,但也会消亡,对此不用过于care。
先看html:

1
2
3
4
5
6
<div id="app">
<input type="text" v-model="value">
<p v-bind="value"></p>
<img v-bind:src="imgsrc" alt="">
<button v-on:click="sayHi">sayHi()</button>
</div>

首先要解析html里id为app的节点内容,有v-model,v-bind,v-on,解析这些命令,得出相应的操作,解析这些节点属性用到最基本的DOM属性获取操作:childNodes、attributes。

1
2
3
4
5
6
7
8
// 模板解析
function init(){
var app = document.getElementById("app");
var nodes = app.childNodes;
[].slice.call(nodes).forEach(function(ele){
search(ele); // 这里要挨个遍历并解析app里的节点
});
}

模板就这样,机上绑定数据,就要有相关数据和函数供模板绑定,我们使用Vue初始化实例的时候都要有data,methods等参数,这里同样也需要data和绑定函数methods,为了方便,这里就定义为$data和funs:

1
2
3
4
5
6
7
8
9
10
11
12
13
var funs = {
sayHi:function(){
console.log("hi...")
},
test:function(){
console.log("test...")
}
}
var $data = {
imgsrc:"https://img6.bdstatic.com/img/image/smallpic/h1.jpg",
value:"123abc"
}

怎样解析模板呢?命令提取而已,无非先去除DOM上的所有属性,从中挑出有价值的,比如以v-打头的,挑出后把带有v-打头属性的节点替换一下文本内容或添加相应的监听函数:

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
function search(node){
var atts = node.attributes;
if(atts){
[].slice.call(atts).forEach(function(attr){
var attrName = attr.name,
attrValue = attr.value,
active = attrName.substring(2),
activeName = attrName.split(":")[1];
if(attrName.indexOf("v-") === -1){
return;
}
if(dataMap[attrValue]){
dataMap[attrValue].push(node);
}
if(attr.name.indexOf("on")!== -1){
node.addEventListener(activeName,funs[attr.value],false);
}
// 解析 bind v-bind:src="XXXX"
if(active.indexOf("bind") !== -1){
if(activeName){
node.setAttribute(activeName,$data[attrValue]);
}else{
node.innerHTML = $data[attrValue];
}
}
if(active.indexOf("model") !== -1){
node.value = $data[attrValue];
// 这是input标签双向绑定的重点
node.addEventListener("input",bindInput,false);
}
function bindInput(event){
node.value = $data[attrValue] = event.target.value;
}
});
}
}

好,到这里已经完成了对模板的初始化解析,该替换的替换,该监听的事件函数也都能添加了,但这是静态的,也就是说,只能第一次绑定data,data变动怎么办呢?这里就要用到Object.defineProperty(),该属性是ES5中的,是个好东西,没有它就没有Vue,不熟悉的可以了解一下,这里不做介绍。总之数据data变动时,会触发set操作,因此在set发生时,说明数据更新了,即可更新模板,原理就这么简单,但我们首先要做的是实现对$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
function observe(data) {
if (!data || typeof data !== "object") {
return;
}
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, val) {
observe(val); // 递归
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function() {
return val;
},
set: function(newValue) {
if(val === newValue){
return;
}
console.log("老的:", val, "变新的:--》", newValue);
val = newValue;
init(); // 更新模板
}
});
}

这时候文本框和data及p元素实现了双向数据绑定,且按钮也添加了点击事件sayHi能成功绑定执行。

至此,一个基于数据劫持的双向数据绑定demo已经完成,核心就是通过Object.defineProperty监听数据的变动,有变动则刷新模板,而模板里解析指令,并执行相应的指令或函数绑定。

但本demo运行时查看源码依然能看到写在标签元素上的指令,而vue模板源码却看不到,为什么?答案是因为vue为了增加性能,把模板节点转换成成文档碎片fragment,进行遍历加工替换等操作后,再把处理后的结果插入id为app的根DOM中,这样总体上只有一次DOM的append操作,而DOM操作是性能的杀手,vue以此提升性能,而本demo没考虑性能优化,是直接遍历替换DOM的,所以demo有很多不足,目的只为说明数据劫持实现双向数据绑定的原理,有空会再写一篇详细的作为补充。