vue的数据双向绑定的实现

几种实现双向绑定的做法

1、发布者-订阅者模式(backbone.js)
2、脏值检查(angular.js)
3、数据劫持(vue.js)

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

思路整理

要实现mvvm的双向绑定,就必须实现一下几点:

  • 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅
    者;
  • 2、实现一个指令解析器Compile,对每个元素的指令进行扫描和解析,根据指令模版替换数据,以及绑定相应的更新函数。
  • 3、实现一个Watcher,作为Observer和Compile的桥梁,能够订阅并收到每个属性的变动通知,执行指令绑定相应的回调函数,从而更新视图。

代码实现

1、实现数据监听器Observer

    function observe (obj, vm){
      if (!data || typeof data !== 'object') {
          return;
      }
      //取出所有属性遍历
      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);
        }
      });
    }

2、实现指令解析器Compile。这里需要用到文档片段DocumentFragment,它可以包含多个子节点,当我们将它插入到DOM中时,只有他的子节点会插入到目标节点中。用DocumentFragement处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂在目标的所有子节点劫持(通过append方法,原DOM中的节点会被自动删除,所以是真的劫持啊~)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。

    /*编译模板*/
    function nodeToFragment(node, vm){
      var flag = document.createDocumentFragment();
      var child;
      while(child = node.firstChild) {
        compile(child);
        flag.appendChild(child);
      }
      return flag;
    }

    function compile (node, vm){
      var reg = /\{\{(.*)\}\}/;
      if(node.nodeType === 1){  //节点类型为元素
        var attr = node.attributes;
        for(var i = 0, alen = attr.length; i < alen; i++) {
          if(attr[i].nodeName == 'v-model' ){
            var name = attr[i].nodeValue; //获取v-model绑定的属性名
            // 对监听该node的input事件,当有输入时,把新值赋给vm的data
            node.addEventListener('input', function (e) {
              //给对应的data属性赋值,进而触发该属性的set方法
              vm[name] = e.target.value;
            })
            node.value = vm[name];
            node.removeAttribute('v-model');
          }
        }
      }
      if(node.nodeType === 3){ //节点类型为text
        if(reg.test(node.nodeValue)) {
          var name = RegExp.$1;
          name = name.trim();
          node.nodeValue = vm[name];
        }
      }
    }

由此实现了:文本框以及文本节点与vue实例中data属性的数据绑定,当输入框内容变化时,data属性中的数据同步变化。
接下来,需要实现data属性中的数据变化时,文本节点的内容同步变化。
3、这里插播一下订阅发布模式(subscribe&publish)
订阅发布模式定义了一种一对多的关系,让多个观察者同事监听某一个主题对象,这个主题对象的状态发生改变时,就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 =》 订阅者执行响应操作
为了要实现“data属性中的数据变化时,文本节点的内容同步变化”,当set方法触发后做的第二件事就是就是作为发布者发出通知,文本节点作为订阅者,在收到通知后执行响应的更新操作。
所以基本思路是:
1、在监听数据的过程中,为data中的每一个属性生成一个主题对象dep。
2、在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会自己添加到响应属性的dep中。
目前已经实现了:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的 set 方法
接下来实现:发出通知 dep.notify() => 触发订阅者的 update 方法 => 更新视图。

    /*发布订阅者*/
    function Watcher(vm, node, name){
      Dep.target = this;
      this.vm = vm;
      this.node = node;
      this.name = name;
      this.update();
      Dep.target = null;
    }
    Watcher.prototype = {
      update: function(){
        this.get();
        if (this.nodeType == 'text') {
          this.node.nodeValue = this.value;
        }
        if (this.nodeType == 'input') {
          this.node.value = this.value;
        }
      },
      get: function(){
        this.value = this.vm[this.name];  //触发响应属性的get;
      }
    }
    function Dep(){
      this.subs = [];
    }
    Dep.prototype  = {
      addSub: function(sub){
        this.subs.push(sub);
      },
      notify: function(){
        this.subs.forEach(function(sub){
          sub.update();
        })
      }
    }

为了给每一个属性生成一个主题对象,所以原来的observe新增new Dep(),为了在属性改变时发出通知,还需要在set方法里执行dep.notify()方法;

3347801-e7df471de2cde051.png
image.png

在编译HTML时,为每一个数据绑定的节点生成一个订阅者。


3347801-6a1553e9ff30244b.png
image.png

完整代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>双向绑定</title>
</head>
<body>
<div id="app">
     <input type="text" v-model="text">
     {{ text }}
</div>

<script type="text/javascript">
    /*监听数据*/
    function observe (obj, vm){
      if (!obj || typeof obj !== 'object') {
          return;
      }
      //取出所有属性遍历
      Object.keys(obj).forEach(function(key) {
        defineReactive(vm, key , obj[key]);
      });
    }
    function defineReactive(obj, key, val){
      //为每一个属性生成一个主题对象。
      var dep = new Dep();
      Object.defineProperty(obj, key, {
        get: function(){
          // 添加订阅者 watcher 到主题对象 Dep
          if(Dep.target) dep.addSub(Dep.target);
          return val;
        },
        set: function(newVal){
          if(newVal === val) return;
          val = newVal;
          dep.notify(); //数据改变时发出通知
          console.log(val);
        }
      });
    }

    /*编译模板*/
    function nodeToFragment(node, vm){
      var flag = document.createDocumentFragment();
      var child;
      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, alen = attr.length; i < alen; i++) {
          if(attr[i].nodeName == 'v-model' ){
            var name = attr[i].nodeValue; //获取v-model绑定的属性名
            // 对监听该node的input事件,当有输入时,把新值赋给vm的data
            node.addEventListener('input', function (e) {
              //给对应的data属性赋值,进而触发该属性的set方法
              vm[name] = e.target.value;
            })
            // node.value = vm[name];
            node.removeAttribute('v-model');
          }
        }
        new Watcher(vm, node, name, 'input');
      }
      if(node.nodeType === 3){ //节点类型为text
        if(reg.test(node.nodeValue)) {
          var name = RegExp.$1;
          name = name.trim();
          // node.nodeValue = vm[name];
          new Watcher(vm, node, name, 'text');
        }
      }
    }
    Watcher.prototype = {
      update: function(){
        this.get();
        if (this.nodeType == 'text') {
          this.node.nodeValue = this.value;
        }
        if (this.nodeType == 'input') {
          this.node.value = this.value;
        }
      },
      get: function(){
        this.value = this.vm[this.name];  //触发响应属性的get;
      }
    }
    /*发布订阅者*/
    function Watcher(vm, node, name, nodeType){
      Dep.target = this;
      this.vm = vm;
      this.node = node;
      this.nodeType = nodeType;
      this.name = name;
      this.update();
      Dep.target = null;
    }

    function Dep(){
      this.subs = [];
    }
    Dep.prototype  = {
      addSub: function(sub){
        this.subs.push(sub);
      },
      notify: function(){
        this.subs.forEach(function(sub){
          sub.update();
        })
      }
    }
    var vm = new Vue({
      el: 'app',
      data: {
        text: 'hello world'
      }
    })
    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); 
    }
</script>
</body>
</html>

参考:
https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension
https://github.com/DMQ/mvvm

猜你喜欢

转载自blog.csdn.net/weixin_34292402/article/details/87639532