Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。我们常用的vue就是使用defineProperty来实现数据的双向绑定。
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。我们常用的vue就是使用defineProperty来实现数据的双向绑定。
obj:要在其上定义属性的对象。
prop:要定义或修改的属性的名称。
descriptor:将被定义或修改的属性描述符。
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
enumerable:当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。
configurable:当且仅当该属性的configurable为true时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。如果为false,则任何尝试删除目标属性或修改属性以下特性(writable, configurable, enumerable)的行为将被无效化
value数据描述符:该属性对应的值。可以是任何有效的JavaScript值(数值,对象,函数等)。默认为undefined。
writable数据描述符:当且仅当该属性的writable为true时,value才能被赋值运算符改变。如果设置成 false,则任何对该属性改写的操作都无效(但不会报错)。
get存取描述符:一个给属性提供getter的方法,如果没有getter则为undefined。一旦目标对象访问该属性,就会调用这个方法,并返回结果。
set存取描述符:一个给属性提供setter的方法,如果没有setter则为undefined。 该方法将接受唯一参数,并将该参数的新值分配给该属性。一旦目标对象设置该属性,就会调用这个方法。
如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。
为了进一步了解Object.defineProperty()的用法,我们来实现一个简单的双向绑定。
html代码如下:
<div id="demo-app">
<input type="text" value="" g-model="num">
<span class="add" g-click="add">click me</span>
<div class="result" g-bind="num"></div>
</div>
相关的js如下:
let myApp = new G({
el: '#demo-app',
data () {
return {
num: 0
}
},
methods: {
add () {
this.num++
}
}
})
no bb, show code:
/* created at 2018-5-3 */
// 先来一个构造函数G,并对它初始化
function G (options) {
this._init(options)
}
// 用来存储所有的Watcher,在data改变时遍历Watcher并更新
G.prototype._binding = {}
// 初始化
G.prototype._init = function (options) {
this.$options = options
this.$el = document.querySelector(options.el)
this.$data = options.data()
this.$methods = options.methods
this._observe(this.$data)
this._compile(this.$el)
}
// 绑定value和this.$data
G.prototype._observe = function (data) {
let value
let _this = this
for (let key in data) {
value = data[key]
this._binding[key] = []
// 键值如果还是object继续遍历
if (typeof value === 'object') {
this._observe(value)
}
// 这次的重点部分,通过defineProperty方法绑定value和this.$data
Object.defineProperty(this.$data, key, {
enumberable: true,
configurable: true,
get () {
return value
},
set (newVal) {
if (value !== newVal) {
value = newVal
// 这里是在数据有改动时,遍历所有的Watcher,并通过_update方法绑定更新dom内的表现
_this._binding[key].forEach(function (item) {
item._update()
})
}
}
})
}
}
// 编译dom结构上的g-x属性
G.prototype._compile = function (el) {
let nodes = el.children
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
if (node.hasAttribute('g-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
// 把g-model的node实例化为GWatcher并存入this._binding
node.addEventListener('input', (() => {
let attr = node.getAttribute('g-model')
this._binding[attr].push(new GWatcher(
node,
'value',
this.$data,
attr
))
// 立即执行的匿名函数返回如下的方法(关联g-modele,更新this.$data)
return () => {
this.$data[attr] = node.value
}
})(), false)
}
// 把g-bind的node实例化为GWatcher并存入this._binding
if (node.hasAttribute('g-bind')) {
let attr = node.getAttribute('g-bind')
this._binding[attr].push(new GWatcher(
node,
'innerHTML',
this.$data,
attr
))
}
if (node.hasAttribute('g-click')) {
let fn = node.getAttribute('g-click')
node.addEventListener('click', () => {
// 注册点击事件,执行g-click的方法,并把作用于绑定到this.$data
this.$methods[fn].bind(this.$data)()
}, false)
}
}
}
/**
* 构造函数
* @param {object} el 需要操作的node节点
* @param {string} attr 改变页面表现的属性
* @param {object} vm 绑定的数据对象
* @param {string} key 需要操作的this.$data内的键名
*/
function GWatcher (el, attr, vm, key) {
this.$el = el
this.$attr = attr
this.$vm = vm
this.$key = key
this._update()
}
// 关联g-bind的node节点和this.$data
GWatcher.prototype._update = function () {
this.$el[this.$attr] = this.$vm[this.$key]
}