上一篇文章說了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的雙向綁定,代碼中也許會有不少寫的不嚴謹的地方,如發現錯誤麻煩大佬們指出~~