使用访问器属性编写一个双向数据绑定

一、原理
  1. 双向数据绑定:数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变;
  2. 实现方法:通过Object.defineProperty()方法来实现;

Object.defineProperty()方法(JavaScript高级程序设计第三版P139):
Object.defineProperty()这个方法接受三个参数 属性所在的对象,属性的名字以及一个描述符对象.

let book = {
	_year: 2004,
	edition: 1
}
Object.defineProperty(book, 'year', {
    get: function() {
	    console.log('调用了get方法');
	    return this._year;
	},
	set: function(newvalue) {
		if (this._year !== newvalue) {
			console.log('调用了set方法');
			this._year = newvalue;
			this.edition += 1;
		}
	}
});
book.year; // 调用了get方法
book.year = 2021; // 调用了set方法
二、双向数据绑定1.0
function Bind(options) {
	this.el = options.el;
	this.data = options.data;
	getNode(this.el, this.data);
}

function getNode(el, data) {
	let inputs = document.querySelector(el);
	let children = inputs.children;
	for (let i = 0; i < children.length; i++) {
		compile(data, children[i]);
	}
}

function compile(obj, data) {
	let key = node.getAttribute('v-model');
	define(data, key, data[key]);
}

function define(obj, key, val) {
	Object.defineProperty(obj, key, {
		get: function() {
			return val;
		},
		set: function(newvalue) {
			if (val !== newvalue) {
				val = newvalue;
				document.getElementsByClassName('insert')[0].innerHTML = newvalue;
			}
		}
	})
}

let inputs = document.getElementsByTagName('input');

let vm = new Bind({
	el: '#app',
	data: {
		name: '',
		age: ''
	}
});

function input1() {
	vm.data.name = inputs[0].value;
}

function input2() {
	vm.data.age = inputs[1].value;
}

这段代码虽然实现了实现了双向数据绑定,但也仅仅只是实现了这个功能,它还存在许多bug
比如:

  1. 代码整体看上去很混乱,并且复用性不强;
  2. 使用的是函数的方法来定义Bind,所以编写代码的时候就需要时时考虑this的指向问题;
  3. 在Bind()函数里面,如果options里面没有el这个属性,那么this.el就为null,后面在获取标签的时候也获取不了,后面也会跟着出错;compile()函数里面也没有判断key是否存在等等;
  4. 在input标签里面添加oninput事件来获取输入的数据;
三、双向数据绑定2.0

改进:

  1. 使用class来定义Bind;
  2. 使用 || 来防止参数options里面不存在el,data这些数据;
  3. 通过使用addEventListener来添加事件,并且考虑了兼容问题;
  4. 为Bind添加了errorList数组来记录绑定时和绑定之后的错误;
  5. 通过创建一个BindError对象,并采用轮询的方法,来对Bind进行监听;

在这里插入图片描述

class Bind {
	constructor(options) {
		this.el = options.el || "";
		this.data = options.data || {};
		this.errorList = [];
		this.getNode();
	}

	getNode() {
		this.rootNode = document.querySelector(this.el);
		const children = this.rootNode ? Array.prototype.slice.call(this.rootNode.children) : [];
		children.forEach((child) => {
			this.complie(child);
		})
	}

	complie(node) {
		const key = node.getAttribute('v-model');
		if (key) {
			this.addEvent(node, 'input', (e) => {
				const { value = 'error' } = e.target;
				this.data[key] = value;
			});
			this.define(this.data, key, this.data[key]);
		} else {
			var div = document.createElement('div');
			div.appendChild(node)
			this.errorList.push(`${div.innerHTML} doesn't have attribute v-model! `);
		}
	}

	addEvent(node, type, handler) {
		if (node.addEventListener) {
			node.addEventListener(type, handler)
		} else if (node.attachEvent) {
			node.attachEvent('on' + type, handler)
		} else {
			node[type] = null;
		}
	}

	define(obj, key, val) {
		Object.defineProperty(obj, key, {
			get: function () {
				return val;
			},
			set: function (newvalue) {
				if (newvalue !== val) {
					val = newvalue;
					document.getElementsByClassName('insert')[0].innerHTML = val;
				}
			}
		});
	}
}
var error = new BindError();
var vm = new Bind({
	el: '#app',
	data: {
		name: '',
		age: ''
	}
})
error.addListenerList(vm);

error.js

class BindError {
    constructor() {
        this.listenerList = [];
    }

    addListenerList(obj) {
        this.listenerList.push(obj);
    }

    interval = setInterval(()=>{
        this.listenerList.forEach((obj)=>{
            var { errorList } = obj;
            var error = errorList.pop();
            if(error){
                throw new Error(error);
            }
        })
    },1000);
}

不足:使用轮询,容易造成资源浪费

双向数据绑定3.0

改进:不适用轮询,使用es6的Proxy来对Bind里面的errorList进行一个拦截;
在这里插入图片描述

class Bind {
	constructor(options) {
		this.el = options.el || "";
		this.data = options.data || {};
		this.errorList = new Proxy([], {
			set(target, prop, value) {
				Reflect.set(target, prop, value);
				throw new Error(value);
			}
		});
		this.getNode();
	}

	getNode() {
		this.rootNode = document.querySelector(this.el);
		const children = this.rootNode ? Array.prototype.slice.call(this.rootNode.children) : [];
		children.forEach((child) => {
			this.complie(child);
		})
	}

	complie(node) {
		const key = node.getAttribute('v-model');
		if (key) {
			this.addEvent(node, 'input', (e) => {
				const { value = 'error' } = e.target;
				this.data[key] = value;
			});
			this.define(this.data, key, this.data[key]);
		} else {
			var div = document.createElement('div');
			div.appendChild(node)
			this.errorList.push(`${div.innerHTML} doesn't have attribute v-model! `);
		}
	}

	addEvent(node, type, handler) {
		if (node.addEventListener) {
			node.addEventListener(type, handler)
		} else if (node.attachEvent) {
			node.attachEvent('on' + type, handler)
		} else {
			node[type] = null;
		}
	}

	define(obj, key, val) {
		Object.defineProperty(obj, key, {
			get: function () {
				return val;
			},
			set: function (newvalue) {
				if (newvalue !== val) {
					val = newvalue;
					document.getElementsByClassName('insert')[0].innerHTML = val;
				}
			}
		});
	}
}

var vm = new Bind({
	el: '#app',
	data: {
		name: '',
		age: ''
	}
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值