通过defineProperty方法实现数据的双向绑定

一、什么是defineProperty?

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。我们常用的vue就是使用defineProperty来实现数据的双向绑定。

二、语法

Object.defineProperty(obj, prop, descriptor)

1. 参数

obj:要在其上定义属性的对象。

prop:要定义或修改的属性的名称。

descriptor:将被定义或修改的属性描述符。

2. 属性描述符

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性。存取描述符是由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()的用法,我们来实现一个简单的双向绑定。

click me

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