一、概述

之前有讲到过vue实现整体的整体流程,讲到过数据的响应式,是通过Object.defineProperity来实现的,当时只是举了一个小小的例子,那么再真正的vue框架里是如何实现数据的双向绑定呢?是如何将vm.data中的属性通过“v-model”和“{{}}”绑定到页面上的呢?下面我们先抛弃vue中DOM渲染的机制,自己来动手实现一双向绑定的demo。

二、实现步骤

1、html部分

根据Vue的语法,定义html需要绑定的DOM,如下代码

2、js部分

由于直接操作DOM是非常损耗性能的,所以这里我们使用DocumentFragment(以下简称为文档片段),由于createDocumentFragment是在内存中创建的一个虚拟节点对象,所以往文档片段里添加DOM节点是不太消耗性能的;此处我们将app下面的节点都劫持到文档片段中,在文档片段中对DOM进行一些操作,然后将文档片段总体重新插入app容器里面去,而且此处插入到app中的节点都是属于文档片段的子孙节点。代码如下:

 // 劫持DOM节点到DocumentFragment中
function nodeToFragment(node) {
var flag = document.createDocumentFragment();
while(node.firstChild) {
flag.appendChild(node.firstChild) // 劫持节点到文档片段中,在此之前对节点进行一些操作; 劫持到一个,对应的DOM容器里会删除掉一个节点
}
return flag
};
var dom = nodeToFragment(document.getElementById('app'))
document.getElementById('app').apendChild(dom) // 将文档片段重新放入app中

对于双向绑定的实现,首先我们来创建vue的实例

 // 创建Vue对象
function Vue(data) {
var id = data.el;
var ele = document.getElementById(id);
this.data = data.data;
obersve(this.data, this) // 将vm.data指向vm
var dom = nodeToFragment(ele, this); // 通过上面的函数劫持DOM节点
ele.appendChild(dom); // 将文档片段重新放入容器
};
// 实例化Vue对象
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})

通过以上代码我们可以看到,实例化Vue对象的时候先是将vm.data指向到了vm,而后是对html节点进行的数据绑定,此处分两步,我们先来看对vm的数据源绑定:

 function definevm(vm, key, value) {
Object.defineProperty(vm, key, {
get: function() {
return value
},
set: function(newval) {
value = newval
console.log(value)
}
})
};
// 指定data到vm
function obersve(data, vm) {
for(var key in data) {
definevm(vm, key, data[key]);
}
} vm.text = 'MrGao';
console.log(vm.text); // MrGao

此处将vm.data的属性指定到vm上,并且实现了对vm的监听,一旦vm的属性发生变化,便会触发其set方法;接下来我们来看下对DOM节点的数据绑定:

 // 绑定数据
function compile(node, vm) {
// console.log(node.nodeName)
var reg = /\{\{(.*)\}\}/; // 匹配{{}}里的内容
if (node.nodeType === 1) { // 普通DOM节点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('keyup', function(e) {
// console.log(e.target.value)
vm[name] = e.target.value //监听input值的变化,重新给vm.text赋值
})
node.value = vm[name];
node.removeAttribute('v-model');
};
};
};
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
node.nodeValue = vm[name]; // 将vm.text的值赋给文本节点
}
}
}
// 劫持DOM节点到DocumentFragment中
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
while(node.firstChild) {
compile(node.firstChild, vm); // 进行数据绑定
flag.appendChild(node.firstChild); // 劫持节点到文档片段中
}
return flag;
};

这样一来,我们就可以通过compile方法将vm.text绑定到input节点和下面的文本节点上,并且监听input节点的keyup事件,当input的value发生改变是,将input的值赋给vm.text,如此vm.text的值也改变了,同时会触发对vm的ste函数;但是vm.text的值是改变了,我们应该如何让文本节点的值同样跟随者vm.text的值改变呢?此时我们就可以使用订阅模式(观察者模式)来实现这一功能;那什么是订阅模式呢?

订阅模式就是好比有一家报社,他每天都要对新的世界大事进行发布,然后报社通知送报员去把发布的新的报纸推送给订阅者,订阅这在拿到报纸后可以获取到新的消息;反映到代码里可以这样理解;当vm.text改变时,触发set方法,然后发布变化的消息,在数据绑定的那里定义订阅者,在定义一个连接两者的“送报员”,每当发布者发布新的消息,订阅者都可以拿到新的消息,代码如下:

 // 定义发布订阅
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
};
// 定义观察者
function Watcher (vm, node, name) {
Dep.target = this; // 发布者和订阅者的桥梁(送报员)
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.target = null;
};
Watcher.prototype = {
update: function() {
this.get();
// console.log(this.node.nodeName)
if (this.node.nodeName === 'INPUT') {
this.node.value = this.value;
} else {
this.node.nodeValue = this.value;
}
},
get: function() {
this.value = this.vm[this.name];
}
}

