首页 课程中心 问答&分享 读书 公众号框架 HUI H.JS 开放源 登录 & 注册
 Vue.js 数据双向绑定原理内核深度解析 ( Object.defineProperty() )
[ 淡淡的风 ] 发布于 : 2017-12-05  浏览 :  次

三种实现双向绑定(响应式)的做法

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

1. 发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value),这种方式现在毕竟太low了,我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

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

DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
XHR响应事件 ( $http )
浏览器Location变更事件 ( $location )
Timer事件( $timeout , $interval )
执行 $digest() 或 $apply()

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


思路整理

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳这里

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

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

如图所示:




实现代码(框架名称以 hui为例):

(function (g, hui) {g.hui = hui;})(this, function(obj){
  this.verson = '2.5.1';
  this.el = obj.el;
  this.data = {};
  this.nodes = {};
  var _self = this
  if(!obj.data){obj.data = {};}
  Object.keys(obj.data).forEach(function(key){
    defineReactive(_self.data, key, obj.data[key]);
  });
  function defineReactive(data, key, val) {
    Object.defineProperty(_self.data, key, {
      enumerable: true,
      configurable: false,
      get: function() {return val;},
      set: function(newVal){
        val = newVal;
        var cnode = _self.nodes[key];
        if(cnode){for(var i = 0;  i < cnode.length; i++){cnode[i].textContent = newVal;}}
      }
    });
  }
  //设置变量值
  this.setData = function(objs){Object.keys(objs).forEach(function(k){_self.data[k] = objs[k];});}
  this.isElementNode = function(node) {return node.nodeType == 1;}
  this.isTextNode = function(node){return node.nodeType == 3;}
  this.compileText = function(node, varName){
    var varName = varName.substring(2, varName.length - 2);
    console.log(varName);
    var value = _self.data[varName];
    node.textContent = typeof value == 'undefined' ? '' : value;
    if(!this.nodes[varName]){this.nodes[varName] = [];}
    this.nodes[varName].push(node);
  }
  this.compileFor = function(node, varName){
    var value = _self.data[varName];
    if(!value){node.parentNode.removeChild(node); return false;}
    var parentDom = node.parentNode;
    for(var i = 0; i < value.length; i++){
      var newNode = node.cloneNode(true);
      newNode.removeAttribute('hui-for');
      var html = newNode.innerHTML;
      var newHtml = html.replace(/{{item}}/g, '{{'+varName+'['+i+']}}');
      console.log(newHtml);
      newNode.innerHTML = newHtml;
      parentDom.appendChild(newNode);
    }
    parentDom.removeChild(node);
    //编译列表
    this.compileSons(parentDom);
  }
  this.compileSons = function(el){
    var childNodes = el.childNodes;
    [].slice.call(childNodes).forEach(function(node){
      var reg = /{{.*?}}/g;
      if (_self.isElementNode(node)){
        _self.compileSons(node);
      }else if (_self.isTextNode(node)){
        var regs = node.textContent.match(reg);
        if(regs){
          if(regs.length == 1){_self.compileText(node, regs[0]);}else{
            var ortherText = node.textContent.split(reg), newTextNodes = [];
            for(var  i = 0; i < ortherText.length; i++){
              node.parentNode.insertBefore(document.createTextNode(ortherText[i]), node);
              if(regs[i]){
                var cnode = document.createTextNode(regs[i]);
                node.parentNode.insertBefore(cnode, node);
                _self.compileText(cnode, regs[i]);
              }
            }
            node.parentNode.removeChild(node);
          }
        }
      }
    });
  }
  this.compile = function () {
    this.els = document.querySelector(this.el);
    if(this.els  == null){return ;}
    this.fragment = document.createDocumentFragment();
    var child;
    while(child = this.els.firstChild){this.fragment.appendChild(child);}
    this.compileSons(this.fragment);
    this.els.appendChild(this.fragment);
  }
  this.compile();
});

html 演示代码

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
{{myname}}...{{test}}123..<br /> {{age}}...

<span>{{test}}</span>

<button onclick="t();">test</button>
<script type="text/javascript" src="hui.js"></script> <script type="text/javascript"> var app = new hui({ el : "body", data : { myname : 'name', age : 18, test : 'test' } }); function t(){ app.setData({myname : 'name new ', test:'test new'}); } </script> </body> </html>


说明:
以上代码已经完整实现了vue的数据双向绑定,对深度理解Vue.js原理有很大帮助,感兴趣的同学可以自己尝试一下!

教程更新
More...