Vue.js双向绑定的实现

一、几种实现双向绑定的做法


目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)
  • 一般通过sub, pub的方式实现数据和视图的绑定监听
脏值检查(angular.js)
  • 通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,angular只有在指定的事件触发时进入脏值检测,大致如下:
    DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
    XHR响应事件 ( $http )
    浏览器Location变更事件 ( $location )
    Timer事件( $timeout , $interval )
    执行 $digest() 或 $apply()
数据劫持(vue.js)
  • vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。


    流程图解

二、双向绑定的实现


1、如何追踪变化

1.1、Object.defineProperty
  • 当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。
  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();
        }
      });
    }
1.2、实现Observer
  • 将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
function observe (obj, vm) {
    Object.keys(obj).forEach(function (key) {
        defineReactive(vm, key, obj[key]);
    });
}

2、数据初始化绑定

2.1 实现Compile
  • compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
    function nodeToFragment (node, vm) {
      var flag = document.createDocumentFragment();
      var child;
      // 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');
          }
        };

        new Watcher(vm, node, name, 'input');
      }
      // 节点类型为 text
      if (node.nodeType === 3) {
        if (reg.test(node.nodeValue)) {
          var name = RegExp.$1; // 获取匹配到的字符串
          name = name.trim();

          new Watcher(vm, node, name, 'text');
        }
      }
    }
2.2 实现Watcher
  • Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
    1、在自身实例化时往属性订阅器(dep)里面添加自己
    2、自身必须有一个update()方法
    3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调
    function Watcher (vm, node, name, nodeType) {
      Dep.target = this;
      this.name = name;
      this.node = node;
      this.vm = vm;
      this.nodeType = nodeType;
      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;
        }
      },
      // 获取 data 中的属性值
      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();
        });
      }
    }

二、最终效果

1、效果
最终效果
2、源码

源码传送门

三、最后

  • 如果有什么疑问,可以在下面评论或者私信我。
  • 如果您觉得有帮助,请给我点个,谢谢。

你可能感兴趣的:(Vue.js双向绑定的实现)