基於ES5defineProperty
實現簡單的 Mvvm框架
PREPARE
現階段前端三大主流框架
react
,vue
,angular
都屬於 MVVM範疇,即 模型—檢視—檢視模型採用資料驅動, 即監聽資料改變,渲染view。
核心是監聽資料的變更!
其中
React
使用的是diff
演算法來實現資料變更檢測的;
Angular
則使用的是zone.js
實現資料變更檢測;
Vue
則使用Object.defineProperty
, 後期版本則使用Object.Proxy
本文參考
Vue
使用Object.defineProperty
實現資料變更檢測, 實現一個簡單的mvvm
框架
INIT
1.下面是一個簡單的類Vue
元件的實現方式
-
html
<div id="app"> <h1>{{song}}</h1> <p>{{singer.a.b}}</p> <p>{{age}}</p> <input type="text" v-model="age" /> </div> 複製程式碼
-
javascript
let mvvm = new Mvvm({ el: `#app`, data: { song: 2, singer: { a: { b: 1 }, c: 1 }, age: 55 } }) 複製程式碼
-
首先是一個
Mvvm
類,接受兩個引數(後期會加入method等引數):el
和data
。
2. 定義Mvvm
類
function Mvvm(options = {}) {
/*定義類的$option屬性,_data私有屬性,並將 私有$option.data的引用複製給私有屬性_data和區域性變 量data
*/
this.$options = options;
let data = this._data = this.$options.data;
/*將data中所有的key設定觀察者,增加資料變更的檢測*/
observe(data);
/*將data中的所有的key代理到Mvvm例項上,形成mvvm例項樹,方便書寫*/
for (let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
})
}
/*資料如果變更,執行dom渲染*/
new Compile(this.$options.el, this)
}
複製程式碼
3.看一下 observe
方法:
function observe(data) {
if (!data || typeof data !== `object`) return;
return new Observe(data);
}
複製程式碼
增加了一個data
型別檢測機制(實際上為了遞迴結尾判斷),實際上執行 Observe
類的例項化
#####4. Observe
類
function Observe(data) {
/*建立一個訊息訂閱釋出池類,包含一個watcher屬性,watcher指向一個Watcher的例項,每個Watcher例項 都是一個訂閱者,另一個屬性是一個事件池陣列events*/
let dep = new Dep();
/*data的每一個鍵值對迴圈執行observe方法*/
for (let key in data) {
let val = data[key]
observe(val);
/*對data的每一個key進行資料攔截, 設定get,當建立一個Watcher例項的時候,顯示觸發get,此時將這個Watcher例項(訂閱者)新增到訊息訂閱釋出池的事件池中;設定set 當設定新的值時候,觸發 set方法, 如果所設值與原來不相等, 則重新監聽新的值得變更, 並觸發Watcher例項(訂閱者)的notify方法,觸發 Watcher例項的回撥,更新檢視資料*/
Object.defineProperty(data, key, {
configurable: true,
get() {
Dep.watcher && dep.addEvent(Dep.watcher);
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
observe(newVal);
dep.notify();
}
})
}
}
複製程式碼
5.訊息訂閱釋出池Dep
-
類:
function Dep() { this.watcher = null; this.events = []; } 複製程式碼
-
例項:
Dep.prototype = { addEvent(event) { this.events.push(event); }, notify() { this.events.forEach(event => event.update()) } } 複製程式碼
#####6. 訂閱者Watcher
-
類
/*Watcher接收三個引數,第一個是整個mvvm例項樹物件, 第二個是html中的插值表示式{{song.a.b}}, fn為設定新值之後的會調*/ function Watcher(vm, exp, fn) { this.fn = fn; this.vm = vm; this.exp = exp; // 將watcher的例項賦值給Dep的watcher屬性,方便呼叫,而不用傳參 Dep.watcher = this; // 進行一次取值操作, 顯示觸發mvvm例項樹某個key的get方法,從而將 Dep.watcher 新增到事件池中 // 將如song.a.b以點號分割成陣列arr,將mvvm例項樹的引用賦值給區域性變數val let arr = exp.split(`.`); let val = vm; // 迴圈arr 取song.a.b的值 arr.forEach(key => { val = val[key] }); // 新增完之後,釋放 Dep.watcher Dep.watcher = null; } 複製程式碼
-
例項
/*Watcher例項的update方法會從mvvm例項樹上取出exp所對應的值,並觸發fn回撥,渲染檢視*/ Watcher.prototype.update = function () { let arr = this.exp.split(`.`); let val = this.vm; arr.forEach(key => { val = val[key] }); this.fn(val); } 複製程式碼
7.Compile類
Compile類用於將所選的el
元素節點 賦值給mvvm
例項樹,並轉為createDocumentFragment
文件片段, 之後所需要替換的文字節點進行正則匹配並替換,之後將新的文件片段統一新增到el
元素節點中。
關於文件碎片可以 在這裡 瞭解
function Compile(el, vm) {
/*獲取元素節點*/
vm.$el = document.querySelector(el);
/*建立文件碎片物件*/
let fragment = document.createDocumentFragment();
/*當vm.$el.firstChild存在時,將vm.$el.firstChild依次加入到文件碎片中*/
while (child = vm.$el.firstChild) {
fragment.appendChild(child);
}
/*對html文件中的插值表示式等進行替換*/
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
// 插值表示式
let txt = node.textContent;
let reg = /{{(.*?)}}/g;
if (node.nodeType === 1 && reg.test(txt)) {
let arr = RegExp.$1.split(`.`);
let val = vm;
arr.forEach(key => {
val = val[key]
});
// 執行替換
node.textContent = txt.replace(reg, val).trim();
// 新增監聽
new Watcher(vm, RegExp.$1, (newVal) => {
node.textContent = txt.replace(reg, newVal).trim();
})
}
// 雙向資料繫結
if(node.nodeType ===1 ) {
let nodeAttr = node.attributes;
// console.log(nodeAttr) Map
Array.from(nodeAttr).forEach(attr => {
let name = attr.name;
let value = attr.value;
if (name.includes(`v-`)) {
let arr = value.split(`.`);
let val = vm;
arr.forEach( key => {
val = val[key]
})
console.log(value)
node.value = val
}
new Watcher(vm, value, (newVal) => {
node.value = newVal;
});
node.addEventListener(`input`, e => {
// 根據傳入的繫結值的物件深度值來處理, 如果是單個值,則直接賦值, 如果是多個,則使用eval()函式處理
if(value.split(`.`).length > 1) {
eval("vm."+ value + "= e.target.value");
} else {
vm[value] = e.target.value
}
})
})
}
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment);
vm.$el.appendChild(fragment);
}
複製程式碼
LAODED
以上是一個簡單的mvvm框架的實現,當然defineProperty還是有一些問題,比如說對應陣列的變更檢測是辦不到,而Proxy的出現則解決了這類問題,有時間的話,大家可以試試基於Proxy去實現一套簡單的mvvm框架。