zoukankan      html  css  js  c++  java
  • 实现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中
     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

  • 相关阅读:
    Atitit。D&D drag&drop拖拽功能c#.net java swing的对比与实现总结
    Atitit.js javascript异常处理机制与java异常的转换 多重catc hDWR 环境 .js exception process Vob7
    Atitit.web 视频播放器classid clsid 大总结quicktime,vlc 1. Classid的用处。用来指定播放器 1 2. 标签用于包含对象,比如图像、音
    ListView与Adapter的那些事儿
    (转)Android反面自动静音
    (转)socket 与 file_get_contents的区别和优势的简单介绍
    Android ArrayAdapter 详解
    ImageView相关
    Android dip,px,pt,sp 的区别
    (转)Android 程序获取、设置铃声、音量、静音、扬声器
  • 原文地址:https://www.cnblogs.com/gaosong-shuhong/p/9278069.html
Copyright © 2011-2022 走看看