项目地点:https://github.com/jrainlau/mog
在线体验:http://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
?在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走,而且到场一些你的主意。
谢谢浏览!