zoukankan      html  css  js  c++  java
  • Vue2.0源码阅读笔记--双向绑定实现原理

      上一篇 文章 了解了Vue.js的生命周期。这篇分析Observe Data过程,了解Vue.js的双向数据绑定实现原理。

    一、实现双向绑定的做法

      前端MVVM最令人激动的就是双向绑定机制了,实现双向数据绑定的做法大致有如下三种:

    1.发布者-订阅者模式(backbone.js)

    思路:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。

    2.脏值检查(angular.js)

    思路:angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,angular只有在指定的事件触发时进入脏值检测,大致如下:

    • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )

    • XHR响应事件 ( $http )

    • 浏览器Location变更事件 ( $location )

    • Timer事件( $timeout , $interval )

    • 执行 $digest() 或 $apply()

    3.数据劫持(Vue.js)

    思路: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

    由此可见,Object.defineProperty() 这个API是Vue实现双向数据绑定的关键,我们先简单了解下这个API,了解更多戳这里

    二、Object.defineProperty()

    简单例子:

    var obj = {};
        Object.defineProperty(obj, 'hello', {
            get: function() {
                console.log('get val:'+ val);
                return val;
           },
          set: function(newVal) {
                val = newVal;
                console.log('set val:'+ val);
            }
        });
    obj.hello='111';
    obj.hello;

    结果:

    如果去掉 obj.hello=‘111’ 这行代码,则get的返回值val会报错val is not defined。可见Object.defineProperty() 监控对数据的操作,可以自动触发数据同步。下面我们先用Object.defineProperty()来实现一个非常简单的双向绑定。

    三、实现简单的双向绑定

     最简单例子:

    <!DOCTYPE html>
     <head></head>
     <body>
      <div id="app">
        <input type="text" id="a">
        <span id="b"></span>
      </div>
    
      <script type="text/javascript">
       var obj = {};
       Object.defineProperty(obj, 'hello', {
           get: function() {
               console.log('get val:'+ val);
               return val;
           },
           set: function(newVal) {
                val = newVal;
                console.log('set val:'+ val);
                document.getElementById('a').value = val;
                document.getElementById('b').innerHTML = val;
           }
        });
        document.addEventListener('keyup', function(e) {
          obj.hello = e.target.value;
        });
       </script>
      </body>
    </html>

    实现效果如下:

    上面例子直接用了dom操作改变了文本节点的值,而且是在我们知道是哪个id的情况下,通过document.getElementById 获取到相应的文本节点,然后直接修改文本节点的值,这种做法是最简单粗暴的。

    封装成一个框架,肯定不能是这种做法,所以我们需要一个解析dom,并能修改dom中相应的变量的模块。

    四、实现简单Compile

    首先我们需要获取文本中真实的dom节点,然后再分析节点的类型,根据节点类型做相应的处理。

    在上面例子我们多次操作了dom节点,为提高性能和效率,会先将所有的节点转换城文档碎片fragment进行编译操作,解析操作完成后,再将fragment添加到原来的真实dom节点中。

    <!DOCTYPE html>
      <head></head>
      <body>
      <div id="app">
        <input type="text" id="a" v-model="text">
        {{text}}
      </div>
     <script type="text/javascript">
      function Compile(node, vm) {
          if(node) {this.$frag = this.nodeToFragment(node, vm);
            return this.$frag;
          }
        }
        Compile.prototype = {
          nodeToFragment: function(node, vm) {
            var self = this;
            var frag = document.createDocumentFragment();
            var child;
    
            while(child = node.firstChild) {
              self.compileElement(child, vm);
              frag.append(child); // 将所有子节点添加到fragment中,child是指向元素首个子节点的引用。将child引用指向的对象append到父对象的末尾,原来child引用的对象就跳到了frag对象的末尾,而child就指向了本来是排在第二个的元素对象。如此循环下去,链接就逐个往后跳了
            }
            return frag;
          },
          compileElement: function(node, vm) {
            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; // 获取v-model绑定的属性名
                  node.addEventListener('input', function(e) {
                    // 给相应的data属性赋值,进而触发该属性的set方法
                     vm.data[name]= e.target.value;
                  });
                  node.value = vm.data[name]; // 将data的值赋给该node
                  node.removeAttribute('v-model');
                }
              };
            }
            //节点类型为text
        if(node.nodeType === 3) { if(reg.test(node.nodeValue)) { var name = RegExp.$1; // 获取匹配到的字符串 name = name.trim(); node.nodeValue = vm.data[name]; // 将data的值赋给该node } } }, }   function Vue(options) { this.data = options.data; var data = this.data; var id = options.el; var dom =new Compile(document.getElementById(id),this); // 编译完成后,将dom返回到app中 document.getElementById(id).appendChild(dom); } var vm = new Vue({ el: 'app', data: { text: 'hello world' } }); </script> </body> </html>

    结果:

    到这,我们做到了获取文本中真实的dom节点,然后分析节点的类型,并能处理节点中相应的变量如上面代码中的{{text}},最后渲染到页面中。接着我们需要和双向绑定联系起来,实现{{text}}响应式的数据绑定。

    五、实现简单observe

    简单的observe定义如下:

    需要监控data的属性值,这个对象的某个值赋值,就会触发setter,这样就能监听到数据变化。然后注意vm.data[name]属性将改为vm[name]

    完整代码如下:

    <!DOCTYPE html>
      <head></head>
      <body>
      <div id="app">
        <input type="text" id="a" v-model="text">
        {{text}}
      </div>
    <script type="text/javascript">
      function Compile(node, vm) {
          if(node) {
            this.$frag = this.nodeToFragment(node, vm);
            return this.$frag;
          }
        }
        Compile.prototype = {
          nodeToFragment: function(node, vm) {
            var self = this;
            var frag = document.createDocumentFragment();
            var child;
    
            while(child = node.firstChild) {
              self.compileElement(child, vm);
              frag.append(child); // 将所有子节点添加到fragment中
            }
            return frag;
          },
          compileElement: function(node, vm) {
            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; // 获取v-model绑定的属性名
                  node.addEventListener('input', function(e) {
                    // 给相应的data属性赋值,进而触发该属性的set方法
                     vm[name]= e.target.value;
                  });
                  node.value = vm[name]; // 将data的值赋给该node
                  node.removeAttribute('v-model');
                }
              };
            }
            //节点类型为text
            if(node.nodeType === 3) {
              if(reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 获取匹配到的字符串
                name = name.trim();
                node.nodeValue = vm[name]; // 将data的值赋给该node
                // new Watcher(vm, node, name);
              }
            }
          },
        }
        function defineReactive (obj, key, val) {
          Object.defineProperty(obj, key, {
            get: function() {
              return val;
            },
            set: function (newVal) {
              if(newVal === val) return;
              val = newVal;
              console.log(val);
            }
          })
        }
        function observe(obj, vm) {
          Object.keys(obj).forEach(function(key) {
            defineReactive(vm, key, obj[key]);
          })
        }
       function Vue(options) {
          this.data = options.data;
          var data = this.data;
          observe(data, this);
          var id = options.el;
          var dom =new Compile(document.getElementById(id),this);
          // 编译完成后,将dom返回到app中
          document.getElementById(id).appendChild(dom);
        }
        var vm = new Vue({
          el: 'app',
          data: {
            text: 'hello world'
          }
        });
      </script>
      </body>
    </html>
    View Code

    结果:

    到这,虽然set方法触发了,但是文本节点{{text}}的内容没有变化,要让绑定的文本节点同步变化,我们需要引入订阅发布模式。

    六、订阅发布模式

    订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

         发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作

     首先我们要一个收集订阅者的容器,定义一个Dep作为主题对象

    然后定义订阅者Watcher

    添加订阅者Watcher到主题对象Dep,发布者发出通知放到属性监听里面

    最后需要订阅的地方

    至此,比较简单地实现了我们第三步用dom操作实现的双向绑定效果,代码:

    <!DOCTYPE html>
      <head></head>
      <body>
      <div id="app">
        <input type="text" id="a" v-model="text">
        {{text}}
      </div>
      <script type="text/javascript">
      function Compile(node, vm) {
          if(node) {
            this.$frag = this.nodeToFragment(node, vm);
            return this.$frag;
          }
        }
        Compile.prototype = {
          nodeToFragment: function(node, vm) {
            var self = this;
            var frag = document.createDocumentFragment();
            var child;
    
            while(child = node.firstChild) {
              self.compileElement(child, vm);
              frag.append(child); // 将所有子节点添加到fragment中
            }
            return frag;
          },
          compileElement: function(node, vm) {
            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; // 获取v-model绑定的属性名
                  node.addEventListener('input', function(e) {
                    // 给相应的data属性赋值,进而触发该属性的set方法
                     vm[name]= e.target.value;
                  });
                  // node.value = vm[name]; // 将data的值赋给该node
                  new Watcher(vm, node, name, 'value');
                }
              };
            }
            //节点类型为text
            if(node.nodeType === 3) {
              if(reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 获取匹配到的字符串
                name = name.trim();
                // node.nodeValue = vm[name]; // 将data的值赋给该node
                new Watcher(vm, node, name, 'nodeValue');
              }
            }
          },
        }
        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, type) {
          Dep.target = this;
          this.name = name;
          this.node = node;
          this.vm = vm;
          this.type = type;
          this.update();
          Dep.target = null;
        }
    
        Watcher.prototype = {
          update: function() {
            this.get();
            this.node[this.type] = this.value; // 订阅者执行相应操作
          },
          // 获取data的属性值
          get: function() {
            this.value = this.vm[this.name]; //触发相应属性的get
          }
        }
        function defineReactive (obj, key, val) {
          var dep = new Dep();
          Object.defineProperty(obj, key, {
            get: function() {
               //添加订阅者watcher到主题对象Dep
              if(Dep.target) {
                // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
                dep.addSub(Dep.target);
              }
              return val;
            },
            set: function (newVal) {
              if(newVal === val) return;
              val = newVal;
              console.log(val);
              // 作为发布者发出通知
              dep.notify();
            }
          })
        }
        function observe(obj, vm) {
          Object.keys(obj).forEach(function(key) {
            defineReactive(vm, key, obj[key]);
          })
        }
    
       function Vue(options) {
          this.data = options.data;
          var data = this.data;
          observe(data, this);
          var id = options.el;
          var dom =new Compile(document.getElementById(id),this);
          // 编译完成后,将dom返回到app中
          document.getElementById(id).appendChild(dom);
        }
        var vm = new Vue({
          el: 'app',
          data: {
            text: 'hello world'
          }
        });
      </script>
      </body>
    </html>
    View Code

    七、总结

    关于双向绑定的实现,看了网上很多资料,开始看到是对Vue源码的解析,看的过程似懂非懂。后来找到参考资料1,然后自己跟着实现一遍,才理解许多。感谢这篇文章的作者,写的由浅入深,比较好理解。为了加深自己的理解,于是自己顺着这个思路写下这个笔记。本文主要了解了几种双向绑定的做法,然后先用原生JS,dom操作实现一个最简单双向绑定,在这个基础上进行改装,为减少dom操作,实现简单的Compile(编译HTML);接着为了实现数据监听,实现observe;最后为了实现数据的双向绑定实现订阅发布模式。

    虽然实现的比较简单,有很多功能没有考虑,不过这个过程还是可以理解到Vue实现双向绑定的原理。过程中,有思考:

    1. Vue的源代码中,用了文档碎片fragment作为真实节点的存储吗?

    之前有听说用VDOM,在Vue源代码中,也找过是否有创建文档碎片,结果没找到。看了参考资料4中,VDOM的介绍,好像是把节点用JS对象模拟。类似:

    ;模板
    <ul id='list'>
      <li class='item'>Item 1</li>
      <li class='item'>Item 2</li>
      <li class='item'>Item 3</li>
    </ul>
    
    ;js对象
    var element = {
      tagName: 'ul', // 节点标签名
      props: { // DOM的属性,用一个对象存储键值对
        id: 'list'
      },
      children: [ // 该节点的子节点
        {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
      ]
    }

    恩,这就又牵扯出模板了。先收住,我先尽量把简单的搞懂。

    2.Compile模块对v-model节点的解析,事件的绑定,我只实现简单的,特定的v-model,还有其它事件绑定如v-on等没有分析,看了别人的代码,情况一多起来,看得就有些吃力,希望后面自己会再来完善,给自己定一个这样的框架在这,代码github:戳这里

    参考资料:

    1.http://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

    2.https://segmentfault.com/a/1190000006599500

    3.https://github.com/fwing1987/MyVue

    4.http://www.kancloud.cn/zmwtp/vue2/149485

    5.http://blog.cgsdream.org/2016/11/05/vue-source-analysis-1/

  • 相关阅读:
    Apache开启Rewrite环境
    php 写入和读取序列化的cookie
    discuzx 制作单页面
    php 操作postgresql
    已安装php动态安装pdo_mysql
    PHP 异步调用 后台调用 持续执行 断开连接/浏览器
    ssh配置文件详解
    gdb 调试中No symbol in current context 故障定位
    Linking fails : relocation truncated to fit: R_X86_程序占用内存大于2GB所导致的问题
    谨慎使用单精度/双精度数值类型
  • 原文地址:https://www.cnblogs.com/wj204/p/6423478.html
Copyright © 2011-2022 走看看