ES6使用新特性Proxy实现的数据绑定功能实例


Posted in Javascript onMay 11, 2020

本文实例讲述了ES6使用新特性Proxy实现的数据绑定功能。分享给大家供大家参考,具体如下:

项目地址:https://github.com/jrainlau/mog
在线体验:https://codepen.io/jrainlau/pen/YpyBBY


作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过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
 })
}

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

后记

Mog仅仅是一个用于学习数据绑定的实验性质的项目,代码仍然不够优雅,功能也不够丰富。但是这个小玩具让我学习了很多。如果你对它有兴趣,欢迎到这里把项目fork走,并且加入一些你的想法。

感谢阅读!

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行效果。

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
TimergliderJS 一个基于jQuery的时间轴插件
Dec 07 Javascript
jquery拖动插件(jquery.drag)使用介绍
Jun 18 Javascript
JavaScript italics方法入门实例(把字符串显示为斜体)
Oct 17 Javascript
Eclipse配置Javascript开发环境图文教程
Jan 29 Javascript
jquery动态添加文本并获取值的方法
Oct 12 Javascript
微信小程序 数组(增,删,改,查)等操作实例详解
Jan 05 Javascript
快速理解 JavaScript 中的 LHS 和 RHS 查询的用法
Aug 24 Javascript
VueRouter导航守卫用法详解
Dec 25 Javascript
详解如何在Vue里建立长按指令
Aug 20 Javascript
JavaScript修改注册表实例代码
Jan 05 Javascript
微信小程序实现搜索功能
Mar 10 Javascript
JavaScript直接调用函数与call调用的区别实例分析
May 22 Javascript
JavaScript异步操作的几种常见处理方法实例总结
May 11 #Javascript
Nuxt默认模板、默认布局和自定义错误页面的实现
May 11 #Javascript
Vue.js获取手机系统型号、版本、浏览器类型的示例代码
May 10 #Javascript
vue总线机制(bus)知识点详解
May 10 #Javascript
vue路由跳转传递参数的方式总结
May 10 #Javascript
javascript单张多张图无缝滚动实例代码
May 10 #Javascript
JavaScript面试中常考的字符串操作方法大全(包含ES6)
May 10 #Javascript
You might like
跟我学Laravel之视图 &amp; Response
2014/10/15 PHP
php简单随机字符串生成方法示例
2017/04/19 PHP
作为PHP程序员你要知道的另外一种日志
2018/07/30 PHP
jQuery 注意事项 与原因分析
2009/04/24 Javascript
JQuery1.4+ Ajax IE8 内存泄漏问题
2010/10/15 Javascript
JS事件Event元素(兼容IE,Firefox,Chorme)
2012/11/01 Javascript
jquery.qrcode在线生成二维码使用示例
2013/08/21 Javascript
一些老手都不一定知道的JavaScript技巧
2014/05/06 Javascript
给应用部分的js代码设定一个统一的入口
2014/06/15 Javascript
Nodejs如何搭建Web服务器
2016/03/28 NodeJs
浅析jquery unbind()方法移除元素绑定的事件
2016/05/24 Javascript
基于bootstrap实现广告轮播带图片和文字效果
2016/07/22 Javascript
AngularJS中$apply方法和$watch方法用法总结
2016/12/13 Javascript
Vue2.0如何发布项目实战
2017/07/27 Javascript
最后说说Vue2 SSR 的 Cookies 问题
2018/05/25 Javascript
VUE v-model表单数据双向绑定完整示例
2019/01/21 Javascript
js常用正则表达式集锦
2019/05/17 Javascript
js面试题之异步问题的深入理解
2020/09/20 Javascript
jQuery实现简单评论区功能
2020/10/26 jQuery
design vue 表格开启列排序的操作
2020/10/28 Javascript
[46:47]2014 DOTA2国际邀请赛中国区预选赛 DT VS HGT
2014/05/22 DOTA
[01:03:22]LGD vs OG 2018国际邀请赛淘汰赛BO3 第一场 8.25
2018/08/29 DOTA
使用Protocol Buffers的C语言拓展提速Python程序的示例
2015/04/16 Python
Python批量转换文件编码格式
2015/05/17 Python
Python使用Windows API创建窗口示例【基于win32gui模块】
2018/05/09 Python
python判断输入日期为第几天的实例
2018/11/13 Python
Python动态赋值的陷阱知识点总结
2019/03/17 Python
Python如何将函数值赋给变量
2020/04/28 Python
python 发送邮件的四种方法汇总
2020/12/02 Python
英国美术用品购物网站:Cass Art
2019/10/08 全球购物
银行自荐信范文
2015/03/25 职场文书
阿凡达观后感
2015/06/10 职场文书
全国劳模先进事迹材料(2016精选版)
2016/02/25 职场文书
CSS3 制作的悬停缩放特效
2021/04/13 HTML / CSS
原生JS封装vue Tab切换效果
2021/04/28 Vue.js
利用Python实现模拟登录知乎
2022/05/25 Python