作为一个前端开发者,使用y实数据实例曾踩了太多的特性“数据绑定”的坑。在早些时候,绑定都是使用y实数据实例通过jQuery之类的工具手动完成这些功能,但是特性当数据量非常大的时候,这些手动的绑定工作让我非常痛苦。直到使用了VueJS,使用y实数据实例这些痛苦才得以终结。特性
VueJS的绑定其中一个卖点,就是使用y实数据实例“数据绑定”。使用者无需关心数据是特性怎么绑定到dom上面的,只需要关注数据就好,绑定因为VueJS已经自动帮我们完成了这些工作。使用y实数据实例
这真的特性非常神奇,我不可救药地爱上了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>. Whats 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走,并且加入一些你的想法。
感谢阅读!