本文实例讲述了ES6使用新特性Proxy实现的数据绑定功能。分享给大家供大家参考,具体如下:
项目地址:https://github.com/jrainlau/mog
在线体验:https://codepen.io/jrainlau/pen/YpyBBY
作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过jQuery
之类的工具手动完成这些功能,但是当数据量非常大的时候,这些手动的工作让我非常痛苦。直到使用了VueJS
,这些痛苦才得以终结。
VueJS
的其中一个卖点,就是“数据绑定”。使用者无需关心数据是怎么绑定到dom上面的,只需要关注数据就好,因为VueJS
已经自动帮我们完成了这些工作。
这真的非常神奇,我不可救药地爱上了VueJS
,并且把它用到我自己的项目当中。随着使用的深入,我更加想知道它深入的原理是什么。
VueJS是如何进行数据绑定的?
通过阅读官方文档,我看到了下面这段话:
把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。
关键词是Object.definProperty
,在MDN文档里面是这么说的:
Object.defineProperty()
方法直接定义一个对象的属性,或者修改对象当中一个已经存在的属性,并返回这个对象。
让我们写个例子来测试一下它。
首先,建立一个钢铁侠对象并赋予他一些属性:
let ironman = { name: 'Tony Stark', sex: 'male', age: '35' }
现在我们使用Object.defineProperty()
方法来对他的一些属性进行修改,并且在控制台把所修改的内容输出:
Object.defineProperty(ironman, 'age', { set (val) { console.log(`Set age to ${val}`) return val } }) ironman.age = '48' // --> Set age to 48
看起来挺完美的。如果把console.log('Set age to ${val}')
改为element.innerHTML = val
,是不是就意味着数据绑定已经完成了呢?
让我们再修改一下钢铁侠的属性:
let ironman = { name: 'Tony Stark', sex: 'male', age: '35', hobbies: ['girl', 'money', 'game'] }
嗯……他就是一个花花公子。现在我想把一些“爱好”添加到他身上,并且在控制台看到对应的输出:
Object.defineProperty(ironman.hobbies, 'push', { value () { console.log(`Push ${arguments[0]} to ${this}`) this[this.length] = arguments[0] } }) ironman.hobbies.push('wine') console.log(ironman.hobbies) // --> Push wine to girl,money,game // --> [ 'girl', 'money', 'game', 'wine' ]
在此之前,我是使用get()
方法去追踪对象的属性变化,但是对于一个数组,我们不能使用这个方法,而是使用value()
方法来代替。虽然这招也灵,但是并非最好的办法。有没有更好的方法可以简化这些追踪对象或数组属性变化的方法呢?
在ECMA2015,Proxy是一个不错的选择
什么是Proxy
?在MDN文档中是这么说的(误):
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Proxy
是ECMA2015的一个新特性,它非常强大,但我并不会讨论太多关于它的东西,除了我们现在需要的一个。现在让我们一起来新建一个Proxy实例:
let ironmanProxy = new Proxy(ironman, { set (target, property, value) { target[property] = value console.log('change....') return true } }) ironmanProxy.age = '48' console.log(ironman.age) // --> change.... // --> 48
符合预期。那么对于数组呢?
let ironmanProxy = new Proxy(ironman.hobbies, { set (target, property, value) { target[property] = value console.log('change....') return true } }) ironmanProxy.push('wine') console.log(ironman.hobbies) // --> change... // --> change... // --> [ 'girl', 'money', 'game', 'wine' ]
仍然符合预期!但是为什么输出了两次change...
呢?因为每当我触发push()
方法的时候,这个数组的length
属性和body
内容都被修改了,所以会引起两次变化。
实时数据绑定
解决了最核心的问题,可以考虑其他的问题了。
想象一下,我们有一个模板和数据对象:
<!-- html template --> <p>Hello, my name is {{name}}, I enjoy eatting {{hobbies.food}}</p> <!-- javascript --> let ironman = { name: 'Tony Stark', sex: 'male', age: '35', hobbies: { food: 'banana', drink: 'wine' } }
通过前面的代码,我们知道如果想要追踪一个对象的属性变化,我们应该把这个属性作为第一个参数传入Proxy
实例。让我们一起来创建一个返回新的Proxy
实例的函数吧!
function $setData (dataObj, fn) { let self = this let once = false let $d = new Proxy(dataObj, { set (target, property, value) { if (!once) { target[property] = value once = true /* Do something here */ } return true } }) fn($d) }
它可以通过以下的方式被使用:
$setData(dataObj, ($d) => { /* * dataObj.someProps = something */ }) // 或者 $setData(dataObj.arrayProps, ($d) => { /* * dataObj.push(something) */ })
除此之外,我们应该实现模板对数据对象的映射,这样才能用Tony Stark
来替换{{name}}
。
function replaceFun(str, data) { let self = this return str.replace(/{{([^{}]*)}}/g, (a, b) => { return data[b] }) } replaceFun('My name is {{name}}', { name: 'xxx' }) // --> My name is xxx
这个函数对于如{ name: 'xx', age: 18 }
的单层属性对象运行良好,但是对于如{ hobbies: { food: 'apple', drink: 'milk' } }
这样的多层属性对象却无能为力。举个例子,如果模板关键字是{{hobbies.food}}
,那么replaceFun()
函数就应该返回data['hobbies']['food']
。
为了解决这个问题,再来一个函数:
function getObjProp (obj, propsName) { let propsArr = propsName.split('.') function rec(o, pName) { if (!o[pName] instanceof Array && o[pName] instanceof Object) { return rec(o[pName], propsArr.shift()) } return o[pName] } return rec(obj, propsArr.shift()) } getObjProp({ data: { hobbies: { food: 'apple', drink: 'milk' } } }, 'hobbies.food') // --> return { food: 'apple', drink: 'milk' }
最终的replaceFun()
函数应该是下面这样子的:
function replaceFun(str, data) { let self = this return str.replace(/{{([^{}]*)}}/g, (a, b) => { let r = self._getObjProp(data, b); console.log(a, b, r) if (typeof r === 'string' || typeof r === 'number') { return r } else { return self._getObjProp(r, b.split('.')[1]) } }) }
一个数据绑定的实例,叫做“Mog”
不为什么,就叫做“Mog”。
class Mog { constructor (options) { this.$data = options.data this.$el = options.el this.$tpl = options.template this._render(this.$tpl, this.$data) } $setData (dataObj, fn) { let self = this let once = false let $d = new Proxy(dataObj, { set (target, property, value) { if (!once) { target[property] = value once = true self._render(self.$tpl, self.$data) } return true } }) fn($d) } _render (tplString, data) { document.querySelector(this.$el).innerHTML = this._replaceFun(tplString, data) } _replaceFun(str, data) { let self = this return str.replace(/{{([^{}]*)}}/g, (a, b) => { let r = self._getObjProp(data, b); console.log(a, b, r) if (typeof r === 'string' || typeof r === 'number') { return r } else { return self._getObjProp(r, b.split('.')[1]) } }) } _getObjProp (obj, propsName) { let propsArr = propsName.split('.') function rec(o, pName) { if (!o[pName] instanceof Array && o[pName] instanceof Object) { return rec(o[pName], propsArr.shift()) } return o[pName] } return rec(obj, propsArr.shift()) } }
使用:
<!-- html --> <div id="app"> <p> 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>". </p> </div> <div id="input-wrapper"> Motto: <input type="text" id="set-motto" autofocus> </div>
<!-- javascript --> let template = document.querySelector('#app').innerHTML let mog = new Mog({ template: template, el: '#app', data: { name: 'mog', lang: 'javascript', work: 'data binding', supports: ['String', 'Array', 'Object'], info: { author: 'Jrain', jsVersion: 'Ecma2015' }, motto: 'Every dog has his day' } }) document.querySelector('#set-motto').oninput = (e) => { mog.$setData(mog.$data, ($d) => { $d.motto = e.target.value }) }
你可以在这里进行在线体验。
后记
Mog
仅仅是一个用于学习数据绑定的实验性质的项目,代码仍然不够优雅,功能也不够丰富。但是这个小玩具让我学习了很多。如果你对它有兴趣,欢迎到这里把项目fork走,并且加入一些你的想法。
感谢阅读!
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新日志
- 黄乙玲1999-无字的情批[台湾首版][WAV+CUE]
- 何超仪.1996-何家淑女(EP)【华星】【WAV+CUE】
- 娃娃.1995-随风【滚石】【WAV+CUE】
- 林俊吉.2007-林俊吉【美华影音】【WAV+CUE】
- 梁静茹《勇气》滚石首版[WAV+CUE][1.1G]
- 刘若英《听说》[WAV+CUE][1.1G]
- 林忆莲《不如重新开始》 24K金 MQA 2022 再版[1.1G]
- 曾庆瑜1991-女人主意[派森][WAV+CUE]
- 江智民2024-《写给海洋HQ》头版限量编号[WAV+CUE]
- 谭咏麟2024《暴风女神Lorelei》头版限量编号MQA-UHQCD[WAV+CUE]
- 群星.2003-滚石黄金十年系列33CD【滚石】【WAV+CUE】
- 萧亚轩.2008-3面夏娃【维京】【WAV+CUE】
- 唐娜.1989-那年情人节好冷【喜玛拉雅】【WAV+CUE】
- 赵传《赵传奇》 滚石SACD系列 SACD限量版[ISO][1.1G]
- 黄龄《痒》天韵文化[WAV+CUE][1G]