一、实现双向绑定的做法
前端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()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
由此可见,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;
  obj.hello='111';
</div>
结果:

如果去掉 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>
</div>
实现效果如下:

上面例子直接用了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');
      }
     };
    }
    //节点类型为textif(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>
</div>
结果:

到这,我们做到了获取文本中真实的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
  
 
