作为一名前端开发者,踩过太多“数据绑定”的坑。在早期,这些功能都是通过jQuery等工具手动完成的,但是当数据量非常大的时候,这些手动的工作让我非常痛苦。直到使用了VueJS,这些痛苦才算结束。VueJS的卖点之一是“数据绑定”。用户不需要关心数据是如何绑定到dom上的,他们只需要关注数据,因为VueJS已经自动帮我们完成了这些工作。真的很神奇,我爱上了VueJS并在我自己的项目中使用它。随着使用的深入,想进一步了解它的深层原理。VueJS是如何进行数据绑定的?通过阅读官方文档,看到了这样一段话:将一个普通的Javascript对象作为其数据选项传递给Vue实例,Vue会遍历其属性,并使用Object.defineProperty将它们转为getters/setters。关键词是Object.definProperty,在MDN文档中是这么说的:Object.defineProperty()方法直接定义一个对象的属性,或者修改对象中已有的一个属性,并返回该对象。让我们写一个例子来测试一下。首先,创建一个钢铁侠对象,并给他一些属性:letironman={name:'TonyStark',sex:'male',age:'35'}现在我们使用Object.defineProperty()方法来定义他的一些属性修改,并在控制台输出修改后的内容:Object.defineProperty(ironman,'age',{set(val){console.log(`Setageto${val}`)returnval}})ironman.age='48'//-->Setageto48看起来很完美。如果把console.log('Setageto${val}')改成element.innerHTML=val,是不是说明数据绑定完成了?我们来修改钢铁侠的属性:letironman={name:'TonyStark',sex:'male',age:'35',hobbies:['girl','money','game']}Well...he是个花花公子。现在我想给他添加一些“爱好”,在控制台看到相应的输出: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)//-->Pushwinetogirl,money,game//-->['girl','money','game','wine']在此之前,我使用get()方法来跟踪对象的属性变化,但是对于一个数组,我们不能使用This方法,而是改用value()方法。虽然这个技巧有效,但这并不是最好的方法。有没有更好的方法来简化这些跟踪对象或数组属性更改的方法?在ECMA2015中,Proxy是一个不错的选择。什么是代理?MDN文档中是这么说的(错):Proxy可以理解为,在目标对象之前设置了一层“拦截”。外部对对象的访问必须先经过这一层拦截。因此,提供了一种机制来过滤和重写外部访问。Proxy是ECMA2015的一个新特性,它非常强大,但我不会过多讨论它,除了我们现在需要的。现在让我们创建一个新的Proxy实例:letironmanProxy=newProxy(ironman,{set(target,property,value){target[property]=valueconsole.log('change....')returntrue}})ironmanProxy.age='48'console.log(ironman.age)//-->更改....//-->48符合预期。数组呢?letironmanProxy=newProxy(ironman.hobbies,{set(target,property,value){target[property]=valueconsole.log('change....')returntrue}})ironmanProxy.push('wine')console.log(ironman.hobbies)//-->change...//-->change...//-->['girl','money','game','wine']还是符合预期!但是为什么有两个变化......?因为每当我触发push()方法时,都会修改这个数组的length属性和body内容,所以会引起两个变化。实时数据绑定解决了核心问题,其他问题可以考虑。假设我们有一个模板和数据对象:
Hello,mynameis{{name}},Ienjoyeatting{{hobbies.food}}
letironman={name:'TonyStark',sex:'male',age:'35',hobbies:{food:'banana',drink:'wine'}}通过前面的代码我们知道如果我们要追踪一个对象的属性发生变化,我们应该将这个属性作为第一个参数传递给Proxy实例。让我们创建一个返回新代理实例的函数!function$setData(dataObj,fn){letself=thisletonce=falselet$d=newProxy(dataObj,{set(target,property,value){if(!once){target[property]=valueonce=true/*Dosomethinghere*/}returntrue}})fn($d)}可以这样使用:$setData(dataObj,($d)=>{/**dataObj.someProps=something*/})//或$setData(dataObj.arrayProps,($d)=>{/**dataObj.push(something)*/})此外,我们应该实现data对象的映射模板,以便{{name}}可以替换为Tony斯塔克。functionreplaceFun(str,data){letself=thisreturnstr.replace(/{{([^{}]*)}}/g,(a,b)=>{returndata[b]})}replaceFun('Mynameis{{name}}',{name:'xxx'})//-->Mynameisxxx这个函数对单层属性对象如{name:'xx',age:18}效果很好,但是对{hobbies:{food:'apple',drink:'milk'}}这样的多层属性对象是无能为力的。例如,如果模板关键字是{{hobbies.food}},则replaceFun()函数应返回data['hobbies']['food']。为了解决这个问题,这里有另一个函数:o[pName],propsArr.shift())}return[pName]}returnrec(obj,propsArr.shift())}getObjProp({data:{hobbies:{food:'apple',drink:'milk'}}},'hobbies.food')//-->return{food:'apple',drink:'milk'}最终的replaceFun()函数应该如下所示:functionreplaceFun(str,data){letself=thisreturnstr.替换(/{{([^{}]*)}}/g,(a,b)=>{letr=self._getObjProp(data,b);console.log(a,b,r)if(typeofr==='string'||typeofr==='number'){returnr}else{returnsself._getObjProp(r,b.split('.')[1])}})}一个数据绑定的实例,就是白叫“莫格”,就叫“莫格”。classMog{构造函数(选项){this.$data=options.datathis.$el=options.elthis.$tpl=options.templatethis._render(this.$tpl,this.$data)}$setData(dataObj,fn){letself=thisletonce=falselet$d=newProxy(dataObj,{set(target,property,value){if(!once){target[property]=valueonce=trueself._render(self.$tpl,self.$data)}returntrue}})fn($d)}_render(tplString,data){document.querySelector(this.$el).innerHTML=this._replaceFun(tplString,data)}_replaceFun(str,data){letself=thisreturnstr.替换(/{{([^{}]*)}}/g,(a,b)=>{letr=self._getObjProp(data,b);console.log(a,b,r)if(typeofr==='string'||typeofr==='number'){returnr}else{returnsself._getObjProp(r,b.split('.')[1])}})}_getObjProp(obj,propsName){letpropsArr=propsName.split('.')functionrec(o,pName){if(!o[pName]instanceofArray&&o[pName]instanceofObject){returnrec(o[pName],propsArr.shift())}returno[pName]}returnrec(obj,propsArr.shift())}}使用:Helloeveryone,mynameis{{name}},Iamamini{{lang}}frameworkforjust{{work}}.Icanbinddatafrom{{supports.0}}、{{supports.1}}和{{supports.2}}。更重要的是,我是由{{info.author}},andwaswrittenin{{info.jsVersion}}.Mymottois"{{motto}}".