再次看了上次寫的博客關于Vue的MVVM,發現雖然介紹了MVVM的原理,但是感覺還不夠詳細,現在就再次根據這篇博客寫詳細一點,來看看new Vue
的時候Vue究竟做了些什么事。
我想,以需求作為出發點來理解原理會比較容易,所以這篇博客會以提出需求 -> 解決需求的方式來寫。
Vue中的MVVM原理介紹
可以先閱讀我的這篇博客了解一下關于Vue的MVVM,另外需要記住這一幅圖(很重要),這張圖就是本篇博客的概括:
回顧
繼上一篇文章Vue中的MVVM--model -> view的綁定,我們完成了對頁面的初始化渲染,達成了如下要求:
但還未完成
view -> model
的綁定,所以不能通過修改數據來觸發視圖的更新,今天就來完成剩余部分
提出需求
繼上圖:
我們需要做到的是:當修改輸入框的數據時,上面的文字也隨之進行刷新。
分析
先來看看還未完成的部分:
其中包含觀察者
Observer
,監聽器Watcher
,然后還有一個Dep
,過程是:
- 在
Compiler
中監聽數據的變化并綁定監聽器; - 在觀察者
Observer
中實現對所有數據的getter
和setter
; - 監聽器
Watcher
把更新事件添加進Dep
的事件隊列中; - 觀察者
Observer
發現數據產生變化的時候通知Dep
; -
Dep
把事件隊列中的更新事件全部執行一遍;
總結下來就是實現兩個事情:
- 添加數據依賴;
- 觸發數據變更事件;
接下來就先創建Watcher
,Observer
,Dep
三個類;
image.png
image.png
image.png
先來看看Dep
是什么
根據上面的的步驟描述,很容易感覺到Dep
像是一個容器,存儲著對視圖的更新事件,是的,這是一個發布訂閱模式的實現,該模式包含事件隊列subs
,添加事件方法addSub
,執行事件隊列函數notify
,移除事件隊列里的事件removeSub
看完發布訂閱模式后,繼續我們的流程。
添加依賴
-
在Compiler中監聽數據的變化并綁定監聽器
在上一篇對model -> view
的綁定中我們有一個針對數據和指令統一進行綁定的方法bind
,為了不和后面的v-bind
指令沖突,現在改為了bindData
;
image.png
在這個函數承載的功能有獲取tag文本和執行視圖的更新,所以我們可以在這個函數中添加對數據的監聽器Watcher
,因為后面需要把更新視圖的事件添加進Dep
中,所以Watcher
中需要的參數要有一個更新事件也就是更新器updater
中的視圖更新函數,此外將當前vm實例和得到的data
鍵值也傳進去備用;
compile中添加數據監聽器
更新器
watcher
接著 -
在觀察者Observer中實現對所有數據的getter和setter
注意這一步需要考慮到數據中含有嵌套的對象,需要進行遞歸操作才能全部添加getter
和setter
,使用的是Object.definedProperty
,Observer
接受的參數是data
對象:
image.png
然后在MVVM
類中代入data
并執行observer
:
image.png
接著對所有data
中的屬性綁定getter
和setter
,這一步需要進行遞歸操作:
image.png
最后回到Compiler
中,實現視圖對數據的修改:
比如在輸入框中修改數據直接反應到data
上
image.png
image.png
來看看成果:
image.png
這個時候在視圖上對數據進行的修改就可以反映到data
上,并觸發該數據的setter
函數; -
監聽器
Watcher
把更新事件添加進Dep
的事件隊列中;
這一步需要考慮一個問題:在什么時候怎么樣把更新事件添加到Dep
中去?
回顧上面所寫的,data
中的每一個屬性都有一個對應的Watcher
,可以在Watcher
中獲取得到對應的data
中的屬性。那么在這個獲取的過程中,又會觸發該屬性的getter
,就可以考慮在該屬性的getter
中添加,分解成一下步驟就是:
① 把這個Watcher
通過構造函數本身的屬性target保留在Dep
中,然后去data
中取值;
image.png
② 取值的時候觸發Observer
中該屬性的getter
,在Observer
中new一個Dep
實例出來,判斷如果Dep類的target
非空(也就是該屬性已被有監聽器),則觸發依賴添加事件depend
;
image.png
③ 這時候的Dep.target
就是被監聽屬性的Watcher
,在Dep類中添加一個方法depend
,用來把該屬性的Watcher
添加進事件隊列subs
中,但是這一步要當前的Watcher
,需要在Watcher類中進行觸發,所以在Watcher
中創建一個函數addDep
,把Dep
的實例作為參數放進去,然后在addDep
中進行更新事件的添加:
image.png
image.png
然后置空Dep.target
,用于下一個數據的依賴添加
image.png
現在我們來看看subs
中有些什么
image.png
可見msg
被引用了兩次就被監聽了兩次,這時候只要當msg
這個數據發生變化并觸發setter
時,將subs
中所有的watcher
實例里的更新回調update
拉出來執行即可 更新視圖
- 更新視圖的時候,我們先要獲取當前的數據新值,然后作為參數放進回調函數中,并且還要對新的數據進行上面的依賴添加步驟,那么
Watcher
還需要一個update
函數用來統一做這個事:
image.png - 在
Observer
的setter
中觸發Dep
的notify
方法,進行視圖的更新:
image.png -
到了這步其實就已經達成效果了:
image.png
- 修復bug
雖然MVVM雙向綁定的功能已經達成,但是還是有不少bug的,其中最嚴重的有兩個
-
當我們多次更新數據的時候,會發現添加進
subs
的watcher
發生了遞增的現象,所以當快速更新數據時就會導致執行函數過多而頁面崩潰;
image.png
造成這個現象的原因是在進行第一次的更新時,watcher
將同一個數據的新值也進行了依賴添加,也就是let newVal = this.get()
這一段;
image.png
既然知道了原因,那么解決起來也很簡單,給每一個被監聽的對象都添加一個id即可。
因為添加sub的操作是在Watcher
中進行的,所以在Watcher
中創建一個對象depIds
image.png
然后給每一個Dep
都添加一個不同的id
image.png
最后在Watcher
中判斷depIds
是否已經有這個id的Dep實例存在,如果沒有則添加進去并執行addSub
,否則不執行:
image.png
效果,無論怎么修改,都只會有固定數量的Watcher
存在:
image.png -
當修改數據為對象的時候,這個對象沒有進行監聽,這個也好解決,只要在
setter
中進行判斷即可,若為對象則針對該對象重新進行監聽
image.png
總結
到這里為止,我們就完成了view -< model
的綁定,并且知道在new Vue
的時候大致做了一些什么事了,剩下的就是逐步完善,例如對更多指令的支持,對methods
以及computed
和watch
的支持。