实现Vue的双向绑定
一、概述
之前有讲到过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中 2function nodeToFragment(node) {
3var flag = document.createDocumentFragment();
4while(node.firstChild) {
5 flag.appendChild(node.firstChild) // 劫持节点到文档片段中,在此之前对节点进行一些操作; 劫持到一个,对应的DOM容器里会删除掉一个节点
6 }
7return flag
8};
9var dom = nodeToFragment(document.getElementById('app'))
10 document.getElementById('app').apendChild(dom) // 将文档片段重新放入app中
对于双向绑定的实现,首先我们来创建vue的实例
1// 创建Vue对象 2function Vue(data) {
3var id = data.el;
4var ele = document.getElementById(id);
5this.data = data.data;
6 obersve(this.data, this) // 将vm.data指向vm
7var dom = nodeToFragment(ele, this); // 通过上面的函数劫持DOM节点
8 ele.appendChild(dom); // 将文档片段重新放入容器
9};
10// 实例化Vue对象
11var vm = new Vue({
12 el: 'app',
13 data: {
14 text: 'hello world'
15 }
16 })
通过以上代码我们可以看到,实例化Vue对象的时候先是将vm.data指向到了vm,而后是对html节点进行的数据绑定,此处分两步,我们先来看对vm的数据源绑定:
1function definevm(vm, key, value) { 2 Object.defineProperty(vm, key, { 3 get: function() { 4return value 5 }, 6 set: function(newval) { 7 value = newval 8 console.log(value) 9 }10 })11};12// 指定data到vm13function obersve(data, vm) {
14for(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// 绑定数据 2function compile(node, vm) {
3// console.log(node.nodeName)
4var reg = /\{\{(.*)\}\}/; // 匹配{{}}里的内容
5if (node.nodeType === 1) { // 普通DOM节点nodeType为1
6var attr = node.attributes 遍历节点属性
7for(var i = 0; i < attr.length; i++) {
8if (attr[i].nodeName === 'v-model') {
9var 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 };
19if (node.nodeType === 3) {
20if (reg.test(node.nodeValue)) {
21var name = RegExp.$1;
22 name = name.trim();
23 node.nodeValue = vm[name]; // 将vm.text的值赋给文本节点
24 }
25 }
26}
27// 劫持DOM节点到DocumentFragment中
28function nodeToFragment(node, vm) {
29var flag = document.createDocumentFragment();
30while(node.firstChild) {
31 compile(node.firstChild, vm); // 进行数据绑定
32 flag.appendChild(node.firstChild); // 劫持节点到文档片段中
33 }
34return 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// 定义发布订阅 2function Dep() {
3this.subs = []
4}
5 Dep.prototype = {
6 addSub: function(sub) {
7this.subs.push(sub);
8 },
9 notify: function() {
10this.subs.forEach(function(sub) {
11 sub.update();
12 })
13 }
14};
15// 定义观察者
16function Watcher (vm, node, name) {
17 Dep.target = this; // 发布者和订阅者的桥梁(送报员)
18this.name = name;
19this.node = node;
20this.vm = vm;
21this.update();
22 Dep.target = null;
23};
24 Watcher.prototype = {
25 update: function() {
26this.get();
27// console.log(this.node.nodeName)
28if (this.node.nodeName === 'INPUT') {
29this.node.value = this.value;
30 } else {
31this.node.nodeValue = this.value;
32 }
33 },
34 get: function() {
35this.value = this.vm[this.name];
36 }
37 }
此时,发布者和订阅者要分别在数据更新时和数据绑定时进行绑定
1// 绑定发布者 2function definevm(vm, key, value) {
3var dep = new Dep // 实例化发布者
4 Object.defineProperty(vm, key, {
5 get: function() {
6if (Dep.target) {
7 dep.addSub(Dep.target) // 为每个属性绑定watcher
8 }
9return value
10 },
11 set: function(newval) {
12 value = newval
13 console.log(value)
14 dep.notify(); // 数据改变执行发布
15 }
16 })
17};
18
19// 绑定订阅者到节点上面
20function compile(node, vm) {
21// console.log(node.nodeName)
22var reg = /\{\{(.*)\}\}/;
23if (node.nodeType === 1) {
24var attr = node.attributes
25for(var i = 0; i < attr.length; i++) {
26if (attr[i].nodeName === 'v-model') {
27var 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];
33new Watcher(vm, node, name); // 初始化绑定input节点
34 node.removeAttribute('v-model');
35 };
36 };
37 };
38if (node.nodeType === 3) {
39if (reg.test(node.nodeValue)) {
40var name = RegExp.$1;
41 name = name.trim();
42// node.nodeValue = vm[name];
43new 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// 定义发布订阅
15function Dep() {
16this.subs = []
17 }
18 Dep.prototype = {
19 addSub: function(sub) {
20this.subs.push(sub);
21 },
22 notify: function() {
23this.subs.forEach(function(sub) {
24 sub.update();
25 })
26 }
27 };
28// 定义观察者
29function Watcher (vm, node, name) {
30 Dep.target = this;
31this.name = name;
32this.node = node;
33this.vm = vm;
34this.update();
35 Dep.target = null;
36 };
37 Watcher.prototype = {
38 update: function() {
39this.get();
40// console.log(this.node.nodeName)
41if (this.node.nodeName === 'INPUT') {
42this.node.value = this.value;
43 } else {
44this.node.nodeValue = this.value;
45 }
46 },
47 get: function() {
48this.value = this.vm[this.name];
49 }
50 }
51// 绑定数据
52function compile(node, vm) {
53// console.log(node.nodeName)
54var reg = /\{\{(.*)\}\}/;
55if (node.nodeType === 1) {
56var attr = node.attributes
57for(var i = 0; i < attr.length; i++) {
58if (attr[i].nodeName === 'v-model') {
59var 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];
65new Watcher(vm, node, name);
66 node.removeAttribute('v-model');
67 };
68 };
69 };
70if (node.nodeType === 3) {
71if (reg.test(node.nodeValue)) {
72var name = RegExp.$1;
73 name = name.trim();
74// node.nodeValue = vm[name];
75new Watcher(vm, node, name);
76 }
77 }
78 }
79// 劫持DOM节点到DocumentFragment中
80function nodeToFragment(node, vm) {
81var flag = document.createDocumentFragment();
82while(node.firstChild) {
83// console.log(node.firstChild)
84 compile(node.firstChild, vm)
85 flag.appendChild(node.firstChild) // 劫持节点到文档片段中
86 }
87return flag
88 };
89function definevm(vm, key, value) {
90var dep = new Dep
91 Object.defineProperty(vm, key, {
92 get: function() {
93if (Dep.target) {
94 dep.addSub(Dep.target)
95 }
96return value
97 },
98 set: function(newval) {
99 value = newval
100 console.log(value)
101 dep.notify();
102 }
103 })
104 };
105// 指定data到vm
106function obersve(data, vm) {
107for(var key in data) {
108 definevm(vm, key, data[key]);
109 }
110 }
111// 创建Vue类
112function Vue (options) {
113this.data = options.data;
114var id = options.el;
115var ele = document.getElementById(id);
116
117// 将data的数据指向vm
118 obersve(this.data, this);
119// 存DOM到文档片段
120var dom = nodeToFragment(ele, this);
121// 编译完成将DOM返回挂在容器中
122 ele.appendChild(dom);
123 };
124// 创建Vue实例
125var 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
以上是 实现Vue的双向绑定 的全部内容, 来源链接: utcz.com/z/508751.html