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脚本复制网页上的一个表格的不错实现方法
Dec 29 Javascript
javascript 页面只自动刷新一次
Jul 10 Javascript
JQuery替换DOM节点的方法
Jun 11 Javascript
AnjularJS中$scope和$rootScope的区别小结
Sep 18 Javascript
javascript获取以及设置光标位置
Feb 16 Javascript
js构建二叉树进行数值数组的去重与优化详解
Mar 26 Javascript
微信小程序js文件改变参数并在视图上及时更新【推荐】
Jun 11 Javascript
jQuery实现轮播图及其原理详解
Apr 12 jQuery
JS数组求和的常用方法实例小结
Jan 07 Javascript
vue项目创建并引入饿了么elementUI组件的步骤
Apr 11 Javascript
详解一次Vue低版本安卓白屏问题的解决过程
May 30 Javascript
如何进行微信公众号开发的本地调试的方法
Jun 16 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
一段php加密解密的代码
2006/10/09 PHP
php array_walk() 数组函数
2011/07/12 PHP
PHP之生成GIF动画的实现方法
2013/06/07 PHP
解析PHP中的正则表达式以及模式匹配
2013/06/19 PHP
php小技巧之过滤ascii控制字符
2014/05/14 PHP
Linux平台PHP5.4设置FPM线程数量的方法
2016/11/09 PHP
PHP PDOStatement::fetchObject讲解
2019/02/01 PHP
JavaScript 创建对象和构造类实现代码
2009/07/30 Javascript
在一个浏览器里呈现所有浏览器测试结果的前端测试工具的思路
2010/03/02 Javascript
js DOM 元素ID就是全局变量
2012/09/20 Javascript
用C/C++来实现 Node.js 的模块(二)
2014/09/24 Javascript
jQuery遍历对象、数组、集合实例
2014/11/08 Javascript
vue checkbox 全选 数据的绑定及获取和计算方法
2018/02/09 Javascript
JavaScript 有用的代码片段和 trick
2018/02/22 Javascript
在react-router4中进行代码拆分的方法(基于webpack)
2018/03/08 Javascript
vue 2.1.3 实时显示当前时间,每秒更新的方法
2018/09/16 Javascript
基于nodejs的雪碧图制作工具的示例代码
2018/11/05 NodeJs
JS如何实现动态添加的元素绑定事件
2019/11/12 Javascript
Python中多线程thread与threading的实现方法
2014/08/18 Python
Python操作串口的方法
2015/06/17 Python
通过python+selenium3实现浏览器刷简书文章阅读量
2017/12/26 Python
详解Django之admin组件的使用和源码剖析
2018/05/04 Python
python利用Tesseract识别验证码的方法示例
2019/01/21 Python
Ubuntu下Anaconda和Pycharm配置方法详解
2019/06/14 Python
简单的Python调度器Schedule详解
2019/08/30 Python
python使用socket 先读取长度,在读取报文内容示例
2019/09/26 Python
澳大利亚Mocha官方网站:包、钱包、珠宝和配饰
2019/07/18 全球购物
怎样写留学自荐信
2013/11/11 职场文书
2014年公司庆元旦活动方案
2014/03/05 职场文书
美容院经理岗位职责
2014/04/03 职场文书
学院党委班子四风问题自查报告及整改措施
2014/10/25 职场文书
王兆力在市委党的群众路线教育实践活动总结大会上的讲话稿
2014/10/25 职场文书
2014年纪检部工作总结
2014/11/12 职场文书
毕业生个人总结
2015/02/28 职场文书
标枪加油稿
2015/07/22 职场文书
Win11 vmware不兼容怎么办?Win11与VMware虚拟机不兼容的解决方法
2023/01/09 数码科技