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

栏目: JavaScript · 发布时间: 5年前

内容简介:写于 2016.10.11项目地址:作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过

写于 2016.10.11

项目地址: github.com/jrainlau/mo… 在线体验: 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
  })
}
复制代码

你可以在这里进行在线体验。


以上所述就是小编给大家介绍的《使用ES6的新特性Proxy来实现一个数据绑定实例》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

XML Hacks

XML Hacks

Michael Fitzgerald / O'Reilly Media, Inc. / 2004-07-27 / USD 24.95

Developers and system administrators alike are uncovering the true power of XML, the Extensible Markup Language that enables data to be sent over the Internet from one computer platform to another or ......一起来看看 《XML Hacks》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具