作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过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,是不是就意味着数据绑定已经完成了呢?
让我们再修改一下钢铁侠的属性:
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, {
ironmanproxy.push('wine')
// --> change...
仍然符合预期!但是为什么输出了两次change...呢?因为每当我触发push()方法的时候,这个数组的length属性和body内容都被修改了,所以会引起两次变化。
实时数据绑定
解决了最核心的问题,可以考虑其他的问题了。
想象一下,我们有一个模板和数据对象:
<!-- html template -->
<p>hello, my name is {{name}}, i enjoy eatting {{hobbies.food}}</p>
<!-- javascript -->
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) {
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()函数应该是下面这样子的:
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) {
self._render(self.$tpl, self.$data)
_render (tplstring, data) {
document.queryselector(this.$el).innerhtml = this._replacefun(tplstring, data)
_replacefun(str, data) {
_getobjprop (obj, propsname) {
使用:
<!-- 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>
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走,并且加入一些你的想法。
感谢阅读!
作者:jrainlau
来源:51cto