原生js实现一个简单的vue的数据双向绑定

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

所以我们要先做好下面3步:

1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

1.实现一个Observer

Observer是一个数据监听器,主要依赖于Object.defineProperty()方法,而这个方法在ie8及以下存在兼容问题,请看(MDN defineProperty)所以如vue官网所说:

兼容性

Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有(兼容 ECMAScript 5 的浏览器。)

正因为这个方法,我们就可以利用Obeject.defineProperty()来监听属性变动 那么就可以把需要observer的数据对象进行递归遍历,给他的每个属性都可以加上get,set。

而当给这个对象的某个值赋值操作的时候,就会触发setter,那么就能监听到了数据变化。

  1. function observer(data) {
  2.    // 当不是对象的时候,退出
  3. if (!data || typeof data !== 'object') {
  4. return;
  5. }
  6. // 取出所有属性遍历
  7. Object.keys(data).forEach(function(key) {
  8.      // 给每个属性加上get,set
  9. defineReactive(data, key, data[key]);
  10. });
  11. };
  12. function defineReactive(data, key, val) {
  13. observer(val); // 监听子属性
  14. Object.defineProperty(data, key, {
  15. enumerable: true, // 可遍历
  16. configurable: false, // 不能修改,删除
  17. get: function() {
  18. return val;
  19. },
  20. set: function(newVal) {
  21. val = newVal;
  22. console.log("已改变 "+newVal);
  23. }
  24. });
  25. }

测试一下:

  1. var obj={
  2. name:'aaaaa',
  3. book:{
  4. name:'bbbbbb'
  5. }
  6. }
  7. observer(obj)
  8. obj.name='cc' //已改变 cc
  9. obj.book.name='dd' //已改变 dd

