解析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 相关文章推荐
ImageZoom 图片放大镜效果(多功能扩展篇)
Apr 14 Javascript
jQuery对象[0]是什么含义?
Jul 31 Javascript
javascript full screen 全屏显示页面元素的方法
Sep 27 Javascript
Css3制作变形与动画效果
Jul 24 Javascript
bootstrap输入框组代码分享
Jun 07 Javascript
AngularJS自定义插件实现网站用户引导功能示例
Nov 07 Javascript
js实现百度登录框鼠标拖拽效果
Mar 07 Javascript
Angularjs 实现动态添加控件功能
May 25 Javascript
微信小程序--组件(swiper)详细介绍
Jun 13 Javascript
Vue组件化通讯的实例代码
Jun 23 Javascript
JavaScript实现的超简单计算器功能示例
Dec 23 Javascript
函数式编程入门实践(一)
Apr 20 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
一个数据采集类
2007/02/14 PHP
PHP 基本语法格式
2009/12/15 PHP
PHP提示Deprecated: mysql_connect(): The mysql extension is deprecated的解决方法
2014/08/28 PHP
php实现获取文章内容第一张图片的方法
2014/11/04 PHP
WordPress中访客登陆实现邮件提醒的PHP脚本实例分享
2015/12/14 PHP
PHP载入图像imagecreatefrom_gif_jpeg_png系列函数用法分析
2016/11/14 PHP
浅析PHP 中move_uploaded_file 上传中文文件名失败
2019/04/17 PHP
关于Laravel参数验证的一些疑与惑
2019/11/19 PHP
PHP isset()及empty()用法区别详解
2020/08/29 PHP
javascript之水平横向滚动歌词同步的应用
2007/05/07 Javascript
javascript分页代码(当前页码居中)
2012/09/20 Javascript
js离开或刷新页面检测(且兼容FF,IE,Chrome)
2014/03/05 Javascript
javascript Slip.js实现整屏滑动的手机网页
2015/11/25 Javascript
JS实现的倒计时效果实例(2则实例)
2015/12/23 Javascript
Bootstrap编写一个兼容主流浏览器的受众门户式风格页面
2016/07/01 Javascript
JS获取一个未知DIV高度的方法
2016/08/09 Javascript
JS设计模式之惰性模式(二)
2017/09/29 Javascript
angularJs提交文本框数据到后台的方法
2018/10/08 Javascript
JS/HTML5游戏常用算法之路径搜索算法 A*寻路算法完整实例
2018/12/14 Javascript
用原生 JS 实现 innerHTML 功能实例详解
2019/04/03 Javascript
Vue3 实现双盒子定位Overlay的示例
2020/12/22 Vue.js
python实现上传样本到virustotal并查询扫描信息的方法
2014/10/05 Python
在Python的Django框架中实现Hacker News的一些功能
2015/04/17 Python
Python过滤列表用法实例分析
2016/04/29 Python
Python学习小技巧之列表项的排序
2017/05/20 Python
Python表示矩阵的方法分析
2017/05/26 Python
详解python基础之while循环及if判断
2017/08/24 Python
python操作ini类型配置文件的实例教程
2020/10/30 Python
CSS3制作3D立方体loading特效
2020/11/09 HTML / CSS
涉外文秘个人求职的自我评价
2013/10/07 职场文书
购房意向书
2014/04/01 职场文书
社会调查研究计划书
2014/05/01 职场文书
反腐倡廉演讲稿
2014/05/22 职场文书
教师四风自我剖析材料
2014/09/30 职场文书
欠款起诉书范文
2015/05/19 职场文书
十大最强奥特曼武器:怪兽战斗仪在榜,第五奥特之父只使用过一次
2022/03/18 日漫