從一張圖開始:
vue的整個內部實現原理都可以抽象成這樣一張圖,在這張圖中,對於抽象Virtual DOM Tree暫不進行實現,以直接操作DOM代替抽象對映的過程。(該圖來自掘金小冊,侵刪。https://juejin.im/book/5a36661851882538e2259c0f/section/5a37bbb35188257d167a4d64)
1.實現功能:
- 資料劫持
- 模板編譯(以v-model和{{}}實現為例
2.檔案目錄:
- mvvm.html(所謂檢視檔案,大家都懂)
- mvvm.js(整合資料劫持與模板編譯部分)
- compile.js(模板編譯)
- observer.js(資料劫持)
- watcher.js(處理依賴)
3.具體實現:
- Observer類:
- 實現資料劫持,為data中每一項屬性設定存取器方法。
- observer(data)遞迴遍歷data物件的每一項屬性。
- defineReactive(obj,key,value)對每一項屬性進行存取器設定,基於Object.defineProperty實現。
Object.defineProperty(obj,key,{ get(){}; set(){}; })複製程式碼
- 模板編譯,對html檔案中的'v-'指令以及{{}}進行編譯,並實現相應的資料更新updater。
- 先將html文件推入到fragment文件碎片中進行管理,此行為將清空頁面,這也就是平時用vue進行開發時會出現也頁面閃爍的原因。v-cloak即是用display:none將頁面的初次渲染去掉,避免閃爍。
- 遞迴將html節點推入fragment
let fragment=document.createDocumentFragment(); let firstChild; //首個子節點 while(firstChild=el.firstChild){//注意文字節點的坑 空格換行 #text fragment.appendChild(firstChild); //此行為將直接轉移頁面中的節點 }複製程式碼
- compile對fragment中的內容進行編譯
- 對節點進行遞迴遍歷,根據nodeType是否為1分為元素節點(可使用指令)和文位元組點({{...}}),分別採用不同的編譯方法。
- compileElement用
node.attributes
取出元素的屬性列表,對屬性進行遍歷,將屬性為‘v-’開頭的指令拿出來,尋找對應的資料更新方法CompileUtil[type](node,this.vm,expr);
。 - compileText用
node.textContent
拿出文字節點的內容,用正則/\{\{([^}]+)\}\}/g
進行匹配,是否滿足{{}}語法,若匹配則執行文字節點的資料更新方法CompileUtil['text'](node,this.vm,expr);
。 - compileUtil物件管理和執行資料更新。
- 對於文字節點:這裡我們傳入表示式時傳入的是類似{{message.a}}的形式,所以需要用正則換掉兩邊的括號
return expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{ arguments[1]即是我們需要的值});複製程式碼
- 同時從data中取出相應的值,這裡我們需要處理常見的一種情況,比如message.a.b,所以不能直接data[message]進行取值,這裡用reduce進行遍歷,取出真實的值。
expr.reduce((prev,next)=>{ return prev[next];},vm.$data)複製程式碼
- 而對於元素節點,則可以直接用reduce取出真實的值。
- 之後就是執行更新方法了,因為我們並沒有實現virtual DOM,這裡就是很簡單操作DOM改變值就行。例如更新文字時
updater && updater(node,this.getVal(vm,expr)); textUpdater(node,value){ node.textContent=value;}, 複製程式碼
3.Watcher類
- 資料監聽,觀察到資料變化時通知編譯,影響檢視,對檢視上出現的表示式進行依賴收集,在第一次初始化渲染檢視時就為每一個出現的資料確定依賴。
- 依賴收集:我們這裡的watcher指的就是檢視上的節點物件,將他加入依賴,我們這裡的做法是,在編譯的時候即在compile中new一個Watcher物件出來,然後在watcher中這樣處理:
Dep.target=this; let value=Watcher.getVal(this.vm,this.expr);//觸發get Dep.target=null;複製程式碼
- 將屬於此節點的watcher,他長這樣:
this.vm=vm; this.expr=expr; this.cb=cb; this.value=this.get();//偽造資料讀取,將Watcher物件存入訂閱者陣列中複製程式碼
- 如上面那段程式碼中,我們將Dep.target指向watcher例項,然後主動觸發資料的get方法,將Dep.target指向的watcher物件進行處理,加入到相應的資料的訂閱者陣列中。
get(){ Dep.target && dep.addSub(Dep.target); return value; }複製程式碼
- 完成依賴處理。
4.Dep類
- 就是我們所謂的釋出訂閱者。
- 他很簡單,長這樣。
class Dep{ constructor(){//訂閱的陣列 this.subs=[]; } addSub(watcher){ this.subs.push(watcher); } notify(){ this.subs.forEach(watcher=>watcher.update()); } }複製程式碼
- addSub用來將watcher加入到相應資料的和觀察者陣列中,subs及用來儲存watcher。noyify即遍歷陣列中每一個觀察者,並執行相應的更新方法update。而update方法則指向例項化Watcher物件時傳入的cb回撥函式。
update(){ this.cb();//觸發檢視更新 }複製程式碼
- cb即是相應元素對應的更新方法。
new Watcher(vm,expr,()=>{ updater && updater(node,this.getVal(vm,expr)); })複製程式碼
這樣我們的框架就完成了,寫這篇部落格初衷是為整理自己的思路,如果有錯誤或不合適還希望指出。
這裡是倉庫地址:https://github.com/sugarhaining/Book_List/tree/master/javascript/MVVM