前言
最近看了些关于vue的博客,觉得只懂的用vue的语法糖,却连一点原理都不知道总有点那啥。
所以今天整理了一些关于简单实现Vue双向数据绑定的知识~
参考资料
正文
实现原理基础
Vue实现双向数据绑定的做法是数据劫持,个人的理解是将需要绑定的元素进行封装,修改set
和get
方法,在元素被访问时达到响应绑定的效果。
我们可以先来看一个非常简单的双向绑定实现
1 | <div id="app"> |
这个例子可已做到对输入框与标签的双向数据绑定,但是为了最后做到如同vue进行双向绑定一样方便,我们还需要做一下对上述步骤的分解和重组。
- 将输入框及文本节点与
data
的数据绑定 - 输入框内容发生变化时,data中的数据同步变化(view=>model)
- data中数据变化时,同样修改绑定的文本节点(model=>view)
绑定数据
DocumentFragment相关
这里需要引入一下DocumentFragment
的知识,按照我们的思路,对绑定的页面元素进行封装过程会引入很大的消耗,我们需要一种方法来避免对DOM元素的直接封装操作。
Vue进行编译时,引入DocumentFragment
作为代替容器。DocumentFragment
可以简单看作一个节点容器,它可以包裹多个子节点,当整个DocumentFragment
插入DOM时,只有其子节点被插入DOM。使用DocumentFragment
代替DOM的直接处理,可以提高速度和性能。
想详细了解DocumentFragment
可以查看DocumentFragment详细说明
封装DOM节点为DocumentFragment的代码如下
1 |
|
上面的convertNode
函数会将传入node的子节点进行劫持,经过处理后重新挂载回目标节点。
数据绑定初始化
简单说明一下这部分的具体思路
- 首先我们需要一个函数
comile
负责将目标节点中所有含f-model
属性的元素及被双括号包裹的文本内容与对应data
绑定。 - 对需要绑定的目标节点,其子节点的封装过程由上面的
convertNode
传递。 - 一个
fake-Vue
对象作为Model层和ViewModel
代码代码1
2
3
4<div id="app">
<input type="text" id="a" f-model='text'>
{{text}}
</div>
1 | function convertNode(node, fm) { |
数据响应
上面的代码就简单实现了将输入框内容及文本节点内容与数据绑定。下一步要做的就是数据对象的响应式。
这里第一步我们需要修改被绑定元素的set
和get
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function observe(obj,fm){
Object.keys(obj).forEach(function(key)){
Object.defindPrpperty(obj,key,{
get:function(){
return obj[key];
}
set:function(newVal){
if(newVal == val)
return;
val = newVal;
console.log(val);
}
});
}
}
然后是修改compile方法,为输入框添加事件监听触发1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function compile(node, fm) {
var reg = /\{\{(.*)\}\}/;
if (node.nodeType === 1) { //节点类型为元素节点
var attr = node.attributes; //对所有属性进行解析
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'f-model') {
//将元素与数据绑定
var bindName = attr[i].nodeValue;
node.addEventListener('input',function(e){
fm.data[bindName] = e.target.value;
node.value = fm.data[bindName];
});
node.removeAttribute('f-model');
}
}
}
if (node.nodeType === 3) { //节点类型为文本节点
if (reg.test(node.nodeValue)) {
var bindName = RegExp.$1.trim();
node.nodeValue = fm.data[bindName];
}
}
}
还需要在fakeVue
中添加observe
调用1
2
3
4
5
6
7function fakeVue(options) {
this.data = options.data;
observe(this.data,this);
var id = options.el;
var dom = convertNode(document.getElementById(id), this);
document.getElementById(id).appendChild(dom);
}
做完上述修改,输入框的内容变化的时候,fm
对象中的值也会一起被修改。这样就完成了model层向View层的绑定。
应用观察者模式
这里我们为了完成View层向Model层的绑定,需要引入观察者模式 Observer Pattern,别名也叫订阅/发布模式。
这里简单描述下观察者模式,比较抽象的理解就是观察者模式中包含两种角色:观察者(订阅者)和被观察者(发布者),作为观察者,只要订阅了被观察者的事件,那么当观察者的状态改变时,被观察者需要主动通知观察者,但是观察者后续采取什么动作是与被观察者无关的。
基本的实现思想是被观察者维护一个订阅列表,列表中存放观察者的对象或观察者提供的回调方法供被观察者进行通知。当被观察者的状态改变时,只要循环执行列表即可。
骄傲javascript中的addEventListener
就是向目标元素订阅相关的动作,如1
element.addEventListener('click', callback, false);
理解了观察者模式,我们也就容易实现View层向Model层的绑定。把Model层作为被观察者,而页面元素作为相关的观察者。
实现双向绑定
实现双向绑定的具体步骤是
- 在对元素进行编译时,对文本节点包装成
Watcher
,添加到data
相关元素的观察者列表里。Watcher
负责更新页面元素。 - 监听过程中,为所有
data
属性生成一个主体对象dep
,dep
中包含需要维护的观察者列表,触发Watcher
的更新动作,即作为订阅信息的发布者。留下一个全局的Dep
对象用于保存目标元素的相关。 - 修改
observe
函数,添加对观察者列表的添加动作。
1 | function Dep() { |
可能上面的代码有点难懂,这里贴一张流程图,一图胜千言,应该不难懂的
黑色的线代表初始化观察者列表等等,红色的线是活动过程中,Model层的状态改变向View层更新的过程。
结语
写完啦写完啦
本文的代码和demo扔在了双向数据绑定demo
感谢阅读~