博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用ES6的新特性Proxy来实现一个数据绑定实例
阅读量:4086 次
发布时间:2019-05-25

本文共 6525 字,大约阅读时间需要 21 分钟。

项目地址:
在线体验:


https://segmentfault.com/a/1190000007443611作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过jQuery之类的工具手动完成这些功能,但是当数据量非常大的时候,这些手动的工作让我非常痛苦。直到使用了VueJS,这些痛苦才得以终结。

VueJS的其中一个卖点,就是“数据绑定”。使用者无需关心数据是怎么绑定到dom上面的,只需要关注数据就好,因为VueJS已经自动帮我们完成了这些工作。

这真的非常神奇,我不可救药地爱上了VueJS,并且把它用到我自己的项目当中。随着使用的深入,我更加想知道它深入的原理是什么。

VueJS是如何进行数据绑定的?

通过阅读官方文档,我看到了下面这段话:

把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。

关键词是Object.definProperty,在里面是这么说的:

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?在中是这么说的(误):

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内容都被修改了,所以会引起两次变化。

实时数据绑定

解决了最核心的问题,可以考虑其他的问题了。

想象一下,我们有一个模板和数据对象:

Hello, my name is {

{name}}, I enjoy eatting {
{hobbies.food}}

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()) }}

使用:

Hello everyone, my name is {

{name}}, I am a mini {
{lang}}
framework for just {
{work}}
. I can bind data from {
{supports.0}}
, {
{supports.1}}
and {
{supports.2}}
. What's more, I was created by {
{info.author}}
, and was written in {
{info.jsVersion}}
. My motto is "{
{motto}}
".

Motto:
let template = document.querySelector('#app').innerHTMLlet 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走,并且加入一些你的想法。

转载地址:http://kkeni.baihongyu.com/

你可能感兴趣的文章
GPS定位
查看>>
地图、显示用户位置、大头针
查看>>
自定义大头针
查看>>
UIButton添加block点击事件
查看>>
利用runtime给类别添加属性
查看>>
本地推送
查看>>
FMDB的使用
查看>>
UIImage存为本地文件与UIImage转换为NSData
查看>>
[转]打印质数的各种算法
查看>>
[转]javascript with延伸的作用域是只读的吗?
查看>>
php的autoload与global
查看>>
IE不支持option的display:none属性
查看>>
[分享]mysql内置用于字符串型ip地址和整数型ip地址转换函数
查看>>
TableDnd(JQuery表格拖拽控件)应用进阶
查看>>
[转]开源中最好的Web开发的资源
查看>>
java接口不能实例化原因浅谈
查看>>
Https加密及攻防
查看>>
Java生成随机不重复推广码邀请码
查看>>
Java8 Lambda表达式介绍
查看>>
【JAVA数据结构】双向链表
查看>>