解析Vue2.0双向绑定实现原理


Posted in Javascript onFebruary 23, 2017

一、实现双向绑定的做法

前端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';

结果:

解析Vue2.0双向绑定实现原理

如果去掉 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>

实现效果如下:

解析Vue2.0双向绑定实现原理

上面例子直接用了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>

结果:

解析Vue2.0双向绑定实现原理

到这,我们做到了获取文本中真实的dom节点,然后分析节点的类型,并能处理节点中相应的变量如上面代码中的{{text}},最后渲染到页面中。接着我们需要和双向绑定联系起来,实现{{text}}响应式的数据绑定。

五、实现简单observe

简单的observe定义如下:

解析Vue2.0双向绑定实现原理

需要监控data的属性值,这个对象的某个值赋值,就会触发setter,这样就能监听到数据变化。然后注意vm.data[name]属性将改为vm[name]

解析Vue2.0双向绑定实现原理

完整代码如下:

<!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
    if(node.nodeType === 3) {
     if(reg.test(node.nodeValue)) {
      var name = RegExp.$1; // 获取匹配到的字符串
      name = name.trim();
      node.nodeValue = vm[name]; // 将data的值赋给该node
      // new Watcher(vm, node, name);
     }
    }
   },
  }
  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 observe(obj, vm) {
   Object.keys(obj).forEach(function(key) {
    defineReactive(vm, key, obj[key]);
   })
  }
   function Vue(options) {
   this.data = options.data;
   var data = this.data;
   observe(data, this);
   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>

结果:

解析Vue2.0双向绑定实现原理

到这,虽然set方法触发了,但是文本节点{{text}}的内容没有变化,要让绑定的文本节点同步变化,我们需要引入订阅发布模式。

六、订阅发布模式

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作

首先我们要一个收集订阅者的容器,定义一个Dep作为主题对象

解析Vue2.0双向绑定实现原理

然后定义订阅者Watcher

解析Vue2.0双向绑定实现原理

添加订阅者Watcher到主题对象Dep,发布者发出通知放到属性监听里面

解析Vue2.0双向绑定实现原理

最后需要订阅的地方

解析Vue2.0双向绑定实现原理

至此,比较简单地实现了我们第三步用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中
    }
    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
       new Watcher(vm, node, name, 'value');
      }
     };
    }
    //节点类型为text
    if(node.nodeType === 3) {
     if(reg.test(node.nodeValue)) {
      var name = RegExp.$1; // 获取匹配到的字符串
      name = name.trim();
      // node.nodeValue = vm[name]; // 将data的值赋给该node
      new Watcher(vm, node, name, 'nodeValue');
     }
    }
   },
  }
  function Dep() {
   this.subs = [];
  }
  Dep.prototype = {
   addSub: function(sub) {
    this.subs.push(sub);
   },
   notify: function() {
    this.subs.forEach(function(sub) {
     sub.update();
    })
   }
  }
  function Watcher(vm, node, name, type) {
   Dep.target = this;
   this.name = name;
   this.node = node;
   this.vm = vm;
   this.type = type;
   this.update();
   Dep.target = null;
  }

  Watcher.prototype = {
   update: function() {
    this.get();
    this.node[this.type] = this.value; // 订阅者执行相应操作
   },
   // 获取data的属性值
   get: function() {
    this.value = this.vm[this.name]; //触发相应属性的get
   }
  }
  function defineReactive (obj, key, val) {
   var dep = new Dep();
   Object.defineProperty(obj, key, {
    get: function() {
      //添加订阅者watcher到主题对象Dep
     if(Dep.target) {
      // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
      dep.addSub(Dep.target);
     }
     return val;
    },
    set: function (newVal) {
     if(newVal === val) return;
     val = newVal;
     console.log(val);
     // 作为发布者发出通知
     dep.notify();
    }
   })
  }
  function observe(obj, vm) {
   Object.keys(obj).forEach(function(key) {
    defineReactive(vm, key, obj[key]);
   })
  }

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

七、总结

关于双向绑定的实现,看了网上很多资料,开始看到是对Vue源码的解析,看的过程似懂非懂。后来找到参考资料1,然后自己跟着实现一遍,才理解许多。感谢这篇文章的作者,写的由浅入深,比较好理解。为了加深自己的理解,于是自己顺着这个思路写下这个笔记。本文主要了解了几种双向绑定的做法,然后先用原生JS,dom操作实现一个最简单双向绑定,在这个基础上进行改装,为减少dom操作,实现简单的Compile(编译HTML);接着为了实现数据监听,实现observe;最后为了实现数据的双向绑定实现订阅发布模式。

虽然实现的比较简单,有很多功能没有考虑,不过这个过程还是可以理解到Vue实现双向绑定的原理。过程中,有思考:

1. Vue的源代码中,用了文档碎片fragment作为真实节点的存储吗?

之前有听说用VDOM,在Vue源代码中,也找过是否有创建文档碎片,结果没找到。看了参考资料4中,VDOM的介绍,好像是把节点用JS对象模拟。类似:

