前言
上一篇文章说了ES6中的Proxy,现在就来利用proxy一步步实现一个模拟vue的双向绑定。
目录
如何实现
- 在学习vue的时候,vue是通过劫持数据的变化,监听到数据变化时改变前端视图。
- 那么要实现双向绑定,必然需要一个监听数据的方法。如文章标题所示,这里使用的
proxy
实现数据的监听。
- 当监听到数据变化时,需要一个watcher响应并调用更新数据的compile方法去更新前端视图。
- 在vue中
v-model
作为绑定的入口。当我们监听到前端input输入信息并绑定了数据项的时候,需要先告知watcher,由watcher改变监听器的数据。
- 大概的双向绑定的原理为:

1.实现一个observer(数据监听器)
利用proxy
实现一个数据监听器很简单,因为proxy
是监听整个对象的变化的,所以可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 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'; vm.data.test = 'changeTest';
vm.data.name; vm.data.test;
|
这样,数据监听器已经基本实现了,但是现在这样只能监听到数据的变化,不能改变前端的视图信息。现在需要实现一个更改前端信息的方法,在VM类中添加方法changeElementData
1 2 3 4 5
|
changeElementData(value) { this.el.innerHTML = value; }
|
在监听到数据变化时调用changeElementData
改变前端数据,handler
的set
方法中调用方法
1 2 3 4
| set(target, propkey, value) { this.changeElementData(value); return true; }
|
在init中设置一个定时器更改数据
1 2 3 4 5 6 7 8
| init() { this.observer(); setTimeout(() => { console.log('change data !!'); this.data.name = 'hello world'; }, 1000) }
|
已经可以看到监听到的信息改变到前端了,但是!

这样写死的绑定数据显然是没有意义,现在实现的逻辑大概如下面的图

2.实现数据动态更新到前端
上面实现了一个简单的数据绑定展示,但是只能绑定一个指定的节点去改变此节点的数据绑定。这样显然是不能满足的,我们知道vue中是以{{key}}
这样的形式去绑定展示的数据的,而且vue中是监听指定的节点的所有子节点的。因此对象中需要在VIEW和OBSERVER之间添加一个监听层WATCHER。当监听到数据发生变化时,通过WATCHER去改变VIEW,如图:

根据这个流程,下一步我们需要做的是:
- 监听整个绑定的element的所有节点并匹配所有节点中的所有
{{text}}
模板
- 监听到数据变化时,告知watcher需要数据改变了,替换前端模板
在VM类的构造器中添加三个参数
1 2 3 4 5
| constructor() { this.fragment = null; this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); this.nodeArr = []; }
|
新建一个方法遍历el
中的所有节点,并存放到fragment
中
1 2 3 4 5 6 7 8 9 10 11 12 13
|
createDocumentFragment() { let documentFragment = document.createDocumentFragment(); let child = this.el.firstChild; while (child) { documentFragment.appendChild(child); child = this.el.firstChild; } this.fragment = documentFragment; }
|
匹配{{}}
的数据并替换模版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| /** * 匹配模板 * @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更新前端
1 2 3 4 5
| watcher(key) { for(const node of this.nodeArr) { this.changeData(node); } }
|
在init
和proxy
的set
方法中执行新增的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| init() { this.observer(); this.createDocumentFragment(); 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; }
|
测试一下:


3.实现数据双向绑定
现在我们的程序已经可以通过改变data动态地改变前端的展示了,接下来需要实现的是一个类似VUEv-model
绑定input的方法,通过input输入动态地将输入的信息输出到对应的前端模板上。大概的流程图如下:

一个简单的实现流程大概如下:
- 获取所有带有v-model的input结点
- 监听输入的信息并设置到对应的data中
在constructor中添加
1 2 3 4
| constructor() { this.modelObj = {}; }
|
在VM类中新增方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| bindModelData(key, node) { if (this.data[key]) { node.addEventListener('input', (e) => { this.data[key] = e.target.value; }, false); } }
setModelData(key, node) { node.value = this.data[key]; }
checkAttribute(node) { return node.getAttribute('y-model'); }
|
在watcher
中执行setModelData
方法,matchElementModule
中执行bindModelData
方法。
修改后的matchElementModule
和watcher
方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| matchElementModule(key, fragment) { const childNodes = fragment || this.fragment.childNodes; [].slice.call(childNodes).forEach((node) => {
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; 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); } }
|
来看一下是否已经成功绑定了,写一下测试代码:


成功!!
最终的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
| 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 = {}; 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; }
matchElementModule(key, fragment) { const childNodes = fragment || this.fragment.childNodes; [].slice.call(childNodes).forEach((node) => {
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; this.changeData(node); this.nodeArr.push(node); }
if(node.childNodes && node.childNodes.length) { this.matchElementModule(key, node.childNodes); } }) }
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); } }
bindModelData(key, node) { if (this.data[key]) { node.addEventListener('input', (e) => { this.data[key] = e.target.value; }, false); } } setModelData(key, node) { node.value = this.data[key]; }
checkAttribute(node) { return node.getAttribute('y-model'); } }
|
最后
本节我们使用Proxy
,从监听器开始,到观察者一步步实现了一个模仿VUE的双向绑定,代码中也许会有很多写的不严谨的地方,如发现错误麻烦大佬们指出~~