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

本文实例讲述了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.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

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

(0)

相关推荐

  • 让微信小程序支持ES6中Promise特性的方法详解

    遇到的问题 微信开发者工具更新版本后, 移除了开发者工具对 ES6 中 Promise 特性原生的支持, 理由是因为实体机器是不支持 Promise 的, 所以我们需要引入第三方的 Promise 库 微信更新日志 解决方案 下载第三方库 在这里我引入的是 Bluebird 库, 可以到Bluebird官网 下载需要的文件,也可以通过本地下载 Bluebrid 提供了两种已经构建好的完整的 Promise 库文件, 未经压缩的 bluebird.js 和已压缩的 bluebird.min.js

  • ES6中let 和 const 的新特性

    在javascript中,我们都知道使用var来声明变量.javascript是函数级作用域,函数内可以访问函数外的变量,函数外不能访问函数内的变量.本文给大家介绍ES6中let 和 const 的特性,具体内容如下所示: let的特性: 1.不存在变量提升现象:即声明前使用,报ReferenceError.适用于for循环计数器. 2.暂时性死区:即在块级作用域内使用let声明变量,该变量只能在该作用域内使用. 3.不允许重复声明:let声明过的变量不能再次声明,否则报错. const的特性:

  • ES6新特性八:async函数用法实例详解

    本文实例讲述了ES6新特性之async函数用法.分享给大家供大家参考,具体如下: 1. async 函数是什么? node.js 版本7及之后版本才支持该函数. 可以简单的理解为他是Generator 函数的语法糖,即Generator 函数调用next() 返回的结果. ① Generator 函数需要next() 或执行器进行执行,而async 函数只需和普通函数一样执行. ② async和await,比起星号和yield,语义更清楚了.async表示函数里有异步操作,await表示紧跟在后

  • es6新特性之 class 基本用法解析

    javaScript 语言中,生成实例对象的传统方法是通过构造函数,与传统的面向对象语言(比如 C++ 和 Java)差异很大,ES6 提供了更接近传统语言的写法,引入了 class(类)这个概念,作为对象的模板.通过class关键字,可以定义类. es6 class 与es5的面向对象的区别: 1. 写法不同,使用关键字class 2.当new一个实例,默认有一个constructor方法,且默认返回实例对象(this),也可以返回另一对象 3.类的所有方法都在prototype属性上,但是不

  • JS基于ES6新特性async await进行异步处理操作示例

    本文实例讲述了JS基于ES6新特性async await进行异步处理操作.分享给大家供大家参考,具体如下: 我们往往在项目中会遇到这样的业务需求,就是首先先进行一个ajax请求,然后再进行下一个ajax请求,而下一个请求需要使用上一个请求得到的数据,请求少了还好说,如果多了,就要一层一层的嵌套,就好像有点callback的写法了,那是相当恶心的,下面我就来讲一下如何使用ES6的新特性async await进行异步处理,使上述情况就好像写同步代码一样,首先我们先举个例子: 先写上json文件: c

  • ES6 十大特性简介

    ES6(ECMAScript2015)的出现,无疑给前端开发人员带来了新的惊喜,它包含了一些很棒的新特性,可以更加方便的实现很多复杂的操作,提高开发人员的效率. 本文主要针对ES6做一个简要介绍. 也许你还不知道ES6是什么, 实际上, 它是一种新的javascript规范.在这个大家都很忙碌的时代,如果你想对ES6有一个快速的了解,那么请继续往下读,去了解当今最流行的编程语言JavaScript最新一代的十大特性. 以下是ES6排名前十的最佳特性列表(排名不分先后): 1.Block-Scop

  • ES6新特性七:数组的扩充详解

    本文实例讲述了ES6新特性之数组的扩充.分享给大家供大家参考,具体如下: 1. Array.from() 1) Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map,他们都部署了iterator接口,字符串也是). let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; // ES5的写法 var arr

  • ES6新特性:使用export和import实现模块化详解

    在ES6前, 前端就使用RequireJS或者seaJS实现模块化, requireJS是基于AMD规范的模块化库,  而像seaJS是基于CMD规范的模块化库,  两者都是为了为了推广前端模块化的工具, 更多有关AMD和CMD的区别, 后面参考给了几个链接: 现在ES6自带了模块化, 也是JS第一次支持module, 在很久以后 ,我们可以直接作用import和export在浏览器中导入和导出各个模块了, 一个js文件代表一个js模块: 现代浏览器对模块(module)支持程度不同, 目前都是

  • ES6新特性之类(Class)和继承(Extends)相关概念与用法分析

    本文实例讲述了ES6新特性之类(Class)和继承(Extends)相关概念与用法.分享给大家供大家参考,具体如下: 一.类(Class) 1.基本语法 JavaScript语言的传统方法是通过构造函数,定义并生成新对象.下面是一个例子 function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'

  • 前端开发不得不知的10个最佳ES6特性

    为了保证可读性,本文采用意译而非直译,并且对源代码进行了大量修改.另外,本文版权归原作者所有,翻译仅用于学习. ES6,正式名称是ECMAScript2015,但是ES6这个名称更加简洁.ES6已经不再是JavaScript最新的标准,但是它已经广泛用于编程实践中.如果你还没用过ES6,现在还不算太晚- 下面是10个ES6最佳特性,排名不分先后: 函数参数默认值 模板字符串 多行字符串 解构赋值 对象属性简写 箭头函数 Promise Let与Const 类 模块化 函数参数默认值 不使用ES6

随机推荐