模板

<ul id='list'>
 <li class='item'>Item 1</li>
 <li class='item'>Item 2</li>
 <li class='item'>Item 3</li>
</ul>

js对象

var element = {
 tagName: 'ul', // 节点标签名
 props: { // DOM的属性,用一个对象存储键值对
  id: 'list'
 },
 children: [ // 该节点的子节点
  {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
  {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
  {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
 ]
}

恩,这就又牵扯出模板了。先收住,我先尽量把简单的搞懂。

2.Compile模块对v-model节点的解析,事件的绑定,我只实现简单的,特定的v-model,还有其它事件绑定如v-on等没有分析,看了别人的代码,情况一多起来,看得就有些吃力,希望后面自己会再来完善,给自己定一个这样的框架在这.

代码:戳这里

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript的null和undefined区别示例介绍
Sep 15 Javascript
javascript操作表格排序实例分析
May 06 Javascript
Javascript对象Clone实例分析
Jun 09 Javascript
如何用js 实现依赖注入的思想,后端框架思想搬到前端来
Aug 03 Javascript
理解javascript中的严格模式
Feb 01 Javascript
基于BootStrap Metronic开发框架经验小结【一】框架总览及菜单模块的处理
May 12 Javascript
JavaScript的ExtJS框架中表格的编写教程
May 21 Javascript
ES6中的数组扩展方法
Aug 26 Javascript
移动端日期插件Mobiscroll.js使用详解
Dec 19 Javascript
详解JavaScript对象的深浅复制
Mar 30 Javascript
父组件中vuex方法更新state子组件不能及时更新并渲染的完美解决方法
Apr 25 Javascript
javascript设计模式 ? 解释器模式原理与用法实例分析
Apr 17 Javascript
bootstrap datetimepicker日期插件超详细使用方法介绍
Feb 23 #Javascript
js获取浏览器和屏幕的各种宽度高度
Feb 22 #Javascript
基于JS实现移动端向左滑动出现删除按钮功能
Feb 22 #Javascript
高效的jQuery代码编写技巧总结
Feb 22 #Javascript
JS拉起或下载app的实现代码
Feb 22 #Javascript
js实现带简单弹性运动的导航条
Feb 22 #Javascript
bootstrap fileinput 插件使用项目总结(经验)
Feb 22 #Javascript
You might like
php ci框架中加载css和js文件失败的解决方法
2014/03/03 PHP
zf框架的校验器InArray使用示例
2014/03/13 PHP
linux下安装php的memcached客户端
2014/08/03 PHP
修改Laravel5.3中的路由文件与路径
2016/08/10 PHP
PHP中$GLOBALS['HTTP_RAW_POST_DATA']和$_POST的区别分析
2017/07/03 PHP
PHP+ajax实现获取新闻数据简单示例
2018/05/08 PHP
php精度计算的问题解析
2019/06/21 PHP
js中巧用cssText属性批量操作样式
2011/03/13 Javascript
jquery中ajax学习笔记一
2011/10/16 Javascript
js计算两个时间之间天数差的实例代码
2013/11/19 Javascript
JavaScript作用域链示例分享
2014/05/27 Javascript
jQuery异步获取json数据方法汇总
2014/12/22 Javascript
使用Jquery实现每日签到功能
2015/04/03 Javascript
js实现的万能flv网页播放器代码
2016/04/30 Javascript
jQuery实现给input绑定回车事件的方法
2017/02/09 Javascript
详解Vue路由开启keep-alive时的注意点
2017/06/20 Javascript
在 Node.js 中使用 async 函数的方法
2017/11/17 Javascript
node使用Mongoose类库实现简单的增删改查
2018/11/08 Javascript
vue.js层叠轮播效果的实例代码
2018/11/08 Javascript
node.js中ws模块创建服务端和客户端,网页WebSocket客户端
2019/03/06 Javascript
JS实现移动端可折叠导航菜单(现代都市风)
2020/07/07 Javascript
vue中移动端调取本地的复制的文本方式
2020/07/18 Javascript
[59:48]DOTA2-DPC中国联赛 正赛 VG vs Magma BO3 第一场 1月26日
2021/03/11 DOTA
简单介绍Python中利用生成器实现的并发编程
2015/05/04 Python
python开发之字符串string操作方法实例详解
2015/11/12 Python
python 实现网上商城,转账,存取款等功能的信用卡系统
2016/07/15 Python
利用python画一颗心的方法示例
2017/01/31 Python
kali中python版本的切换方法
2019/07/11 Python
Python dict和defaultdict使用实例解析
2020/03/12 Python
自我鉴定思想方面
2013/10/07 职场文书
毕业典礼演讲稿
2014/05/13 职场文书
小学社团活动总结
2014/06/27 职场文书
2016中秋晚会开幕词
2016/03/03 职场文书
【海涛dota】偶遇拉娜娅 质量局德鲁伊第一视角解说
2022/04/01 DOTA
Redis 限流器
2022/05/15 Redis
使用Apache Camel表达REST服务的方法
2022/06/10 Servers