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 相关文章推荐
动态添加js事件实现代码
Mar 12 Javascript
详细分析Javascript中创建对象的四种方式
Aug 17 Javascript
JS正则表达式之非捕获分组用法实例分析
Dec 28 Javascript
从零学习node.js之文件操作(三)
Feb 21 Javascript
Vue.js结合bootstrap实现分页控件
Mar 10 Javascript
浅谈struts1 &amp; jquery form 文件异步上传
May 25 jQuery
AngularJS实现自定义指令与控制器数据交互的方法示例
Jun 19 Javascript
jQuery实现浏览器之间跳转并传递参数功能【支持中文字符】
Mar 28 jQuery
讲解vue-router之什么是编程式路由
May 28 Javascript
vue两个组件间值的传递或修改方式
Jul 04 Javascript
js+html5实现手机九宫格密码解锁功能
Jul 30 Javascript
Vue ECharts实现机舱座位选择展示功能
May 15 Vue.js
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
DISCUZ 分页代码
2007/01/02 PHP
允许phpmyadmin空密码登录的配置方法
2011/05/29 PHP
测试php连接mysql是否成功的代码分享
2014/01/24 PHP
php中的filesystem文件系统函数介绍及使用示例
2014/02/13 PHP
php如何连接sql server
2015/10/16 PHP
php遍历替换目录下文件指定内容的方法
2016/11/10 PHP
Yii框架分页实现方法详解
2017/05/20 PHP
PHP Ajax跨域问题解决方案代码实例
2020/08/01 PHP
动态调用css文件——jquery的应用
2007/02/20 Javascript
JS记录用户登录次数实现代码
2014/01/15 Javascript
js实现动态加载脚本的方法实例汇总
2015/11/02 Javascript
Angularjs中UI Router的使用方法
2016/05/14 Javascript
js实现的简练高效拖拽功能示例
2016/12/21 Javascript
工厂模式在JS中的实践
2017/01/18 Javascript
JavaScript实现替换字符串中最后一个字符的方法
2017/03/07 Javascript
基于nodejs 的多页面爬虫实例代码
2017/05/31 NodeJs
vue + webpack如何绕过QQ音乐接口对host的验证详解
2018/07/01 Javascript
vue3.0 CLI - 3.2 路由的初级使用教程
2018/09/20 Javascript
node.js命令行教程图文详解
2019/05/27 Javascript
在vue-cli中引入lodash.js并使用详解
2019/11/13 Javascript
在 Vue 中使用 JSX 及使用它的原因浅析
2020/02/10 Javascript
[37:23]DOTA2上海特级锦标赛主赛事日 - 3 胜者组第二轮#2Secret VS EG第二局
2016/03/04 DOTA
[01:11:27]2018DOTA2亚洲邀请赛小组赛 A组加赛 Newbee vs Optic
2018/04/03 DOTA
[02:12]打造更好的电竞完美世界:完美盛典回顾篇
2018/12/19 DOTA
详细解读Python中解析XML数据的方法
2015/10/15 Python
python GUI库图形界面开发之PyQt5滚动条控件QScrollBar详细使用方法与实例
2020/03/06 Python
python实现PDF中表格转化为Excel的方法
2020/06/16 Python
容易被忽略的Python内置类型
2020/09/03 Python
Python JSON常用编解码方法代码实例
2020/09/05 Python
使用phonegap进行本地存储的实现方法
2017/03/31 HTML / CSS
详解Canvas 跨域脱坑实践
2018/11/07 HTML / CSS
雪山饭庄的创业计划书范文
2014/01/18 职场文书
应聘教师自荐书
2014/06/16 职场文书
2015年女生节活动总结
2015/02/27 职场文书
初中地理教学反思
2016/02/19 职场文书
优秀乡村医生事迹材料(2016精选版)
2016/02/29 职场文书