使用ES6的新特性Proxy来实现一个数据绑定实例
栏目: JavaScript · 发布时间: 5年前
内容简介:写于 2016.10.11项目地址:作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过
写于 2016.10.11
项目地址: github.com/jrainlau/mo… 在线体验: codepen.io/jrainlau/pe…
作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过 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
?在
中是这么说的(误):
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 }) } 复制代码
你可以在这里进行在线体验。
以上所述就是小编给大家介绍的《使用ES6的新特性Proxy来实现一个数据绑定实例》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Golang Echo数据绑定中time.Time类型绑定失败
- 如何在Symfony的表单中添加一个未绑定字段,否则绑定到一个实体?
- js双向绑定
- 延迟静态绑定——static
- 绑定自定义事件
- angular组件双向绑定
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
XML Hacks
Michael Fitzgerald / O'Reilly Media, Inc. / 2004-07-27 / USD 24.95
Developers and system administrators alike are uncovering the true power of XML, the Extensible Markup Language that enables data to be sent over the Internet from one computer platform to another or ......一起来看看 《XML Hacks》 这本书的介绍吧!