资讯专栏INFORMATION COLUMN

使用ES6的新特性Proxy来实现一个数据绑定实例

yanwei / 2710人阅读

摘要:的其中一个卖点,就是数据绑定。为了解决这个问题,再来一个函数最终的函数应该是下面这样子的一个数据绑定的实例,叫做不为什么,就叫做。后记仅仅是一个用于学习数据绑定的实验性质的项目,代码仍然不够优雅,功能也不够丰富。

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

作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过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内容都被修改了,所以会引起两次变化。

实时数据绑定

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

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


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").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走,并且加入一些你的想法。

感谢阅读!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80836.html

相关文章

  • 重温ES6核心概念和基本用法

    摘要:凡是部署了属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。 ES6在2015年6月就得以批准,至今已两年了。近一年多以来陆续看过很多ES6的资料,工作项目中也逐步的用上了很多ES6的特性(let,const,promise,Template strings,Class,箭头函数等等),不得不说,这些特性给开发带来了非常多的便利。但是做决定我的ES6知识其...

    philadelphia 评论0 收藏0
  • [译] ES6 学习笔记:关于 ES2015 特性的详细概述

    摘要:将转换成常见的使用实现的基于迭代器的迭代。处停止迭代器基于鸭子模型接口这里使用语法仅仅为了说明问题使用支持为了使用迭代器属性需要引入。生成器是迭代器的子类,包含了附加的与。 原文地址:http://babeljs.io/docs/learn-...本文基于Luke Hoban精妙的文章《es6features》,请把star献给他,你可以在此尝试这些特性REPL。 概述 ECMAScr...

    leoperfect 评论0 收藏0
  • es6 promise面试

    摘要:执行函数会返回一个遍历器对象,每一次函数里面的都相当一次遍历器对象的方法,并且可以通过方法传入自定义的来改变函数的行为。函数可以通过配合函数更轻松更优雅的实现异步编程和控制流管理。它和构造函数的不同点类的内部定义的所有方法,都是不可枚举的。 let const的命令 在ES6之前,声明变量只能用var,var方式声明变量其实是很不合理的,准确的说,是因为ES5里面没有块级作用域是很不合...

    timger 评论0 收藏0
  • 【重温基础】14.元编程

    摘要:本文是重温基础系列文章的第十四篇。元,是指程序本身。有理解不到位,还请指点,具体详细的介绍,可以查看维基百科元编程。拦截,返回一个布尔值。 本文是 重温基础 系列文章的第十四篇。 这是第一个基础系列的最后一篇,后面会开始复习一些中级的知识了,欢迎持续关注呀! 接下来会统一整理到我的【Cute-JavaScript】的JavaScript基础系列中。 今日感受:独乐乐不如众乐乐...

    cc17 评论0 收藏0
  • 使用ES6特性实现简单的MVVM(1)--数据驱动

    摘要:尝试使用新特性,自己来实现一个及的各种特性。我们可以利用这个特性来实现对数据的监听结果简单的操作我们已经可以对简单的数据操作进行监听虽然还有各种问题,接下来,我们只要在监听到后进行数据操作即可。 尝试使用es6新特性,自己来实现一个mvvm及vue的各种特性。相关代码放在github,会持续更新,欢迎赏个star。本篇文章为系列文章的第一篇,会比较容易理解,后续会持续更新后面的记录。文...

    ispring 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<