Vue.js双向绑定原理

Posted Z皓

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue.js双向绑定原理相关的知识,希望对你有一定的参考价值。

Vue.js最核心的功能有两个,一个是响应式的数据绑定系统,另一个是组件系统。本文仅仅探究双向绑定是怎样实现的。先讲涉及的知识点,再用简化的代码实现一个简单的hello world示例。

 

一、访问器属性

访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <script>
      var obj = {};
      Object.defineProperty(obj, \'hello\', {
        get: function() {
          console.log(\'get方法被调用了\');
        },
        set: function(val) {
          console.log(\'set方法被调用了,参数是\' + val);
        }
      });
      obj.hello; //get方法被调用了
      obj.hello = \'abc\'; //set方法被调用了,参数是abc
    </script>
  </body>
</html>
View Code

get和set方法内部的this都指向obj,这意味着get和set函数可以操作对象内部的值。另外,访问器属性的会“覆盖”同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。

 

二、极简的双向绑定实现

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <input type="text" id="a" />
    <span id="b"></span>
    <script>
      var obj = {};
      Object.defineProperty(obj, \'hello\', {
        set: function(newval) {
          document.getElementById(\'a\').value = newval;
          document.getElementById(\'b\').innerHTML = newval
        }
      });
      document.addEventListener(\'keyup\', function(e) {
        obj.hello = e.target.value;
      })
    </script>
  </body>
</html>
View Code

此例实现的效果是:随着文本框输入文字的变化,span中会同步显示相同的内容。在js或者在控制台上显式的修改obj.hello的值,视图会相应的更新。这样就实现了model=>view以及view=>model的双向绑定。

以上就是Vue实现双向绑定的基本原理。

 

三、分解任务

上述示例仅仅是为了说明原理,我们最终要实现的是:

<div id="app">
  <input type="text" v-model="text">
  {{ text }}
</div>

var vm = new Vue({
  el:\'#app\',
  data:{
    text:\'hello world\'
  }
})

首先将该任务分成几个子任务:

1、输入框以及文本节点与data中的数据绑定;

2、输入框内容变化时,data中的数据同步变化,即view =>model的变化;

3、data中的数据变化时,文本节点的内容同步变化,即model =>view的变化;

要实现任务1,需要对DOM进行编译,这里有一个知识点:DocumentFragment。

 

四、DocumentFragment

DocumentFragment(文档片段)可以看做节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过append方法,DOM中的节点会被自动删除)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <div id="app">
      <input type="text" id="a" />
      <span id="b"></span>
    </div>
    <script>
      var dom = nodeToFragment(document.getElementById(\'app\'));
      console.log(dom);

      function nodeToFragment(node) {
        var flag = document.createDocumentFragment();
        var child;
        while(child == node.firstChild) {
          flag.appendChild(child); //劫持node的所有子节点
        }
        return flag;
      }

      document.getElementById(\'app\').appendChild(dom); //返回到app中
    </script>
  </body>
</html>
View Code

 

五、数据初始化绑定

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Two-way-data-binding</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="text"> {{ text }}
    </div>

    <script>
      function compile(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.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.data[name]; //将data的值赋给该node
          }
        }
      }

      function nodeToFragment(node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        // 所有表达式必然会返回一个值,赋值表达式亦不例外
        // 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法
        // 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
        // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
        while(child = node.firstChild) {
          compile(child, vm);
          flag.appendChild(child); // 将子节点劫持到文档片段中
        }
        return flag;
      }

      function Vue(options) {
        this.data = options.data;
        var id = options.el;
        var dom = nodeToFragment(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

以上代码实现了任务一,我们可以看到,hello world已经呈现在输入框和文本节点中。

 

六、响应式的数据绑定

再来看任务2的是实现思路:当我们在输入框输入数据的时候,首先触发input事件或者keyup、change事件,在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。我们会利用defineProperty将data中的text设置为vm的访问器属性,因此给vm.text赋值就会触发set方法。在set方法中主要做两件事,第一是更新属性的值,第二留到任务3来说。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Two-way-data-binding</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="text"> {{ text }}
    </div>

    <script>
      function observe(obj, vm) {
        Object.keys(obj).forEach(function(key) {
          defineReactive(vm, key, obj[key]);
        })
      }

      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 nodeToFragment(node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        // 所有表达式必然会返回一个值,赋值表达式亦不例外
        // 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法
        // 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
        // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
        while(child = node.firstChild) {
          compile(child, vm);
          flag.appendChild(child); // 将子节点劫持到文档片段中
        }
        return flag;
      }

      function compile(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
          }
        }
      }

      function Vue(options) {
        this.data = options.data;
        var data = this.data;
        observe(data, this);
        var id = options.el;
        var dom = nodeToFragment(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

任务2也就完成了,text属性值会与输入框的内容同步变化(打开浏览器后台进行查看)。

 

七、订阅/发布模式(subscribe&publish)

text属性变化了,set方法触发了,但是文本节点的内容没有变化。如何让同样绑定到text的文本节点也同步变化呢?这里又有一个知识点:订阅发布模式。

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。发布者发出通知 =>主题对象收到通知并推送给订阅者 =>订阅者执行相应操作。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Two-way-data-binding</title>
  </head>
  <body>
    <script>
      //一个发布者publisher
      var pub =Vue.js双向绑定的实现原理

Vue.js双向绑定的实现原理

Vue.js双向绑定的实现原理

Vue.js实现双向绑定的原理

VUE底层原理之数据双向绑定

vue双向绑定的原理