從零到一實現類vue2.*的簡單MVVM框架

海寧不想說話發表於2019-02-02

從一張圖開始:


從零到一實現類vue2.*的簡單MVVM框架

vue的整個內部實現原理都可以抽象成這樣一張圖,在這張圖中,對於抽象Virtual DOM Tree暫不進行實現,以直接操作DOM代替抽象對映的過程。(該圖來自掘金小冊,侵刪。https://juejin.im/book/5a36661851882538e2259c0f/section/5a37bbb35188257d167a4d64)

1.實現功能:

  1. 資料劫持
  • 模板編譯(以v-model和{{}}實現為例

2.檔案目錄:

  1. mvvm.html(所謂檢視檔案,大家都懂)
  2. mvvm.js(整合資料劫持與模板編譯部分)
  3. compile.js(模板編譯)
  4. observer.js(資料劫持)
  5. watcher.js(處理依賴)

3.具體實現:

  1.  Observer類:
  • 實現資料劫持,為data中每一項屬性設定存取器方法。
  • observer(data)遞迴遍歷data物件的每一項屬性。
  • defineReactive(obj,key,value)對每一項屬性進行存取器設定,基於Object.defineProperty實現。
  • Object.defineProperty(obj,key,{
        get(){};
        set(){};
    })複製程式碼
  • Compile類:
    • 模板編譯,對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


    相關文章