本文实例讲述了ES6使用新特性Proxy实现的数据绑定功能。分享给大家供大家参考,具体如下:
项目地址:https://github.com/jrainlau/mog
在线体验:https://codepen.io/jrainlau/pen/YpyBBY
项目地址:https://github.com/jrainlau/mog
在线体验:https://codepen.io/jrainlau/pen/YpyBBYhttps://github.com/jrainlau/moghttps://codepen.io/jrainlau/pen/YpyBBY作为一个前端开发者,曾踩了太多的“数据绑定”的坑。在早些时候,都是通过jQuery之类的工具手动完成这些功能,但是当数据量非常大的时候,这些手动的工作让我非常痛苦。直到使用了VueJS,这些痛苦才得以终结。jQueryVueJSVueJS的其中一个卖点,就是“数据绑定”。使用者无需关心数据是怎么绑定到dom上面的,只需要关注数据就好,因为VueJS已经自动帮我们完成了这些工作。VueJSVueJS这真的非常神奇,我不可救药地爱上了VueJS,并且把它用到我自己的项目当中。随着使用的深入,我更加想知道它深入的原理是什么。VueJSVueJS是如何进行数据绑定的?通过阅读官方文档,我看到了下面这段话:
把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。
把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。关键词是Object.definProperty,在MDN文档里面是这么说的:Object.definPropertyMDN文档
Object.defineProperty()方法直接定义一个对象的属性,或者修改对象当中一个已经存在的属性,并返回这个对象。
Object.defineProperty()方法直接定义一个对象的属性,或者修改对象当中一个已经存在的属性,并返回这个对象。Object.defineProperty()让我们写个例子来测试一下它。首先,建立一个钢铁侠对象并赋予他一些属性:
let ironman = {
name: 'Tony Stark',
sex: 'male',
age: '35'
}
let ironman = {
name: 'Tony Stark',
sex: 'male',
age: '35'
}现在我们使用Object.defineProperty()方法来对他的一些属性进行修改,并且在控制台把所修改的内容输出:Object.defineProperty()
Object.defineProperty(ironman, 'age', {
set (val) {

console.log(`Set age to ${val}`)

return val
}
})

ironman.age = '48'
// --> Set age to 48
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,是不是就意味着数据绑定已经完成了呢?console.log('Set age to ${val}')element.innerHTML = val让我们再修改一下钢铁侠的属性:
let ironman = {
name: 'Tony Stark',
sex: 'male',
age: '35',
hobbies: ['girl', 'money', 'game']
}
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' ]
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()方法来代替。虽然这招也灵,但是并非最好的办法。有没有更好的方法可以简化这些追踪对象或数组属性变化的方法呢?get()value()在ECMA2021,Proxy是一个不错的选择什么是Proxy?在MDN文档中是这么说的(误):ProxyMDN文档MDN文档
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy是ECMA2021的一个新特性,它非常强大,但我并不会讨论太多关于它的东西,除了我们现在需要的一个。现在让我们一起来新建一个Proxy实例: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, {
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' ]
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内容都被修改了,所以会引起两次变化。change...push()lengthbody实时数据绑定解决了最核心的问题,可以考虑其他的问题了。想象一下,我们有一个模板和数据对象:

Hello, my name is {{name}}, I enjoy eatting {{hobbies.food}}




let ironman = {
name: 'Tony Stark',
sex: 'male',
age: '35',
hobbies: {

food: 'banana',

drink: 'wine'
}
}

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实例的函数吧!ProxyProxy
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)
}
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)

*/
})
$setData(dataObj, ($d) => {
/*

* dataObj.someProps = something

*/
})

// 或者

$setData(dataObj.arrayProps, ($d) => {
/*

* dataObj.push(something)

*/
})除此之外,我们应该实现模板对数据对象的映射,这样才能用Tony Stark来替换{{name}}。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
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']。{ 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' }
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()函数应该是下面这样子的: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])

}

})
}
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())
}

}
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:









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: 'Ecma2021'

},

motto: 'Every dog has his day'
}
})

document.querySelector('#set-motto').oninput = (e) => {
mog.$setData(mog.$data, ($d) => {

$d.motto = e.target.value
})
}


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: 'Ecma2021'

},

motto: 'Every dog has his day'
}
})

document.querySelector('#set-motto').oninput = (e) => {
mog.$setData(mog.$data, ($d) => {

$d.motto = e.target.value
})
}你可以在这里进行在线体验。这里后记Mog仅仅是一个用于学习数据绑定的实验性质的项目,代码仍然不够优雅,功能也不够丰富。但是这个小玩具让我学习了很多。如果你对它有兴趣,欢迎到这里把项目fork走,并且加入一些你的想法。Mog这里感谢阅读!感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools./code/HtmlJsRun测试上述代码运行效果。在线HTML/CSS/JavaScript代码运行工具在线HTML/CSS/JavaScript代码运行工具http://tools./code/HtmlJsRun关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》javascript面向对象入门教程JavaScript错误与调试技巧总结JavaScript数据结构与算法技巧总结JavaScript遍历算法与技巧总结JavaScript数学运算用法总结希望本文所述对大家JavaScript程序设计有所帮助。