本文首發在 前端開發博客
MVVM
是當前時代前端平常業務開發中的必備模式(相關框架如react
,vue
,angular
等), 使用 MVVM
能夠將開發者的精力更專一於業務上的邏輯,而不須要關心如何操做 dom
。雖然如今都 9012 年了,mvvm
相關原理的介紹已經爛大街了,但出於學習基礎知識的目的(使用 proxy
實現的 vue
3.0 還在開發中), 在參考了以前 vue.js
的總體思路以後,本身動手實現了一個簡易的經過 proxy
實現的 mvvm
。javascript
本項目代碼已經開源在 github,項目正在持續完善中,歡迎交流學習,喜歡請點個 star 吧!
<html> <body> <div id="app"> <div>{{title}}</div> </div> </body> </html>
import MVVM from '@fe_korey/mvvm'; new MVVM({ view: document.getElementById('app'), model: { title: 'hello mvvm!' }, mounted() { console.log('主程編譯完成,歡迎使用MVVM!'); } });
Complier
模塊實現解析、收集指令,並初始化視圖Observer
模塊實現了數據的監聽,包括添加訂閱者和通知訂閱者Parser
模塊實現解析指令,提供該指令的更新視圖的更新方法Watcher
模塊實現創建指令與數據的關聯Dep
模塊實現一個訂閱中心,負責收集,觸發數據模型各值的訂閱列表流程爲:Complier
收集編譯好指令後,根據指令不一樣選擇不一樣的Parser
,根據Parser
在Watcher
中訂閱數據的變化並更新初始視圖。Observer
監聽數據變化而後通知給 Watcher
,Watcher
再將變化結果通知給對應Parser
裏的 update
刷新函數進行視圖的刷新。html
將整個數據模型 data
傳入Observer
模塊進行數據監聽前端
this.$data = new Observer(option.model).getData();
循環遍歷整個 dom
,對每一個 dom
元素的全部指令進行掃描提取vue
function collectDir(element) { const children = element.childNodes; const childrenLen = children.length; for (let i = 0; i < childrenLen; i++) { const node = children[i]; const nodeType = node.nodeType; if (nodeType !== 1 && nodeType !== 3) { continue; } if (hasDirective(node)) { this.$queue.push(node); } if (node.hasChildNodes() && !hasLateCompileChilds(node)) { collectDir(element); } } }
對每一個指令進行編譯,選擇對應的解析器Parser
java
const parser = this.selectParsers({ node, dirName, dirValue, cs: this });
將獲得的解析器Parser
傳入Watcher
,並初始化該 dom
節點的視圖node
const watcher = new Watcher(parser); parser.update({ newVal: watcher.value });
全部指令解析完畢後,觸發 MVVM
編譯完成回調$mounted()
react
this.$mounted();
使用文檔碎片document.createDocumentFragment()
來代替真實 dom
節點片斷,待全部指令編譯完成後,再將文檔碎片追加回真實 dom
節點git
let child; const fragment = document.createDocumentFragment(); while ((child = this.$element.firstChild)) { fragment.appendChild(child); } //解析完後 this.$element.appendChild(fragment); delete $fragment;
在Complier
模塊編譯後的指令,選擇不一樣聽解析器解析,目前包括ClassParser
,DisplayParser
,ForParser
,IfParser
,StyleParser
,TextParser
,ModelParser
,OnParser
,OtherParser
等解析模塊。es6
switch (name) { case 'text': parser = new TextParser({ node, dirValue, cs }); break; case 'style': parser = new StyleParser({ node, dirValue, cs }); break; case 'class': parser = new ClassParser({ node, dirValue, cs }); break; case 'for': parser = new ForParser({ node, dirValue, cs }); break; case 'on': parser = new OnParser({ node, dirName, dirValue, cs }); break; case 'display': parser = new DisplayParser({ node, dirName, dirValue, cs }); break; case 'if': parser = new IfParser({ node, dirValue, cs }); break; case 'model': parser = new ModelParser({ node, dirValue, cs }); break; default: parser = new OtherParser({ node, dirName, dirValue, cs }); }
不一樣的解析器提供不一樣的視圖刷新函數update()
,經過update
更新dom
視圖github
//text.js function update(newVal) { this.el.textContent = _toString(newVal); }
OnParser
解析事件綁定,與數據模型中的 methods
字段對應
//詳見 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts el.addEventListener(handlerType, e => { handlerFn(scope, e); });
ForParser
解析數組
//詳見 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts
ModelParser
解析雙向綁定,目前支持input[text/password] & textarea
,input[radio]
,input[checkbox]
,select
四種狀況的雙向綁定,雙綁原理:
數據變化更新表單:跟其餘指令更新視圖同樣,經過update
方法觸發更新表單的value
function update({ newVal }) { this.model.el.value = _toString(newVal); }
表單變化更新數據:監聽表單變化事件如input
,change
,在回調裏set
數據模型
this.model.el.addEventListener('input', e => { model.watcher.set(e.target.value); });
MVVM
模型中的核心,通常經過 Object.defineProperty
的 get
,set
方法進行數據的監聽,在 get
裏添加訂閱者,set
裏通知訂閱者更新視圖。在本項目採用 Proxy
來實現數據監聽,好處有三:
Proxy
只會監聽自身的每個屬性,若是屬性是對象,則該對象不會被監聽,因此須要遞歸監聽Proxy
替代原數據對象var proxy = new Proxy(data, { get: function(target, key, receiver) { //若是知足條件則添加訂閱者 dep.addDep(curWatcher); return Reflect.get(target, key, receiver); }, set: function(target, key, value, receiver) { //若是知足條件則通知訂閱者 dep.notfiy(); return Reflect.set(target, key, value, receiver); } });
在 Complier
模塊裏對每個解析後的 Parser
進行指令與數據模型直接的綁定,並觸發 Observer
的 get
監聽,添加訂閱者(Watcher
)
this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);
Observer
的 set
監聽 -> Dep
的 notfiy
方法(通知訂閱者的全部訂閱列表) -> 執行訂閱列表全部 Watcher
的 update
方法 -> 執行對應 Parser
的 update
-> 完成更新視圖Watcher
裏的 set
方法用於設置雙向綁定值,注意訪問層級MVVM
的訂閱中心,在這裏收集數據模型的每一個屬性的訂閱列表class Dep { constructor() { this.dependList = []; } addDep() { this.dependList.push(dep); } notfiy() { this.dependList.forEach(item => { item.update(); }); } }
目前該 mvvm
項目只實現了數據綁定
和視圖更新
的功能,經過這個簡易輪子的實現,對 dom
操做,proxy
,發佈訂閱模式
等若干基礎知識都進行了再次理解,查漏補缺。同時歡迎你們一塊兒探討交流,後面會繼續完善!