此时,发布者和订阅者要分别在数据更新时和数据绑定时进行绑定

 // 绑定发布者
function definevm(vm, key, value) {
var dep = new Dep // 实例化发布者
Object.defineProperty(vm, key, {
get: function() {
if (Dep.target) {
dep.addSub(Dep.target) // 为每个属性绑定watcher
}
return value
},
set: function(newval) {
value = newval
console.log(value)
dep.notify(); // 数据改变执行发布
}
})
}; // 绑定订阅者到节点上面
function compile(node, vm) {
// console.log(node.nodeName)
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('keyup', function(e) {
// console.log(e.target.value)
vm[name] = e.target.value
})
// node.value = vm[name];
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); // 文本节点绑定订阅者
}
}
}

到这里vue的双绑定就实现了,此文仅为实现最简单的双向绑定,一些其它复杂的条件都没有考虑在内,为理想状态下,如有纰漏还望指正,下面附上完整代码

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<input type="text" id="a" v-model="text">
{{text}}
</div>
</body>
<script>
// 定义发布订阅
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
};
// 定义观察者
function Watcher (vm, node, name) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.target = null;
};
Watcher.prototype = {
update: function() {
this.get();
// console.log(this.node.nodeName)
if (this.node.nodeName === 'INPUT') {
this.node.value = this.value;
} else {
this.node.nodeValue = this.value;
}
},
get: function() {
this.value = this.vm[this.name];
}
}
// 绑定数据
function compile(node, vm) {
// console.log(node.nodeName)
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('keyup', function(e) {
// console.log(e.target.value)
vm[name] = e.target.value
})
// node.value = vm[name];
new Watcher(vm, node, name);
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);
}
}
}
// 劫持DOM节点到DocumentFragment中
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
while(node.firstChild) {
// console.log(node.firstChild)
compile(node.firstChild, vm)
flag.appendChild(node.firstChild) // 劫持节点到文档片段中
}
return flag
};
function definevm(vm, key, value) {
var dep = new Dep
Object.defineProperty(vm, key, {
get: function() {
if (Dep.target) {
dep.addSub(Dep.target)
}
return value
},
set: function(newval) {
value = newval
console.log(value)
dep.notify();
}
})
};
// 指定data到vm
function obersve(data, vm) {
for(var key in data) {
definevm(vm, key, data[key]);
}
}
// 创建Vue类
function Vue (options) {
this.data = options.data;
var id = options.el;
var ele = document.getElementById(id); // 将data的数据指向vm
obersve(this.data, this);
// 存DOM到文档片段
var dom = nodeToFragment(ele, this);
// 编译完成将DOM返回挂在容器中
ele.appendChild(dom);
};
// 创建Vue实例
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
</script>
</html>

参考文章:https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

最新文章

  1. org.apache.log4j.Logger详解
  2. 为Python安装pymssql模块来连接SQLServer
  3. iOS Version 和 Build 版本号
  4. HTML5系列四(WebWorker、地理定位)
  5. JQuery类型转换
  6. (easy)LeetCode 203.Remove Linked List Elements
  7. Write a program to convert decimal to 32-bit unsigned binary.
  8. __construct()和__initialize()
  9. 基于 Java 2 运行时安全模型的线程协作--转
  10. spring容器启动的加载过程(二)
  11. mkdosfs 安装
  12. 论文选读一: Towards end-to-end reinforcement learning of dialogue agents for information access
  13. P1579哥德巴赫猜想
  14. 【JAVA8】双冒号
  15. Scala之隐式转换implicit详解
  16. 【CH6201】走廊泼水节
  17. python-s and s.strip()
  18. BloomFilter——大规模数据处理利器(爬虫判重)
  19. QQ怎么 发送 已经录好的视频
  20. windows如何查看删除记录

热门文章

  1. esxi 配置 交换主机 虚拟机交换机 linux centos 配置双网卡
  2. C#6.0-8.0新功能、ValueTuple
  3. 机器学习笔记——k-近邻算法(一)(摘抄于《机器学习实战》)
  4. 静态链表过程演示及代码实现(A - B) U (B - A)
  5. python 优雅的解析 jsonp
  6. 【miscellaneous】MPEG2、MPEG4、H264的差异
  7. Asp.net SignalR 实现服务端消息实时推送到所有Web端
  8. Java check是否是日期类型
  9. nssm使用,安装服务、删除服务
  10. javascript jssdk退出微信的方法