一、概述
之前有讲到过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中的节点都是属于文档片段的子孙节点。代码如下:
1 // 劫持DOM节点到DocumentFragment中
2 function nodeToFragment(node) {
3 var flag = document.createDocumentFragment();
4 while(node.firstChild) {
5 flag.appendChild(node.firstChild) // 劫持节点到文档片段中,在此之前对节点进行一些操作; 劫持到一个,对应的DOM容器里会删除掉一个节点
6 }
7 return flag
8 };
9 var dom = nodeToFragment(document.getElementById('app'))
10 document.getElementById('app').apendChild(dom) // 将文档片段重新放入app中
对于双向绑定的实现,首先我们来创建vue的实例
1 // 创建Vue对象
2 function Vue(data) {
3 var id = data.el;
4 var ele = document.getElementById(id);
5 this.data = data.data;
6 obersve(this.data, this) // 将vm.data指向vm
7 var dom = nodeToFragment(ele, this); // 通过上面的函数劫持DOM节点
8 ele.appendChild(dom); // 将文档片段重新放入容器
9 };
10 // 实例化Vue对象
11 var vm = new Vue({
12 el: 'app',
13 data: {
14 text: 'hello world'
15 }
16 })
通过以上代码我们可以看到,实例化Vue对象的时候先是将vm.data指向到了vm,而后是对html节点进行的数据绑定,此处分两步,我们先来看对vm的数据源绑定:
1 function definevm(vm, key, value) {
2 Object.defineProperty(vm, key, {
3 get: function() {
4 return value
5 },
6 set: function(newval) {
7 value = newval
8 console.log(value)
9 }
10 })
11 };
12 // 指定data到vm
13 function obersve(data, vm) {
14 for(var key in data) {
15 definevm(vm, key, data[key]);
16 }
17 }
18
19 vm.text = 'MrGao';
20 console.log(vm.text); // MrGao
此处将vm.data的属性指定到vm上,并且实现了对vm的监听,一旦vm的属性发生变化,便会触发其set方法;接下来我们来看下对DOM节点的数据绑定:
1 // 绑定数据
2 function compile(node, vm) {
3 // console.log(node.nodeName)
4 var reg = /\{\{(.*)\}\}/; // 匹配{{}}里的内容
5 if (node.nodeType === 1) { // 普通DOM节点nodeType为1
6 var attr = node.attributes 遍历节点属性
7 for(var i = 0; i < attr.length; i++) {
8 if (attr[i].nodeName === 'v-model') {
9 var name = attr[i].nodeValue; // 获取绑定的值
10 node.addEventListener('keyup', function(e) {
11 // console.log(e.target.value)
12 vm[name] = e.target.value //监听input值的变化,重新给vm.text赋值
13 })
14 node.value = vm[name];
15 node.removeAttribute('v-model');
16 };
17 };
18 };
19 if (node.nodeType === 3) {
20 if (reg.test(node.nodeValue)) {
21 var name = RegExp.$1;
22 name = name.trim();
23 node.nodeValue = vm[name]; // 将vm.text的值赋给文本节点
24 }
25 }
26 }
27 // 劫持DOM节点到DocumentFragment中
28 function nodeToFragment(node, vm) {
29 var flag = document.createDocumentFragment();
30 while(node.firstChild) {
31 compile(node.firstChild, vm); // 进行数据绑定
32 flag.appendChild(node.firstChild); // 劫持节点到文档片段中
33 }
34 return flag;
35 };
这样一来,我们就可以通过compile方法将vm.text绑定到input节点和下面的文本节点上,并且监听input节点的keyup事件,当input的value发生改变是,将input的值赋给vm.text,如此vm.text的值也改变了,同时会触发对vm的ste函数;但是vm.text的值是改变了,我们应该如何让文本节点的值同样跟随者vm.text的值改变呢?此时我们就可以使用订阅模式(观察者模式)来实现这一功能;那什么是订阅模式呢?
订阅模式就是好比有一家报社,他每天都要对新的世界大事进行发布,然后报社通知送报员去把发布的新的报纸推送给订阅者,订阅这在拿到报纸后可以获取到新的消息;反映到代码里可以这样理解;当vm.text改变时,触发set方法,然后发布变化的消息,在数据绑定的那里定义订阅者,在定义一个连接两者的“送报员”,每当发布者发布新的消息,订阅者都可以拿到新的消息,代码如下:
1 // 定义发布订阅
2 function Dep() {
3 this.subs = []
4 }
5 Dep.prototype = {
6 addSub: function(sub) {
7 this.subs.push(sub);
8 },
9 notify: function() {
10 this.subs.forEach(function(sub) {
11 sub.update();
12 })
13 }
14 };
15 // 定义观察者
16 function Watcher (vm, node, name) {
17 Dep.target = this; // 发布者和订阅者的桥梁(送报员)
18 this.name = name;
19 this.node = node;
20 this.vm = vm;
21 this.update();
22 Dep.target = null;
23 };
24 Watcher.prototype = {
25 update: function() {
26 this.get();
27 // console.log(this.node.nodeName)
28 if (this.node.nodeName === 'INPUT') {
29 this.node.value = this.value;
30 } else {
31 this.node.nodeValue = this.value;
32 }
33 },
34 get: function() {
35 this.value = this.vm[this.name];
36 }
37 }
此时,发布者和订阅者要分别在数据更新时和数据绑定时进行绑定
1 // 绑定发布者
2 function definevm(vm, key, value) {
3 var dep = new Dep // 实例化发布者
4 Object.defineProperty(vm, key, {
5 get: function() {
6 if (Dep.target) {
7 dep.addSub(Dep.target) // 为每个属性绑定watcher
8 }
9 return value
10 },
11 set: function(newval) {
12 value = newval
13 console.log(value)
14 dep.notify(); // 数据改变执行发布
15 }
16 })
17 };
18
19 // 绑定订阅者到节点上面
20 function compile(node, vm) {
21 // console.log(node.nodeName)
22 var reg = /\{\{(.*)\}\}/;
23 if (node.nodeType === 1) {
24 var attr = node.attributes
25 for(var i = 0; i < attr.length; i++) {
26 if (attr[i].nodeName === 'v-model') {
27 var name = attr[i].nodeValue;
28 node.addEventListener('keyup', function(e) {
29 // console.log(e.target.value)
30 vm[name] = e.target.value
31 })
32 // node.value = vm[name];
33 new Watcher(vm, node, name); // 初始化绑定input节点
34 node.removeAttribute('v-model');
35 };
36 };
37 };
38 if (node.nodeType === 3) {
39 if (reg.test(node.nodeValue)) {
40 var name = RegExp.$1;
41 name = name.trim();
42 // node.nodeValue = vm[name];
43 new Watcher(vm, node, name); // 文本节点绑定订阅者
44 }
45 }
46 }
到这里vue的双绑定就实现了,此文仅为实现最简单的双向绑定,一些其它复杂的条件都没有考虑在内,为理想状态下,如有纰漏还望指正,下面附上完整代码
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <title>Vue</title>
6 </head>
7 <body>
8 <div id="app">
9 <input type="text" id="a" v-model="text">
10 {{text}}
11 </div>
12 </body>
13 <script>
14 // 定义发布订阅
15 function Dep() {
16 this.subs = []
17 }
18 Dep.prototype = {
19 addSub: function(sub) {
20 this.subs.push(sub);
21 },
22 notify: function() {
23 this.subs.forEach(function(sub) {
24 sub.update();
25 })
26 }
27 };
28 // 定义观察者
29 function Watcher (vm, node, name) {
30 Dep.target = this;
31 this.name = name;
32 this.node = node;
33 this.vm = vm;
34 this.update();
35 Dep.target = null;
36 };
37 Watcher.prototype = {
38 update: function() {
39 this.get();
40 // console.log(this.node.nodeName)
41 if (this.node.nodeName === 'INPUT') {
42 this.node.value = this.value;
43 } else {
44 this.node.nodeValue = this.value;
45 }
46 },
47 get: function() {
48 this.value = this.vm[this.name];
49 }
50 }
51 // 绑定数据
52 function compile(node, vm) {
53 // console.log(node.nodeName)
54 var reg = /\{\{(.*)\}\}/;
55 if (node.nodeType === 1) {
56 var attr = node.attributes
57 for(var i = 0; i < attr.length; i++) {
58 if (attr[i].nodeName === 'v-model') {
59 var name = attr[i].nodeValue;
60 node.addEventListener('keyup', function(e) {
61 // console.log(e.target.value)
62 vm[name] = e.target.value
63 })
64 // node.value = vm[name];
65 new Watcher(vm, node, name);
66 node.removeAttribute('v-model');
67 };
68 };
69 };
70 if (node.nodeType === 3) {
71 if (reg.test(node.nodeValue)) {
72 var name = RegExp.$1;
73 name = name.trim();
74 // node.nodeValue = vm[name];
75 new Watcher(vm, node, name);
76 }
77 }
78 }
79 // 劫持DOM节点到DocumentFragment中
80 function nodeToFragment(node, vm) {
81 var flag = document.createDocumentFragment();
82 while(node.firstChild) {
83 // console.log(node.firstChild)
84 compile(node.firstChild, vm)
85 flag.appendChild(node.firstChild) // 劫持节点到文档片段中
86 }
87 return flag
88 };
89 function definevm(vm, key, value) {
90 var dep = new Dep
91 Object.defineProperty(vm, key, {
92 get: function() {
93 if (Dep.target) {
94 dep.addSub(Dep.target)
95 }
96 return value
97 },
98 set: function(newval) {
99 value = newval
100 console.log(value)
101 dep.notify();
102 }
103 })
104 };
105 // 指定data到vm
106 function obersve(data, vm) {
107 for(var key in data) {
108 definevm(vm, key, data[key]);
109 }
110 }
111 // 创建Vue类
112 function Vue (options) {
113 this.data = options.data;
114 var id = options.el;
115 var ele = document.getElementById(id);
116
117 // 将data的数据指向vm
118 obersve(this.data, this);
119 // 存DOM到文档片段
120 var dom = nodeToFragment(ele, this);
121 // 编译完成将DOM返回挂在容器中
122 ele.appendChild(dom);
123 };
124 // 创建Vue实例
125 var vm = new Vue({
126 el: 'app',
127 data: {
128 text: 'hello world'
129 }
130 })
131 </script>
132 </html>
参考文章:https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension
来源:oschina
链接:https://my.oschina.net/u/4361557/blog/3912724