上一篇文章说了ES6中的Proxy,如今就来利用proxy一步步实现一个模拟vue的双向绑定。javascript
proxy
实现数据的监听。v-model
做为绑定的入口。当咱们监听到前端input输入信息并绑定了数据项的时候,须要先告知watcher,由watcher改变监听器的数据。利用proxy
实现一个数据监听器很简单,由于proxy
是监听整个对象的变化的,因此能够这样写:前端
class VM {
constructor(options, elementId) {
this.data = options.data || {}; // 监听的数据对象
this.el = document.querySelector(elementId);
this.init(); // 初始化
}
// 初始化
init() {
this.observer();
}
// 监听数据变化方法
observer() {
const handler = {
get: (target, propkey) => {
console.log(`监听到${propkey}被取啦,值为:${target[propkey]}`);
return target[propkey];
},
set: (target, propkey, value) => {
if(target[propkey] !== value){
console.log(`监听到${propkey}变化啦,值变为:${value}`);
}
return true;
}
};
this.data = new Proxy(this.data, handler);
}
}
// 测试一下
const vm = new VM({
data: {
name: 'defaultName',
test: 'defaultTest',
},
}, '#app');
vm.data.name = 'changeName'; // 监听到name变化啦,值变为:changeName
vm.data.test = 'changeTest'; // 监听到test变化啦,值变为:changeTest
vm.data.name; // 监听到name被取啦,值为:changeName
vm.data.test; // 监听到test被取啦,值为:changeTest
复制代码
这样,数据监听器已经基本实现了,可是如今这样只能监听到数据的变化,不能改变前端的视图信息。如今须要实现一个更改前端信息的方法,在VM类中添加方法changeElementData
vue
// 改变前端数据
changeElementData(value) {
this.el.innerHTML = value;
}
复制代码
在监听到数据变化时调用changeElementData
改变前端数据,handler
的set
方法中调用方法java
set(target, propkey, value) {
this.changeElementData(value);
return true;
}
复制代码
在init中设置一个定时器更改数据node
init() {
this.observer();
setTimeout(() => {
console.log('change data !!');
this.data.name = 'hello world';
}, 1000)
}
复制代码
已经能够看到监听到的信息改变到前端了,可是! bash
这样写死的绑定数据显然是没有意义,如今实现的逻辑大概以下面的图 app
上面实现了一个简单的数据绑定展现,可是只能绑定一个指定的节点去改变此节点的数据绑定。这样显然是不能知足的,咱们知道vue中是以{{key}}
这样的形式去绑定展现的数据的,并且vue中是监听指定的节点的全部子节点的。所以对象中须要在VIEW和OBSERVER之间添加一个监听层WATCHER。当监听到数据发生变化时,经过WATCHER去改变VIEW,如图: post
{{text}}
模板在VM类的构造器中添加三个参数学习
constructor() {
this.fragment = null; // 文档片断
this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); // 匹配全部{{}}模版
this.nodeArr = []; // 全部带有模板的前端结点
}
复制代码
新建一个方法遍历el
中的全部节点,并存放到fragment
中测试
/** * 建立一个文档片断 */
createDocumentFragment() {
let fragment = document.createDocumentFragment();
let child = this.el.firstChild;
// 循环添加到文档片断中
while (child) {
this.fragment.appendChild(child);
child = this.el.firstChild;
}
this.fragment = fragment;
}
复制代码
匹配{{}}
的数据并替换模版
/**
* 匹配模板
* @param { string } key 触发更新的key
* @param { documentElement } fragment 结点
*/
matchElementModule(key, fragment) {
const childNodes = fragment || this.fragment.childNodes;
[].slice.call(childNodes).forEach((node) => {
if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
this.changeData(node);
this.nodeArr.push(node); // 保存带有模板的结点
}
// 递归遍历子节点
if(node.childNodes && node.childNodes.length) {
this.matchElementModule(key, node.childNodes);
}
})
}
/**
* 改变视图数据
* @param { documentElement } node
*/
changeData(node) {
const matchArr = node.defaultContent.match(this.matchModuleReg); // 获取全部须要匹配的模板
let tmpStr = node.defaultContent;
for(const key of matchArr) {
tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || '');
}
node.textContent = tmpStr;
}
复制代码
实现watcher,数据变化是触发此watcher更新前端
watcher(key) {
for(const node of this.nodeArr) {
this.changeData(node);
}
}
复制代码
在init
和proxy
的set
方法中执行新增的方法
init() {
this.observer();
this.createDocumentFragment(this.el); // 将绑定的节点都放入文档片断中
for (const key of Object.keys(this.data)) {
this.matchElementModule(key);
}
this.el.appendChild(this.fragment); // 将初始化的数据输出到前端
}
set: () => {
if(target[propkey] !== value) {
target[propkey] = value;
this.watcher(propkey);
}
return true;
}
复制代码
测试一下:
如今咱们的程序已经能够经过改变data动态地改变前端的展现了,接下来须要实现的是一个相似VUEv-model
绑定input的方法,经过input输入动态地将输入的信息输出到对应的前端模板上。大概的流程图以下:
一个简单的实现流程大概以下:
在constructor中添加
constructor() {
this.modelObj = {};
}
复制代码
在VM类中新增方法
// 绑定 y-model
bindModelData(key, node) {
if (this.data[key]) {
node.addEventListener('input', (e) => {
this.data[key] = e.target.value;
}, false);
}
}
// 设置 y-model 值
setModelData(key, node) {
node.value = this.data[key];
}
// 检查y-model属性
checkAttribute(node) {
return node.getAttribute('y-model');
}
复制代码
在watcher
中执行setModelData
方法,matchElementModule
中执行bindModelData
方法。
修改后的matchElementModule
和watcher
方法以下
matchElementModule(key, fragment) {
const childNodes = fragment || this.fragment.childNodes;
[].slice.call(childNodes).forEach((node) => {
// 监听全部带有y-model的结点
if (node.getAttribute && this.checkAttribute(node)) {
const tmpAttribute = this.checkAttribute(node);
if(!this.modelObj[tmpAttribute]) {
this.modelObj[tmpAttribute] = [];
};
this.modelObj[tmpAttribute].push(node);
this.setModelData(tmpAttribute, node);
this.bindModelData(tmpAttribute, node);
}
// 保存全部带有{{}}模版的结点
if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
this.changeData(node);
this.nodeArr.push(node); // 保存带有模板的结点
}
// 递归遍历子节点
if(node.childNodes && node.childNodes.length) {
this.matchElementModule(key, node.childNodes);
}
})
}
watcher(key) {
if (this.modelObj[key]) {
this.modelObj[key].forEach(node => {
this.setModelData(key, node);
})
}
for(const node of this.nodeArr) {
this.changeData(node);
}
}
复制代码
来看一下是否已经成功绑定了,写一下测试代码:
成功!!
最终的代码以下:
class VM {
constructor(options, elementId) {
this.data = options.data || {}; // 监听的数据对象
this.el = document.querySelector(elementId);
this.fragment = null; // 文档片断
this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); // 匹配全部{{}}模版
this.nodeArr = []; // 全部带有模板的前端结点
this.modelObj = {}; // 绑定y-model的对象
this.init(); // 初始化
}
// 初始化
init() {
this.observer();
this.createDocumentFragment();
for (const key of Object.keys(this.data)) {
this.matchElementModule(key);
}
this.el.appendChild(this.fragment);
}
// 监听数据变化方法
observer() {
const handler = {
get: (target, propkey) => {
return target[propkey];
},
set: (target, propkey, value) => {
if(target[propkey] !== value) {
target[propkey] = value;
this.watcher(propkey);
}
return true;
}
};
this.data = new Proxy(this.data, handler);
}
/** * 建立一个文档片断 */
createDocumentFragment() {
let documentFragment = document.createDocumentFragment();
let child = this.el.firstChild;
// 循环向文档片断添加节点
while (child) {
documentFragment.appendChild(child);
child = this.el.firstChild;
}
this.fragment = documentFragment;
}
/** * 匹配模板 * @param { string } key 触发更新的key * @param { documentElement } fragment 结点 */
matchElementModule(key, fragment) {
const childNodes = fragment || this.fragment.childNodes;
[].slice.call(childNodes).forEach((node) => {
// 监听全部带有y-model的结点
if (node.getAttribute && this.checkAttribute(node)) {
const tmpAttribute = this.checkAttribute(node);
if(!this.modelObj[tmpAttribute]) {
this.modelObj[tmpAttribute] = [];
};
this.modelObj[tmpAttribute].push(node);
this.setModelData(tmpAttribute, node);
this.bindModelData(tmpAttribute, node);
}
// 保存全部带有{{}}模版的结点
if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
this.changeData(node);
this.nodeArr.push(node); // 保存带有模板的结点
}
// 递归遍历子节点
if(node.childNodes && node.childNodes.length) {
this.matchElementModule(key, node.childNodes);
}
})
}
/** * 改变视图数据 * @param { documentElement } node */
changeData(node) {
const matchArr = node.defaultContent.match(this.matchModuleReg); // 获取全部须要匹配的模板
let tmpStr = node.defaultContent;
for(const key of matchArr) {
tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || '');
}
node.textContent = tmpStr;
}
watcher(key) {
if (this.modelObj[key]) {
this.modelObj[key].forEach(node => {
this.setModelData(key, node);
})
}
for(const node of this.nodeArr) {
this.changeData(node);
}
}
// 绑定 y-model
bindModelData(key, node) {
if (this.data[key]) {
node.addEventListener('input', (e) => {
this.data[key] = e.target.value;
}, false);
}
}
// 设置 y-model 值
setModelData(key, node) {
node.value = this.data[key];
}
// 检查y-model属性
checkAttribute(node) {
return node.getAttribute('y-model');
}
}
复制代码
本节咱们使用Proxy
,从监听器开始,到观察者一步步实现了一个模仿VUE的双向绑定,代码中也许会有不少写的不严谨的地方,如发现错误麻烦大佬们指出~~