现在已经监听了数据中的属性变化了,而接下来就是在代码中加入发布者-订阅者模式,关于这个设计模式不懂的可以看这里(js设计模式-发布订阅模式

首先我们先创造一个订阅器Dep,他是用来收集订阅者Watcher的,然后在属性发生变化的时候执行对应订阅者的更新函数update 。

在上面的 defineReactive 函数最后加入下面代码:

  1. function Dep() {
  2. this.subs = [];//存放消息数组
  3. }
  4. Dep.prototype = {
  5. addSub: function(sub) {  //增加订阅者函数
  6. this.subs.push(sub);
  7. },
  8. notify: function() {    //发布消息函数
  9. this.subs.forEach(function(sub) {
  10. sub.update();   //这里是订阅者的更新方法
  11. });
  12. }
  13. };

然后修改上面的 defineReactive 函数:

  1. function defineReactive(data, key, val) {
  2. var dep = new Dep(); //实例化一个订阅器
  3. observer(val); // 监听子属性
  4. Object.defineProperty(data, key, {
  5. enumerable: true, // 可遍历
  6. configurable: false, // 不能修改,删除
  7. get: function() {
  8. return val;
  9. },
  10. set: function(newVal) {
  11. if (val === newVal){return} //当前后数值相等,不做改变
  12. val = newVal;
  13. dep.notify(); //当前后数值变化,这时就通知订阅者了
  14. console.log("已改变 "+newVal);
  15. }
  16. });
  17. }

在vue里面针对data这个对象的处理,区分了数组和对象,我们这只考虑对象这一种情况,更多(vue-数组处理)正由于这种处理,所以vue对于:

注意事项

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

这种情况,设计了 vm.$set 这个实例方法来弥补这方面限制带来的不便。

2.实现Watcher

在第一步我们实现了订阅器,这一步实现订阅者,从上面代码看,在dep 调用 notify() 方法的时候,我们就该去遍历订阅者Watcher了,并且调用他自己的update()方法,

先实现订阅者对象,如下:

  1. function Watcher (vm, exp, cb){
  2. this.cb = cb;
  3. this.vm = vm;
  4. this.exp = exp;
  5. this.value = this.get();//初始化的时候就调用
  6. }
  7.  
  8. Watcher.prototype={
  9. // 只有在订阅者Watcher初始化的时候才需要添加订阅者
  10. get:function(){
  11. Dep.target = this; // 在Dep.target缓存下订阅者
  12. var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
  13. Dep.target = null; // 释放订阅者
  14. return value;
  15. },
  16. // dep.subs[i].notify() 会执行到这里
  17. update:function() {
  18. this.run();
  19. },
  20. run:function() {
  21. // 执行 get()获得value ,call更改cb的this指向 。
  22. var value = this.vm.data[this.exp];
  23. var oldVal = this.value;
  24. if (value !== oldVal) {
  25. this.value = value;
  26. this.cb.call(this.vm, value, oldVal);
  27. }
  28. }
  29. }

而在这个时候,我们需要通过Dep的addSub(),将Watcher加进去,在Watcher的构造函数中会调用这句话

  1. this.value = this.get();//初始化的时候就调用

所以我们可以在 defineReactive 函数中做个调整,修改一下Object.defineProperty的get要调用的函数,来对应Watcher构造函数的这个get函数。

而在这里需要判断一下:是不是Watcher的构造函数在调用,如果是,说明他就是这个属性的订阅者。

这里就会用到    Dep.target   这样一个全局唯一的变量,用来判断。代码如下:

  1. function defineReactive(data, key, val) {
  2. var dep = new Dep(); //实例化一个订阅器
  3. observer(val); // 监听子属性
  4. Object.defineProperty(data, key, {
  5. enumerable: true, // 可遍历
  6. configurable: false, // 不能修改,删除
  7. get: function() {
  8. // 如果这个属性存在,说明这是watch 引起的
  9. if(Dep.target){
  10. // 那我调用dep.addSub把这个订阅者加入订阅器里面
  11. dep.addSub(Dep.target)
  12. }
  13. return val;
  14. },
  15. set: function(newVal) {
  16. if (val === newVal){return} //当前后数值相等,不做改变
  17. val = newVal;
  18. dep.notify(); //当前后数值变化,这时就通知订阅者了
  19. console.log("已改变 "+newVal);
  20. }
  21. });
  22. }

  最后在Dep函数后面加上

  1.         Dep.target = null;//释放每一个订阅者

写到这里我们可以写个简单的例子来测试一下:

  先写个函数将我们的Observer和Watcher关联起来:

  1. function pvp(data,el,exp){
  2. this.data=data;
  3. observer(data); //给data每个属性加上get,set
  4. el.innerHTML=this.data[exp]; //粗暴的直接绑定,测试一下效果
  5. new Watcher(this,exp,function(val){
  6. el.innerHTML = val; //调用Watcher直接赋值
  7. })
  8. return this
  9. }

  在html中写下:

  1.     <h1 id="name">{{name}}</h1>

  js中写下:

  1. var ele = document.querySelector('#app');
  2. var pvp = new pvp(
  3.   {test: 'hello pvp'},
  4.   ele,
  5.   'test'
         );

  结果如下:

好了,下面我们来实现对dom节点的解析,可以让pvp写起来更像vue的写法

3.实现Compile     

解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,这个环节需要对dom操作比较频繁,

所以可以用一个通用的办法,先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理。请看(空文档对象

  1. //创造一个空白节点
  2. nodeToFragment: function (el) {
  3. var fragment = document.createDocumentFragment();
  4. var child = el.firstChild;
  5. while (child) {
  6. // 将Dom每个元素都移入fragment中
  7. fragment.appendChild(child);
  8. child = el.firstChild;
  9. }
  10. return fragment;
  11. },

而在这里我们需要实现一个 Compile 方法,代码如下:

  1. function Compile(el, vm) {
  2. this.vm = vm;
  3. this.el = document.querySelector(el);
  4. this.fragment = null;
  5. this.init(); //初始化一个方法,直接调用解析节点
  6. }
  7. Compile.prototype = {
  8. init: function () {
  9. if (this.el) {
  10. this.fragment = this.nodeToFragment(this.el); //调用了上面的方法,把元素放入并返回
  11. this.compileElement(this.fragment);//对这个里面的元素解析
  12. this.el.appendChild(this.fragment);//再重新放回去
  13. } else {
  14. console.error("找不到节点")
  15. }
  16. },
  17. //创造一个空白节点
  18. nodeToFragment: function (el) {
  19. var fragment = document.createDocumentFragment();
  20. var child = el.firstChild;
  21. while (child) {
  22. // 将Dom每个元素都移入fragment中
  23. fragment.appendChild(child);
  24. child = el.firstChild;
  25. }
  26. return fragment;
  27. },
  28. // 解析节点
  29. compileElement: function (el) {
  30. var childNodes = el.childNodes;
  31. var self = this;
  32. [].slice.call(childNodes).forEach(function(node) {
  33. var reg = /\{\{(.*)\}\}/;
  34. var text = node.textContent;
  35. if (node.nodeType == 1) { //如果是元素节点
  36. self.compileFirst(node);
  37. } else if (node.nodeType == 3 && reg.test(text)) { //如果是文本节点
  38. self.compileText(node, reg.exec(text)[1]);
  39. }
  40. if (node.childNodes && node.childNodes.length) { //如果下面还有子节点,继续循环
  41. self.compileElement(node);
  42. }
  43. });
  44. },
  45. //如果是元素节点
  46. compileFirst: function(node) {
  47. var nodeAttrs = node.attributes;
  48. var self = this;
  49. Array.prototype.forEach.call(nodeAttrs, function(attr) {
  50. var attrName = attr.name;
  51. var exp = attr.value;
  52. if (attrName='p-model') { //当这个属性为p-model的时候就解析model
  53. self.compileModel(node, self.vm, exp);
  54. }
  55. });
  56. },
  57. //如果是文本节点
  58. compileText: function(node, exp) {
  59. var self = this;
  60. var initText = this.vm[exp];
  61. this.updateText(node, initText);
  62. new Watcher(this.vm, exp, function (value) {
  63. self.updateText(node, value);//通知Watcher,开始订阅
  64. });
  65. },
  66. //解析p-model
  67. compileModel: function (node, vm, exp) {
  68. var self = this;
  69. var val = this.vm[exp];
  70. this.modelUpdater(node, val);
  71. new Watcher(this.vm, exp, function (value) {
  72. self.modelUpdater(node, value); //通知Watcher,开始订阅
  73. });
  74. node.addEventListener('input', function(e) {
  75. var newValue = e.target.value;
  76. if (val === newValue) {
  77. return;
  78. }
  79. self.vm[exp] = newValue;
  80. val = newValue;
  81. });
  82. },
  83. updateText: function (node, value) {
              //这里是直接替换文本节点
  84. node.textContent = typeof value == 'undefined' ? '' : value;
  85. },
  86. modelUpdater: function(node, value, oldValue) {
  87. //如果不存在就返回空,这里是更新model
  88. node.value = typeof value == 'undefined' ? '' : value;
  89. }
  90. }

然后我们在对上面的关联函数  pvp  进行修改主要是对 data 用 defineProperty 方法进行再一次封装,方便我们使用 xx.data 的写法去调用属性,在对这个函数进行修改,代码如下:

  1. function Pvp(options){
  2. var self = this;
  3. this.data=options.data;
  4.  
  5. Object.keys(this.data).forEach(function(key) {
  6. self.proxyKeys(key);
  7. });
  8. observer(this.data); //给data每个属性加上get,set
  9. new Compile(options.el,this)
  10. return this
  11. }
  12. Pvp.prototype = {
  13. proxyKeys: function (key) {
  14. var self = this;
  15. Object.defineProperty(this, key, {
  16. enumerable: false,
  17. configurable: true,
  18. get: function getter () {
  19. return self.data[key];
  20. },
  21. set: function setter (newVal) {
  22. self.data[key] = newVal;
  23. }
  24. });
  25. }
  26. }

  最后在我们的html的script中写下:

  1. var src=new Pvp({
  2. el: '#app',
  3. data: {
  4. test: 'hello world',
  5. }
  6. });

  打完收工!!!!  最后点(源码)获取源码

原生js实现 vue的数据双向绑定的更多相关文章

  1. Vue的数据双向绑定和Object.defineProperty()

    Vue是前端三大框架之一,也被很多人指责抄袭,说他的两个核心功能,一个数据双向绑定,一个组件化分别抄袭angular的数据双向绑定和react的组件化思想,咱们今天就不谈这种大是大非,当然我也没到达那 ...

  2. vue中数据双向绑定的实现原理

    vue中最常见的属v-model这个数据双向绑定了,很好奇它是如何实现的呢?尝试着用原生的JS去实现一下. 首先大致学习了解下Object.defineProperty()这个东东吧! * Objec ...

  3. 【Vue】-- 数据双向绑定的原理 --Object.defineProperty()

    Object.defineProperty()方法被许多现代前端框架(如Vue.js,React.js)用于数据双向绑定的实现,当我们在框架Model层设置data时,框架将会通过Object.def ...

  4. 对象的属性类型 和 VUE的数据双向绑定原理

    如[[Configurable]] 被两对儿中括号 括起来的表示 不可直接访问他们 修改属性类型:使用Object.defineProperty()  //IE9+  和标准浏览器  支持 查看属性的 ...

  5. vue实现数据双向绑定的原理

    一.知识准备Object.defineProperty( )方法可以直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象.Object.defineProperty(obj,pr ...

  6. 利用JS实现vue中的双向绑定

    Vue 已经是主流框架了 它的好处也不用多说,都已经是大家公认的了 那我们就来理解一下Vue的单向数据绑定和双向数据绑定 然后再使用JS来实现Vue的双向数据绑定 单向数据绑定 指的是我们先把模板写好 ...

  7. 一、vue的数据双向绑定的实现

    响应式系统 一.概述 Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图 ...

  8. vue中数据双向绑定注意点

    最近一个vue和element的项目中遇到了一个问题: 动态生成的对象进行双向绑定是失败 直接贴代码: <el-form :model="addClass" :rules=& ...

  9. Vue的数据双向绑定原理——Object-defineProperty

    一.定义 ①方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象. ②vue.js的双向数据绑定就是通过Object.defineProperty方法实现的,俗称属性拦截 ...

随机推荐

  1. 如何在Linux下的C++文件使用GDB调试

    首先在Linux下写好一个.Cpp的文件. #include<stdio.h> #include<stdlib.h> using namespace std; void sho ...

  2. 在Linux系统中使用Vim读写远程文件

    大家好,我是良许. 今天我们讨论一个 Vim 使用技巧--用 Vim 读写远程文件.要实现这个目的,我们需要使用到一个叫 netrw.vim 的插件.从 Vim 7.x 开始,netrw.vim 就被 ...

  3. math库常用函数

  4. luogu P3409 值日班长值周班长 exgcd

    LINK:值日班长值周班长 题目描述非常垃圾. 题意:一周5天 每周有一个值周班长 每天有一个值日班长 值日班长日换 值周班长周换. 共n个值日班长 m个值周班长 A是第p个值日班长 B是第q个值日班 ...

  5. Android JNI之静态注册

    这篇说静态注册,所谓静态注册,就是native的方法是直接通过方法名的规定格式和Java端的声明处代码对应起来的,其对应规则如下: JNIEXPORT <返回值> JNICALL Java ...

  6. HA模式下的java api访问要点

    在非HA架构的HDFS中,客户端要通过java接口调用HDFS时一般是在JobRunner的类中按照下面的方式: 因为nodename只有一个节点所以会在代码中显式的指明要连接哪一个节点:但是在HA模 ...

  7. 40行Python制作超炫酷动态排序图,有了它高逼格PPT再也不愁!

        本文首发于量化投资与机器学习 转载于  https://mp.weixin.qq.com/s/KaB_7oXZf0_IV97y0pRPmQ 前言 最近,这种动态排序条形图视频超级火,如下图: ...

  8. 声明式事务xml Spring

    !--JDBC事务管理器--><bean id="tansactionManager" class="org.springframework.jdbc.dat ...

  9. 【NOIP2013】火柴排队 题解(贪心+归并排序)

    前言:一道水题. ----------------------- 题目链接 题目大意:给出数列$a_i$和$b_i$,问使$\sum_{i=1}^n (a_i-b_i)^2$最小的最少操作次数. 首先 ...

  10. python6.2类的封装

    class Card(object): def __init__(self,num,pwd,ban): self.num=num#卡号 self.pwd=pwd#密码 self.__ban=ban#余 ...