使用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来实现一个数据绑定实例》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

图解HTTP

图解HTTP

【日】上野宣 / 于均良 / 人民邮电出版社 / 2014-4-15 / 49.00元

本书对互联网基盘——HTTP协议进行了全面系统的介绍。作者由HTTP协议的发展历史娓娓道来,严谨细致地剖析了HTTP协议的结构,列举诸多常见通信场景及实战案例,最后延伸到Web安全、最新技术动向等方面。本书的特色为在讲解的同时,辅以大量生动形象的通信图例,更好地帮助读者深刻理解HTTP通信过程中客户端与服务器之间的交互情况。读者可通过本书快速了解并掌握HTTP协议的基础,前端工程师分析抓包数据,后......一起来看看 《图解HTTP》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具