使用ES6的新特性Proxy来实现一个数据绑定实例

开发 前端
作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过jQuery之类的工具手动完成这些功能,但是当数据量非常大的时候,这些手动的工作让我非常痛苦。直到使用了VueJS,这些痛苦才得以终结。

[[176173]]

作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过jQuery之类的工具手动完成这些功能,但是当数据量非常大的时候,这些手动的工作让我非常痛苦。直到使用了VueJS,这些痛苦才得以终结。

VueJS的其中一个卖点,就是“数据绑定”。使用者无需关心数据是怎么绑定到dom上面的,只需要关注数据就好,因为VueJS已经自动帮我们完成了这些工作。

这真的非常神奇,我不可救药地爱上了VueJS,并且把它用到我自己的项目当中。随着使用的深入,我更加想知道它深入的原理是什么。

VueJS是如何进行数据绑定的?

通过阅读官方文档,我看到了下面这段话:

把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。

关键词是Object.definProperty,在MDN文档里面是这么说的:

Object.defineProperty()方法直接定义一个对象的属性,或者修改对象当中一个已经存在的属性,并返回这个对象。

让我们写个例子来测试一下它。

首先,建立一个钢铁侠对象并赋予他一些属性:

  1. let ironman = { 
  2.   name'Tony Stark'
  3.   sex: 'male'
  4.   age: '35' 
  5.  

现在我们使用Object.defineProperty()方法来对他的一些属性进行修改,并且在控制台把所修改的内容输出:

  1. Object.defineProperty(ironman, 'age', { 
  2.   set (val) { 
  3.     console.log(`Set age to ${val}`) 
  4.     return val 
  5.   } 
  6. }) 
  7.  
  8. ironman.age = '48' 
  9. // --> Set age to 48  

看起来挺完美的。如果把console.log('Set age to ${val}')改为element.innerHTML = val,是不是就意味着数据绑定已经完成了呢?

让我们再修改一下钢铁侠的属性:

  1. let ironman = { 
  2.   name'Tony Stark'
  3.   sex: 'male'
  4.   age: '35'
  5.   hobbies: ['girl''money''game'
  6.  

嗯……他就是一个花花公子。现在我想把一些“爱好”添加到他身上,并且在控制台看到对应的输出:

  1. Object.defineProperty(ironman.hobbies, 'push', { 
  2.   value () { 
  3.     console.log(`Push ${arguments[0]} to ${this}`) 
  4.     this[this.length] = arguments[0] 
  5.   } 
  6. }) 
  7.  
  8. ironman.hobbies.push('wine'
  9. console.log(ironman.hobbies) 
  10.  
  11. // --> Push wine to girl,money,game 
  12. // --> [ 'girl', 'money', 'game', 'wine' ]  

在此之前,我是使用get()方法去追踪对象的属性变化,但是对于一个数组,我们不能使用这个方法,而是使用value()方法来代替。虽然这招也灵,但是并非最好的办法。有没有更好的方法可以简化这些追踪对象或数组属性变化的方法呢?

在ECMA2015,Proxy是一个不错的选择

什么是Proxy?在MDN文档中是这么说的(误):

Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Proxy是ECMA2015的一个新特性,它非常强大,但我并不会讨论太多关于它的东西,除了我们现在需要的一个。现在让我们一起来新建一个Proxy实例:

  1. let ironmanProxy = new Proxy(ironman, { 
  2.   set (target, property, value) { 
  3.     target[property] = value 
  4.     console.log('change....'
  5.     return true 
  6.   } 
  7. }) 
  8.  
  9. ironmanProxy.age = '48' 
  10. console.log(ironman.age) 
  11.  
  12. // --> change.... 
  13. // --> 48  

符合预期。那么对于数组呢?

  1. let ironmanProxy = new Proxy(ironman.hobbies, { 
  2.   set (target, property, value) { 
  3.     target[property] = value 
  4.     console.log('change....'
  5.     return true 
  6.   } 
  7. }) 
  8.  
  9. ironmanProxy.push('wine'
  10. console.log(ironman.hobbies) 
  11.  
  12. // --> change... 
  13. // --> change... 
  14. // --> [ 'girl', 'money', 'game', 'wine' ]  

仍然符合预期!但是为什么输出了两次change...呢?因为每当我触发push()方法的时候,这个数组的length属性和body内容都被修改了,所以会引起两次变化。

实时数据绑定

解决了最核心的问题,可以考虑其他的问题了。

想象一下,我们有一个模板和数据对象:

  1. <!-- html template --> 
  2. <p>Hello, my name is {{name}}, I enjoy eatting {{hobbies.food}}</p> 
  3.  
  4. <!-- javascript --> 
  5. let ironman = { 
  6.   name'Tony Stark'
  7.   sex: 'male'
  8.   age: '35'
  9.   hobbies: { 
  10.     food: 'banana'
  11.     drink: 'wine' 
  12.   } 
  13.  

通过前面的代码,我们知道如果想要追踪一个对象的属性变化,我们应该把这个属性作为第一个参数传入Proxy实例。让我们一起来创建一个返回新的Proxy实例的函数吧! 

  1. function $setData (dataObj, fn) { 
  2.     let self = this 
  3.     let once = false 
  4.     let $d = new Proxy(dataObj, { 
  5.       set (target, property, value) { 
  6.         if (!once) { 
  7.           target[property] = value 
  8.           once = true 
  9.           /* Do something here */ 
  10.         } 
  11.         return true 
  12.       } 
  13.     }) 
  14.     fn($d) 
  15.   }  

它可以通过以下的方式被使用:

  1. $setData(dataObj, ($d) => { 
  2.   /*  
  3.    * dataObj.someProps = something 
  4.    */ 
  5. }) 
  6.  
  7. // 或者 
  8.  
  9. $setData(dataObj.arrayProps, ($d) => { 
  10.   /*  
  11.    * dataObj.push(something) 
  12.    */ 
  13. })  

除此之外,我们应该实现模板对数据对象的映射,这样才能用Tony Stark来替换{{name}}。

  1. function replaceFun(str, data) { 
  2.     let self = this 
  3.     return str.replace(/{{([^{}]*)}}/g, (a, b) => { 
  4.       return data[b] 
  5.     }) 
  6.   } 
  7.  
  8. replaceFun('My name is {{name}}', { name'xxx' }) 
  9. // --> My name is xxx  

这个函数对于如{ name: 'xx', age: 18 }的单层属性对象运行良好,但是对于如{ hobbies: { food: 'apple', drink: 'milk' } }这样的多层属性对象却无能为力。举个例子,如果模板关键字是{{hobbies.food}},那么replaceFun()函数就应该返回data['hobbies']['food']。

为了解决这个问题,再来一个函数:

  1. function getObjProp (obj, propsName) { 
  2.     let propsArr = propsName.split('.'
  3.     function rec(o, pName) { 
  4.       if (!o[pName] instanceof Array && o[pName] instanceof Object) { 
  5.         return rec(o[pName], propsArr.shift()) 
  6.       } 
  7.       return o[pName] 
  8.     } 
  9.     return rec(obj, propsArr.shift()) 
  10.   } 
  11.  
  12. getObjProp({ data: { hobbies: { food: 'apple', drink: 'milk' } } }, 'hobbies.food'
  13. // --> return  { food: 'apple', drink: 'milk' }  

最终的replaceFun()函数应该是下面这样子的: 

  1. function replaceFun(str, data) { 
  2.     let self = this 
  3.     return str.replace(/{{([^{}]*)}}/g, (a, b) => { 
  4.       let r = self._getObjProp(data, b); 
  5.       console.log(a, b, r) 
  6.       if (typeof r === 'string' || typeof r === 'number') { 
  7.         return r 
  8.       } else { 
  9.         return self._getObjProp(r, b.split('.')[1]) 
  10.       } 
  11.     }) 
  12.   }  

一个数据绑定的实例,叫做“Mog”

不为什么,就叫做“Mog”。

  1. class Mog { 
  2.   constructor (options) { 
  3.     this.$data = options.data 
  4.     this.$el = options.el 
  5.     this.$tpl = options.template 
  6.     this._render(this.$tpl, this.$data) 
  7.   } 
  8.  
  9.   $setData (dataObj, fn) { 
  10.     let self = this 
  11.     let once = false 
  12.     let $d = new Proxy(dataObj, { 
  13.       set (target, property, value) { 
  14.         if (!once) { 
  15.           target[property] = value 
  16.           once = true 
  17.           self._render(self.$tpl, self.$data) 
  18.         } 
  19.         return true 
  20.       } 
  21.     }) 
  22.     fn($d) 
  23.   } 
  24.  
  25.   _render (tplString, data) { 
  26.     document.querySelector(this.$el).innerHTML = this._replaceFun(tplString, data) 
  27.   } 
  28.  
  29.   _replaceFun(str, data) { 
  30.     let self = this 
  31.     return str.replace(/{{([^{}]*)}}/g, (a, b) => { 
  32.       let r = self._getObjProp(data, b); 
  33.       console.log(a, b, r) 
  34.       if (typeof r === 'string' || typeof r === 'number') { 
  35.         return r 
  36.       } else { 
  37.         return self._getObjProp(r, b.split('.')[1]) 
  38.       } 
  39.     }) 
  40.   } 
  41.  
  42.   _getObjProp (obj, propsName) { 
  43.     let propsArr = propsName.split('.'
  44.     function rec(o, pName) { 
  45.       if (!o[pName] instanceof Array && o[pName] instanceof Object) { 
  46.         return rec(o[pName], propsArr.shift()) 
  47.       } 
  48.       return o[pName] 
  49.     } 
  50.     return rec(obj, propsArr.shift()) 
  51.   } 
  52.  
  53.  

使用: 

  1. <!-- html --> 
  2.  
  3.     <div id="app"
  4.       <p> 
  5.         Hello everyone, my name is <span>{{name}}</span>, I am a mini <span>{{lang}}</span> framework for just <span>{{work}}</span>. I can bind data from <span>{{supports.0}}</span>, <span>{{supports.1}}</span> and <span>{{supports.2}}</span>. What's more, I was created by <span>{{info.author}}</span>, and was written in <span>{{info.jsVersion}}</span>. My motto is "<span>{{motto}}</span>"
  6.       </p> 
  7.     </div> 
  8.     <div id="input-wrapper"
  9.       Motto: <input type="text" id="set-motto" autofocus>     </div>   
  1. <!-- javascript --> 
  2.  
  3. let template = document.querySelector('#app').innerHTML 
  4.  
  5. let mog = new Mog({ 
  6.   template: template, 
  7.   el: '#app'
  8.   data: { 
  9.     name'mog'
  10.     lang: 'javascript'
  11.     work'data binding'
  12.     supports: ['String''Array''Object'], 
  13.     info: { 
  14.       author: 'Jrain'
  15.       jsVersion: 'Ecma2015' 
  16.     }, 
  17.     motto: 'Every dog has his day' 
  18.   } 
  19. }) 
  20.  
  21. document.querySelector('#set-motto').oninput = (e) => { 
  22.   mog.$setData(mog.$data, ($d) => { 
  23.     $d.motto = e.target.value 
  24.   }) 
  25.  

你可以在这里进行在线体验。

后记

Mog仅仅是一个用于学习数据绑定的实验性质的项目,代码仍然不够优雅,功能也不够丰富。但是这个小玩具让我学习了很多。如果你对它有兴趣,欢迎到这里把项目fork走,并且加入一些你的想法。

感谢阅读!

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2023-11-23 10:21:11

ECMAScriptJavaScript

2020-07-01 07:58:20

ES6JavaScript开发

2017-08-31 14:25:34

前端JavascriptES6

2016-01-21 11:05:52

ES6JavaScript新特性

2021-12-13 11:54:13

SetEs6接口

2023-03-01 15:39:50

JavaScrip对象属性ES6

2023-05-28 23:49:38

JavaScrip开发

2022-06-01 09:06:58

ES6数组函数

2021-08-16 07:05:58

ES6Promise开发语言

2022-08-05 13:14:25

ES2022JavaScript代码

2020-02-06 09:00:00

ESES2020前端

2012-12-20 11:16:16

IBMdW

2020-11-16 08:10:04

ES6迭代器JavaScript

2022-09-21 12:46:39

开发JavaScrip代码

2022-01-26 07:18:57

ES6WeakSetMap

2015-09-16 10:17:42

Node.jsES 6特性

2021-09-01 10:37:25

鸿蒙HarmonyOS应用

2017-06-03 15:43:54

数据项目框架

2017-06-19 08:30:35

大数据数据可视化报表

2023-02-23 16:49:11

ES6技巧
点赞
收藏

51CTO技术栈公众号