一、实现双向绑定的一个极简方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<input type="text" id="a">
<span id="b"></span>
<script type="text/javascript">
var obj = {};
Object.defineProperty(obj,'hello',{
set:function(newVal) {
document.getElementById('a').value = newVal;
document.getElementById('b').innerHTML = newVal;
}
})
document.addEventListener('keyup',function(e) {
obj.hello = e.target.value;
})
</script>
</body>
</html>
上述代码实现的基本逻辑和功能
1.监听键盘抬起事件(这里我直接用的document监听,因为冒泡机制,document也会受到这个事件),触发就把当前事件的value给obj的hello属性
2.由于obj的hello属性有属性描述对象,所以在给这个hello属性赋值的时候,会触发set方法,从而把页面中id为a和b的两个节点的值都给改成了新的值,这里需要注意的一点是,id为a和b的两个节点,他们设置自身值的方法是不一样的,input可以设置value属性,但如果是其他的标签呢,比如span标签,你直接设置value等于某个值,是无效的,而要设置span的innerHTML(仅仅作一个友情提示~)
二、通过DocumentFragment实现view单向读取vue的数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model='text'>
{{age}}
</div>
<script>
var vm = new Vue({
el:'app',
data: {
text:'helloWorld',
name:'张三',
age:23
}
})
//上面模拟一个vue实例的创建过程,和vue写法一样
function Vue(options) {
this.data = options.data;
var id = options.el;
var outer = document.getElementById(id)
var fragment = nodeToFragment(outer,this);
document.getElementById(id).appendChild(fragment);
}
//上面是vue的构造函数
function nodeToFragment(node,vm) {
var fragment = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
compile(child,vm);
fragment.append(child);
}
return fragment;
}
//node代表外层的节点,里面相当于是vue中el管理的那一片区域,vm就是vue实例
//nodeToFragment的作用,就是把真实HTML中el管理的那一片区域,转成documentFragment,把里面的标签,按照绑定的情况从vue中把对应数据给documentFragment里头的节点
function compile(node,vm) {
var reg = /\{\{(.*)\}\}/
if (node.nodeType === 1) {
var attr = node.attributes;
for(let i = 0;i < attr.length;i++) {
if (attr[i].nodeName === 'v-model') {
var value = vm.data[attr[i].nodeValue];
node.value = value;
node.removeAttribute('v-model')
}
}
}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
node.nodeValue = vm.data[name];
}
}
}
//compile的作用,就是完成数据的转移,把vue实例里的数据,给到节点就可以了,node可能是文本节点,也可能是元素节点,所以根据不同的情况为其填入对应值
</script>
</body>
</html>
上述代码实现的基本逻辑和功能
1.通过documentFragment将数据进行劫持,然后再添加到原来的元素中
2.compile对元素中v-model或者{{}}mustache语法进行识别,并把vue实例中的数据装进去
3.此时数据只是view把vue中的数据读取过来了,只是一个初始化,此时改变vue中的数据或者改变输入框,互相都不能实现同步
三、对v-model的节点添加事件,实现view到model的数据绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model='text'>
{{age}}
</div>
<script>
var vm = new Vue({
el:'app',
data: {
text:'helloWorld',
name:'张三',
age:23
}
})
function Vue(options) {
this.data = options.data;
var id = options.el;
var data =options.data;
observe(data,this)
var outer = document.getElementById(id)
var fragment = nodeToFragment(outer,this);
document.getElementById(id).appendChild(fragment);
}
function observe(dataObj,vm) {
Object.keys(dataObj).forEach((key) => {
makeItReactiveOnVM(vm,key,dataObj[key]);
})
}
//上述代码把data对象里面所有属性和值遍历并加到了vm上面
function makeItReactiveOnVM(vm,key,value) {
Object.defineProperty(vm,key,{
get:function() {
return value;
},
set:function(newValue) {
if (newValue === value) return;
value = newValue;
}
})
}
//makeItReactiveOnVM就把data对象的每个属性,都弄到了vue实例中,并且实现了set和get方法
function nodeToFragment(node,vm) {
var fragment = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
compile(child,vm);
fragment.append(child);
}
return fragment;
}
function compile(node,vm) {
var reg = /\{\{(.*)\}\}/
if (node.nodeType === 1) {
var attr = node.attributes;
for(let i = 0;i < attr.length;i++) {
if (attr[i].nodeName === 'v-model') {
var name = attr[i].nodeValue
node.addEventListener('input',function(e) {
vm[name] = e.target.value;
})
//这里顺道给这个v-model的元素加上监听事件,如果值变了,就要改vue实例中的数据
node.value = vm[name];
//之前observe把数据直接给了vue实例,所以不用再vm.data[name]了
node.removeAttribute('v-model')
}
}
}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
//node.nodeValue = vm.data[name];
node.nodeValue = vm[name]
}
}
}
</script>
</body>
</html>
上述代码实现的基本逻辑和功能
1.在compile也就是数据初次渲染的时候,就给v-model的元素添加了监听事件,如果发生了input就把数据给vue
2.在上面添加了observe和makeItReactiveOnVM方法,把数据直接弄到了vue实例下面,而不是vue的data下面了,并且为每个属性都设置了set和get方法,方便我们下一步实现vue中数据的改变,同步到输入框中数据改变
四、为vue的每个属性添加Dep对象,并在初次时,设置watcher实现发布订阅者模式的双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model='text'>
{{age}}
</div>
<script>
function Vue(options) {
this.data = options.data;
var data =options.data;
observe(data,this)
var id = options.el;
var outer = document.getElementById(id)
var fragment = nodeToFragment(outer,this);
document.getElementById(id).appendChild(fragment);
}
function observe(dataObj,vm) {
Object.keys(dataObj).forEach((key) => {
makeItReactiveOnVM(vm,key,dataObj[key]);
})
}
function makeItReactiveOnVM(vm,key,value) {
var dep = new Dep();
Object.defineProperty(vm,key,{
get:function() {
if (Dep.target) {
//能进入到这里,说明是在调用new watcher,因为Dep.target有值
dep.addSub(Dep.target);
}
return value;
},
set:function(newValue) {
if (newValue === value) return;
value = newValue;
dep.notify();
}
});
}
function nodeToFragment(node,vm) {
var fragment = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
compile(child,vm);
fragment.appendChild(child);
}
return fragment;
}
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) {
vm[name] = e.target.value;
})
new watcher(vm,node,name,'input');
node.removeAttribute('v-model')
}
}
}
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,'text');
}
}
}
function watcher (vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
watcher.prototype = {
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
get: function () {
this.value = this.vm[this.name]; // 触发相应属性的 get
}
}
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify:function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
}
var vm = new Vue({
el:'app',
data: {
text:'helloWorld',
name:'张三',
age:23
}
})
</script>
</body>
</html>
上述代码的实现逻辑和功能
1.每个属性的描述对象内,都有一个dep对象,dep对象内有所有订阅了这个属性值的对象,存在subs数组中
2.在compile的时候,就依据使用该值的node,name,vm,nodeType创建一个watcher,从而给node赋值,使用的该watcher的update方法触发了对应属性的get方法,从而在其dep对象里面保存了该watcher对象
3.非常要注意的是代码的顺序,new vue操作是在最后,如果放在第一行,function这些会提前,但是watcher.prototype不会提前,所以调用update方法会出问题,这一点需要注意
4.model到view层,如果数据改变,会调用notify方法通知所有的watcher,watcher会重新获取vue的值到watcher对象中,并操控元素改变相关的值,并且dep不会重复添加watcher,因为watcher只有在被new的时候,Dep.target才会指向新创建的watcher对象,后面在更新的之后,只会调用其update方法更新数据
总结:
实现双向绑定主要是两个步骤
1.把vue实例中的数据监听,为vue实例中每一条data的key通过defineProperty劫持,使得这些数据挂载在vue实例上,并且每个属性的get和set都有对应的dep在管理,如果get,如果set就有对应的行为。
2.把页面上的v-model,{{}}都通过compile给解析出来,弄到fragment里头再放回去,compile的时候,会在这些对应的node上,都设置一个watcher,watcher里保存了vue实例,如果变化了,dep会通知watcher调用update方法,此时再从vue中拿数据更新即可。
总之,就是vue实例先实现数据的劫持,然后解析页面,页面中受监控的节点如果变动,就会通知到vue,vue又会通知所有用了这个属性的watcher。在数据端,有一个dep在管理,在页面那里,有一个watcher在管理。
注:本文是自我的理解和梳理,在看相关博客的时候,有一些费解和没看懂的地方,自己进行了整理,然后自己逐步照着实现了一遍,把完整的代码也贴上了,有问题欢迎交流~
网友评论