实现Vue的双向绑定

久未见 提交于 2021-02-17 10:57:36

一、概述

之前有讲到过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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!