解析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 原型和继承(Prototypes and Inheritance)
Apr 01 Javascript
firefox firebug中文入门教程 脚本之家新年特别版
Jan 02 Javascript
js不完美解决click和dblclick事件冲突问题
Jul 16 Javascript
instanceof和typeof运算符的区别详解
Jan 06 Javascript
AngularJS 实现按需异步加载实例代码
Oct 18 Javascript
js如何实现淡入淡出效果
Nov 18 Javascript
全面解析JavaScript里的循环方法之forEach,for-in,for-of
Apr 20 Javascript
JS中使用mailto实现将用户在网页中输入的内容传递到本地邮件客户端
Oct 08 Javascript
jquery.picsign图片标注组件实例详解
Feb 02 jQuery
jQuery实现动态添加和删除input框实例代码
Mar 26 jQuery
vue+eslint+vscode配置教程
Aug 09 Javascript
Vue 中获取当前时间并实时刷新的实现代码
May 12 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
在JavaScript中调用php程序
2009/03/09 PHP
CentOS系统中PHP安装扩展的方式汇总
2017/04/09 PHP
javascript 字符串连接的性能问题(多浏览器)
2008/11/18 Javascript
JavaScript页面刷新与弹出窗口问题的解决方法
2010/03/02 Javascript
JavaScript异步调用定时方法并停止该方法实现代码
2012/03/16 Javascript
jQuery-serialize()输出序列化form表单值的方法
2012/12/26 Javascript
jsp js鼠标移动到指定区域显示选项卡离开时隐藏示例
2013/06/14 Javascript
可以用鼠标拖动的DIV实现思路及代码
2013/10/21 Javascript
javascript事件函数中获得事件源的两种不错方法
2014/03/17 Javascript
jQuery元素的隐藏与显示实例
2015/01/20 Javascript
浅析JS动态创建元素【两种方法】
2016/04/20 Javascript
Bootstrap Table 搜索框和查询功能
2017/11/30 Javascript
JavaScript调用模式与this关键字绑定的关系
2018/04/21 Javascript
微信小程序登录换取token的教程
2018/05/31 Javascript
javascriptvoid(0)含义以及与&quot;#&quot;的区别讲解
2019/01/19 Javascript
Vue实现剪贴板复制功能
2019/12/31 Javascript
python排序方法实例分析
2015/04/30 Python
Scrapy爬虫实例讲解_校花网
2017/10/23 Python
Python切片操作实例分析
2018/03/16 Python
Python可视化mhd格式和raw格式的医学图像并保存的方法
2019/01/24 Python
详解python中的index函数用法
2019/08/06 Python
Python解压 rar、zip、tar文件的方法
2019/11/19 Python
opencv 图像轮廓的实现示例
2020/07/08 Python
HTML5边玩边学(2)基础绘图实现方法
2010/09/21 HTML / CSS
伦敦一家西班牙童装精品店:La Coqueta
2018/02/02 全球购物
美国儿童玩具、装扮和玩偶商店:Magic Cabin
2018/09/02 全球购物
新闻记者实习自我鉴定
2013/09/19 职场文书
小学生国旗下演讲稿
2014/04/25 职场文书
医学检验专业自荐信
2014/09/18 职场文书
2014领导班子四风剖析对照检查材料思想汇报
2014/09/20 职场文书
2014国庆节国旗下演讲稿(精选版)
2014/09/26 职场文书
2014年党风廉政建设工作总结
2014/11/19 职场文书
会议欢迎词范文
2015/01/27 职场文书
2016党员党章学习心得体会
2016/01/14 职场文书
2019学校请假条格式及范文
2019/06/25 职场文书
python 爬取华为应用市场评论
2021/05/29 Python