defineProperty
實現簡單的 Mvvm框架現階段前端三大主流框架
react
,vue
,angular
都屬於 MVVM範疇,即 模型---視圖---視圖模型javascript採用數據驅動, 即監聽數據改變,渲染view。html
核心是監聽數據的變動!前端
其中
React
使用的是diff
算法來實現數據變動檢測的;vue
Angular
則使用的是zone.js
實現數據變動檢測;java
Vue
則使用Object.defineProperty
, 後期版本則使用Object.Proxy
node本文參考
Vue
使用Object.defineProperty
實現數據變動檢測, 實現一個簡單的mvvm
框架react
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
。app
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)
}
複製代碼
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();
}
})
}
}
複製代碼
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);
}
複製代碼
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);
}
複製代碼
以上是一個簡單的mvvm框架的實現,固然defineProperty仍是有一些問題,好比說對應數組的變動檢測是辦不到,而Proxy的出現則解決了這類問題,有時間的話,你們能夠試試基於Proxy去實現一套簡單的mvvm框架。