JavaScript 實現一個簡單的MVVM前端框架(ES6語法)
阿新 • • 發佈:2019-04-06
模仿 image 詳細 發布 regexp doc eof bject htm
前言
隨著前端各大框架的崛起,為我們平時的開發帶來了相當的便利,我們不能一直停留在應用層面,今天就自己動手實現一個乞丐版的MVVM小框架
完整代碼github地址
效果
html代碼
<div id="app">
<p>{{a}}</p>
<p>{{b.b}}</p>
<input type="text" v-model="a">
</div>
js調用代碼
const vm = new Mvvm({ el: '#app', data: { a: 1, b: { b : 2 } } })
基本是模仿vue的調用方式
實現步驟
- 數據劫持Observe
- 數據代理(讓Mvvm對象可以處理數據)
- 模板編譯Compile
- 發布訂閱
- 視圖與數據進行關聯
- 實現雙向數據綁定
代碼分析
// 定義框架類名Mvvm,我們則可以直接實例化new Mvvm() 來調用 class Mvvm { constructor(options){ /** * options 則是前臺傳來的數據 * { el: '#app', data: { a: 1, b: { b : 2 } } } */ const {el,data} = options; this._data = data; Observe.observeData(data); // 通過該函數把所有前臺傳來的data中的數據劫持 this.mount(data); // 把所有的data數據代理到this,也就是Mvvm對象上 Mvvm.compile(el,this); // 解析模板數據,也就是解析HTML中的{{a}} {{b.b}} } // 把data中的數據掛載到this上 mount(data){ // 遍歷data數據 通過defineProperty進行重新創建屬性到this上 for(let key in data){ Object.defineProperty(this,key,{ enumerable:true, // 可枚舉 get(){ return this._data[key]; }, set(newVal){ this._data[key] = newVal; } }) } } // 解析模板功能 static compile(el,_that){ new Compile(el,_that); } } // 對數據進行劫持 class Observe{ constructor(data){ this.deepObserve(data); } deepObserve(data){ let dep = new Dep(); // 創建一個可觀察對象 for(let key in data){ let value = data[key]; Observe.observeData(value); // 遞歸調用數據劫持方法 this.mount(data,key,value,dep); // 數據劫持主體方法 } } mount(data,key,value,dep){ // 其實就是把data中的數據一層層遞歸的通過defineProperty方式創建 Object.defineProperty(data,key,{ enumerable:true, get(){ Dep.target && dep.addSub(Dep.target); //Dep.target = watcher 這個存在的時候,添加到可觀察對象數組中 return value; // get返回該值 }, set(newVal){ // 當設置值時,新值老值進行比對 if(newVal === value){ return; } value = newVal; Observe.observeData(newVal);// 把後來手動設置的值也劫持了 dep.notify(); // 發布所有的訂閱來更新界面數據 } }) } static observeData(data){ // 遞歸的終止條件,這裏寫的並不完善!! 我們主要目的還是理解mvvm if(typeof data !== 'object'){ return ; } return new Observe(data); } } class Compile{ constructor(el,vm){ // vm = this vm.$el = document.querySelector(el); // 創建一個文檔片段 let fragment = document.createDocumentFragment(); let child; while(child = vm.$el.firstChild){ // 不斷遍歷DOM,添加到文檔片段(內存)中 fragment.appendChild(child); } // replace是解析HTML的核心函數 this.replace(fragment,this,vm); // 把更新後的文檔片段插入回DOM,達到更新視圖的目的 vm.$el.appendChild(fragment); } // 解析DOM replace(fragment,that,vm){ // 循環文檔片段中的DOM節點 Array.from(fragment.childNodes).forEach(function (node) { let text = node.textContent; // 節點值 let reg = /\{\{(.*)\}\}/; // 正則匹配{{}}裏面的值 // nodeType === 3 表示文本節點 if(node.nodeType === 3 && reg.test(text)){ let arr = RegExp.$1.split('.'); // RegExp.$1獲取到 b.b , 並通過.轉換成數組 let val = vm; // val 指針指向 vm對象地址 arr.forEach(function (k) { val = val[k]; // vm['b'] 可以一層層取到值 }); // 給這個node創建一個watcher對象,用於後期視圖動態更新使用 new Watcher(vm,RegExp.$1,function (newVal) { node.textContent = text.replace(reg,newVal); }); // 更新視圖 {{a}} ==> 1 node.textContent = text.replace(reg,val); } // 元素節點 if(node.nodeType === 1){ let nodeAttrs = node.attributes; // 獲取DOM節點上的屬性列表 // 遍歷該屬性列表 Array.from(nodeAttrs).forEach((attr)=>{ let name = attr.name; // 獲取屬性名 v-model let exp = attr.value; // 獲取屬性值 "a" if(name.startsWith('v-')){ node.value = vm[exp]; // 實現了把a的值添加到input輸入框內 } // 給該node創建一個watcher對象,用於動態更新視圖 new Watcher(vm,exp,function (newVal) { node.value = newVal; // 更新輸入框的值 }); // 輸入框添加事件 node.addEventListener('input',function (e) { // 會調用數據劫持中的set方法,從而觸發 dep.notify()發布所有的訂閱來更新界面數據 vm[exp] = e.target.value; },false); }) } // 遞歸解析DOM節點 if(node.childNodes){ that.replace(node,that,vm); } }); } } // 簡單的發布訂閱 class Dep{ constructor(){ this.subs = []; } addSub(sub){ this.subs.push(sub); } notify(){ this.subs.forEach(sub=>{ sub.update(); }) } } // Watcher對象,用來跟node的關聯起來。把後期需要更新的node變成Watcher對象,存入內存中 class Watcher{ constructor(vm,exp,fn){ this.vm = vm; // this對象 this.exp = exp; // 值 this.fn = fn; // 回調函數 Dep.target = this; // 發布訂閱對象Dep,添加一個屬性target = this 也是當前watcher let val = vm; let arr = exp.split('.'); arr.forEach(function (k) { val = val[k]; // 這個步驟會循環的調用改對象的get,所以就會把該watcher添加到觀察數組中 }); Dep.target = null; } // 給每個watcher都加一個update方法,用來發布 update(){ // 通過最新 this對象,去取到最新的值,觸發watcher的回調函數,來更新node節點中的數據,以達到更新視圖的目的 let val = this.vm; let arr = this.exp.split('.'); arr.forEach(function (k) { val = val[k]; }); this.fn(val); // 這個傳入的val就是最新計算出來的值 } }
小結
代碼已經全部寫了詳細的註釋,但是可能還是會有難以理解的地方,這個時候多動手練練,使用下可能會讓你更加熟悉MVVM原理
JavaScript 實現一個簡單的MVVM前端框架(ES6語法)