模拟Vue 中数据双向绑定

一、原理

如果使用 Object.defineProperty,实现一个最简单的双向绑定其实很简单,只需如下:

1
2
3
4
5
6
7
8
9
var Vue = {};
Object.defineProperty(Vue,'$data',{
set(val){
document.getElementById('vue-item').innerText = val
}
});
document.addEventListener('keyup', function(e){
Vue.$data = e.target.value
})

vue-bind.gif
上面这个 demo 就是 vue 双向绑定最简化的原理。

二、替换元素

想想我们使用 vue 时的规则

1
2
3
4
5
6
new Vue({
el:'#app',
data:{
text:'hello world'
}
});

写上页面结构:

1
2
3
4
<div id = 'app'>
<input type='text' v-model='text'>
text(markedown不让有,假设text‘双中括号’在里面)
</div>

我们把 Vue 抽象为一个构造函数,传入这些值

1
2
3
4
5
function Vue(options){
this.data = options.data;
this.id = options.el;
getAllNode(document.getElementById(this.id), this);
};

替换掉节点中所有的双中括号里的内容:

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
function compile (node, vm){
var reg = /\{\{(.*)\}\}/;//匹配‘双中括号’
// 节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++){
if(attr[i].nodeName === 'v-model'){
var name = attr[i].nodeValue;// 获取绑定的属性的名字
node.value = vm.data[name];// 替换值
node.removeAttribute('v-model');  //移除v-model
}
};
}
// 节点类型为text
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; //匹配到的第一个字符
name = name.trim();
node.nodeValue = vm.data[name]; // 将data的值赋值给node
}
}
};

function getAllNode(node, vm){
var length = node.childNodes.length;
for(var i = 0; i < length; i++){
compile(node.childNodes[i], vm)
}
};

这样就可以成功替换掉双中括号的内容:

vue-data.png

三、绑定元素

上面我们只是替换了元素,但还没有实现绑定
实现数据绑定,就要用到 definedProperty 的 set 和 get 方法:
首先我们要给 vue 的所有属性都添加 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
function Vue(){
// *****
observe(data,this)
}

// 遍历
function observe (obj, vm){
for(var key in obj){
active(vm, key, obj[key]);
}
}
// 添加set和get
function active (obj, key, val) {
Object.defineProperty(obj, key, {
get(){
return val;
},
set(newVal){
if (newVal === val){ return };
val = newVal;
}
});
}

再来明确我们要做的事,获取输入的值,改变 Vue 中相应的 data 的值,同时改变‘双中括号’中的值;

我们已经给 data 的每个属性都添加了 get 和 set 的方法,现在要做的就是如何触发它们。

触发它肯定是在赋值的时候,所以我们在有 v-model 属性的节点监听输入事件,同时赋值,触发 set 事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function compile (node, vm){
// ***********
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++){
if(attr[i].nodeName === 'v-model'){
var name = attr[i].nodeValue;// 获取绑定的属性的名字


// 监听input事件
node.addEventListener('input', function(e){
// 给相应的data属性赋值,触发set
vm.data[name] = e.target.value
})

node.value = vm.data[name];// 替换输入框的值为data中的值
node.removeAttribute('v-model');
}
};
}
// ************
}

我们监听了 input 事件,接下来要获取输入的值并同步改变文本;

我们肯定希望只希望哪里改变了就对哪里做处理就行了,所以我们引入一个简单的发布——订阅组件:

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

pubsub.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
pub: function(){
this.subs.forEach(function(sub){
sub.update();
})
}
}

在添加 set 和 get 的同时订阅事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function active (obj, key, val) {

var pubsub = new pubsub();

Object.defineProperty(obj.data, key, {
get(){
// 添加订阅
if(Pubsub.target){
pubsub.addSub(Pubsub.target);
}
return val;
},
set(newVal){
if (newVal === val){ return };
val = newVal;

// 发出通知
pubsub.pub();
}
});
}

添加一个方法,来在 pubsub 发出通知时处理事件,我们命名为 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
25
26
27
function Watcher(vm, node, name){
Pubsub.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Pubsub.target = null
}

Watcher.prototype = {
update(){
this.get();
this.node.nodeValue = this.value
},
// 获取data中的属性值
get(){
this.value = this.vm[this.name] // 触发相应的get
}
}

function getAllNode(node, vm){
var length = node.childNodes.length;
for(var i = 0; i < length; i++){
compile(node.childNodes[i], vm)
}
};

这个 watcher 我们在什么时候添加呢?当然是在一开始的时候(compile 里):

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
function compile (node, vm){
var reg = /\{\{(.*)\}\}/;// 匹配‘双中括号’内任意字符
// 节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++){
if(attr[i].nodeName === 'v-model'){
var name = attr[i].nodeValue;// 获取绑定的属性的名字

node.addEventListener('input', function(e){
// 给相应的data属性赋值,触发set
vm.data[name] = e.target.value
})

node.value = vm.data[name];// 替换输入框的值为data中的值
node.removeAttribute('v-model');
}
};
};
// 节点类型为text
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; //匹配到的第一个字符
name = name.trim();

// node.nodeValue = vm[name];

new Watcher(vm, node, name);// 观察输入的值
}
}
};

vue-data.png

至此,便模拟了整个数据绑定的流程。

四、总结

最后理清整个过程的思路

创建 Vue:
vue-data.png

input 事件:
vue-data.png

源码在我的 github 仓库:https://github.com/lastnigtic/vue-bindData