IT科技

使用ES6的新特性Proxy来实现一个数据绑定实例

时间:2010-12-5 17:23:32  作者:IT科技类资讯   来源:系统运维  查看:  评论:0
内容摘要:作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过jQuery之类的工具手动完成这些功能,但是当数据量非常大的时候,这些手动的工作让我非常痛苦。直到使用了VueJS,这些痛苦才得以

作为一个前端开发者,使用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走,并且加入一些你的想法。

感谢阅读!

copyright © 2025 powered by 益强资讯全景  滇ICP备2023006006号-31sitemap