1. 程式人生 > >手寫實現vue的MVVM響應式原理

手寫實現vue的MVVM響應式原理

  MVVM響應式實現原理:

  1.模板編譯

  2.資料劫持

  3.watcher

  MVVM------------------檢視-----模型----檢視模型

  三者與Vue的對應:view對應template,vm對應newVue({…}),model對應data

  文中應用到的資料名詞:

  nodeType判斷節點是否是元素節點

  querySelector建立一個元素節點

  createDocumentFragment文件碎片

  attributes獲取元素屬性集合

  textContent獲取文字內容

  reduce(prev,next,currentIndex){}一個可以用上一個元素和當前元素做處理的方法

  defineProperty(obj,key,value){}資料攔截的主要方法

 

  首先建立一個vue的例項,建立mvvm.js,構建mvvm類。獲取el的節點和data放入例項中,在將Observer.js(資料劫持)和Compile.js(模板編譯)放入mvvm的js,全部在index頁面執行.

  第一步:模板編譯

  我首先製作Compile.js,也就是模板編譯。

 

首先需要獲取el 這個屬性的值   用nodeType === 1判斷是不是元素節點. 如果不是則用 queryselector() 生成一個節點 。  這樣做的目的是,有些人el:#app  有些人是document.getElementById('app')。 不管倆者如何,我們都要生成一個節點來供後續使用。

 隨後判斷el節點是否存在,如果存在。則進行編譯 ,  這裡編譯最好不要在dom裡進行遍歷編譯,非常耗效能 。 我推薦的是用 createDocumentFragment() 方法. 建立一個虛擬節點物件, 在這個虛擬節點物件裡進行遍歷以及對應的操作。

 

那麼說到虛擬節點, 我們需要將我們獲取的el節點整個放入進去 ,進行遍歷,將app裡的每一個子節點都搬到fragement 變數中。

 然後進行節點的編譯。這裡的節點又分為元素節點和文字節點。 還是用剛剛的nodeType判斷區分嗎,然後做對應的操作。

接下來我們先編譯元素節點首先我們需要知道,獲取元素節點要做什麼,為什麼獲取元素節點。我是希望通過獲取元素節點上的關於vue的指令,比如:v-model,v-html,v-for。等等...那麼這些指令是放在元素節點上的屬性裡,所以我們用attributes獲取元素節點的屬性名的集合,也就是我們說的v-model。通過遍歷這個attr屬性名的集合,獲取每個屬性名。通過isDirective函式判斷attrName包含v-的屬性,這裡我做給假設,好方便理解。這裡通過上面的過濾,可以得出attrName是一個指令名的集合。那我假設這個指令名為v-model。我首先獲取v-model的值,也就是expr。然後做一個解耦物件CompileUtil,方便後面製作其他的指令。所以這裡呼叫的是CompileUtil[model]{node,this.v,,expr};

 

  呼叫model的指令後,在model這個函式裡做相對應的處理。這裡的watcher建構函式先不用管,後面的事情。這裡的uptate['modelUptate']和model一樣放在CompileUtil中,方便管理。如果updateFn存在的話,則執行updateFn(),將v-model的值賦予input節點的value.下圖中的getVal是防止v-model=’messge.a'這種巢狀物件的。這個函式裡,首先利用split將messge.a拆分成[messge,a]陣列。然後利用reduce方法返回上一個元素[當前元素],而最下面的vm.$data是reduce方法遍歷的初始值。也就是data。
  因為data:{messge:{a:'hello.world'}}.這樣的編譯,元素節點就可以編譯出來了,可以將data的值編譯到元素節點上了。

 

        接下來編譯文字節點,那文字節點,我們首先獲取文字節點裡的值,然後利用正則的test找{{a}},和之前的元素節點一樣,執行對應的函式。,執行對應的行數。這裡第86-90可以先不管,不過這裡的textVal和上面的getVal函式不一樣,首先是需要將符合條件的元素裡的變數取出來也就是{{a}}裡的a,argments[1]就是a變數。在考慮到物件巢狀,就執行上面的getVal。然後就可以將data裡的值替換到文本里了。

 這樣元素節點和文字節點都編譯完成了。然後將整個虛擬節點丟回dom樹裡去 。MVVM的編譯就結束了

 第二步:資料劫持,函式很少。但比較繞.這裡執行observe,利用遞迴遍歷,將data裡的鍵值對全部拿出來處理,執行defineReactive函式,這裡18行可以先不看。 看下面的最重點的Object.defineProperty()。這裡要傳入劫持的物件,劫持的鍵,以及回撥函式。這裡回撥函式裡倆個引數在下圖。

然後,get函式是取值是做對應的操作,set函式是設定值做對應的操作。至此資料劫持就完成了

 

 

 第三步:watcher 監察者 ,一旦變化執行對應的操作。也就是將模板編譯和資料劫持倆個函式聯絡在一起。有銜接。

這裡建立watcher類,將需要的引數獲取。 vm是例項,expr是值,cb是回撥函式callback。watcher例項裡的value = get方法的返回值,value執行一次巢狀處理返回。這裡監察者作用主要是 一 更新值,二是執行callback回撥函式cb。三將自己的例項,放入dep的target裡。那麼watcher監察者就製作好了。

最後的連線部分,首先data裡的每個屬性值都被加上了set和get

1.獲取值

在最開始編譯的時候,編譯節點的文字節點處理和元素節點處理的時候執行watcher函式,在watcher函式裡的get函式中將 watcher函式自己放入de問值的時候,則會執行get函式,將 每個watcher放入dep陣列中 。

2.修改值

 在修改值的時候,會觸發Observer.js 的defineProperty的set函式,set函式裡比較新的值和舊的值,value是編譯時候的值,newValue是set函式的第一個引數,也就是修改後的新值 。   將倆者比較,如果不同,就執行Dep建構函式的notify函式。notify則會遍歷全部存在的dep數組裡的watcher的update方法。在watcher的update方法中,比較值的不同,如果不同就則執行回撥函式,將檢視更新。這個回撥函式是巢狀在處理文字節點和元素節點的方法裡。

 

v-model的雙向繫結

  至於v-model的雙向繫結,其實是繫結輸入框的輸入事件。將輸入事件新的值賦值給input節點的value值,然後值的改變,執行set函式,將檢視改變。檢視的改變,會執行wacther的回撥函式,文字節點也會重新賦值。

 

 

最終效果:

 

 

 

 

 

 

 

 

 

這就是mvvm響應式原理的實現,如果有殘缺講不清楚的地方,歡迎指出。謝謝。