疫情期間學習成果繼續輸出,若有不對的地方請指出,感激不盡!
在做vue項目的時候都會運行以下這段代碼,我只知道這是new了一個vue實例,然后初始化,編譯,掛載,卸載,如下圖:
但是vue內部都具體怎么操作的一概不知,今天學習源碼的過程中發現了終於知道了其中的奧秘。我們來一步步的解析這個過程都做了哪些事。先找到項目的入口文件。
1.首先找到項目的入口文件
我們會在啟動vue項目的時候都會執行npm run dev,那這個指令都做了哪些操作,我們可以打開packge.json文件找到這行指令
可以看到有個scripts/config.js文件,這就是這個指令執行的配置文件,我們打開config.js繼續往下找
這個是當前項目的入口地址。
可以看到這個文件主要做的是擴展$mount方法,處理template和el選項,不管傳過來的options是template還是el選項,最終都會編譯成render函數。
同時導出了一個vue
可以看到這個vue是從runtime/index文件引入的,打開這個文件
這個文件定義了$mount方法,並且由mountComponent()這個方法執行掛載,將根組件掛載到宿主元素
這個文件還不是真正定義vue的地方,這個文件導出的vue是從core/index文件引入的
打開core/index文件
這個文件定義了全局API,set、delete、nextTick等方法,同時從instance/index引入了Vue,打開這個文件
終於找到了定義vue的地方,可以看到這個這個里面有幾個核心方法及其作用,分別是
initMixin(Vue) // 實現init函數
stateMixin(Vue) // 狀態相關api $data,$props,$set,$delete,$watch
eventsMixin(Vue)// 事件相關api $on,$once,$off,$emit
lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy
renderMixin(Vue)// 渲染api _render,$nextTick
當執行new Vue的時候就是執行this._init()這個方法,進行生命周期的初始化,而init方法是由initMixin(Vue)這個方法實現的
2.項目初始化init()方法的調用
這個方法首先整合options選項,將用戶傳遞的options選項與當前構造函數的options選項及其父級構造函數的options選項合並生成一個新的options並賦值給$options。
然后再判斷如果用戶傳遞了el選項,就自動開啟模板編譯階段與掛載階段,如果沒有el選項就需要用戶手動執行vm.$mount方法進行模板編譯和掛載階段。其中ininState()會初始化事件、 props、 methods、 data、 computed 與 watch,我們着重來看一看data的初始化過程。其實就是數據響應化和依賴收集的過程。
先判斷data是否是函數,如果是函數的話就getData函數並將返回值賦給data和vm._data,daita中的數據會被保存在vm._data中。
這個方法中有一個observe方法,對data中的屬性進行遍歷,執行相應的操作,具體看一下observe方法都做了什么
這個方法返回了一個Observe實例
判斷當前數據類型
如果是object類型就就執行defineReactive()方法
defineReactive定義對象屬性的getter/setter,getter負責添加依賴,setter負責通知更新 ,這是數據響應化的處理。再來看一下依賴收集,這個方法中有一個Dep,看一下它的作用
Dep就是訂閱者,負責管理一組Watcher(觀察者),包括watcher實例的增刪及通知更新 ,用 addSub 方法可以在目前的 Dep 對象中增加一個 Watcher 的訂閱操作; 用 notify 方法通知 Dep 對象的 subs 中的所有 Watcher 對象觸發更新操作 ,下面再看Watcher的作用
watcher一創建就會執行get方法進行依賴收集
Watcher解析一個表達式並收集依賴,當數值變化時觸發回調函數,常用於$watch API和指令中。
每個組件也會有對應的Watcher,數值變化會觸發其update函數導致重新渲染
到此,初始化過程就完成了。
3. $mount執行掛載
$mount方法是在runtime/index這個文件里面定義的
我們可以看到$mount這個方法是由mountComponent()這個函數執行的
可以看到,這個函數會new一個Watcher,調用更新函數更新組件
其中updateComponent方法會做兩件事,第一是通過_render()方法將之前編譯的render function 渲染成VNode,第二是通過_updata()方法將虛擬dom轉換成真實dom顯示在頁面上。
接下來我們來分別看這兩個步驟
4.渲染函數render
打開render.js
這里面有個createElement()函數,它的核心作用是將用戶傳進來的render function轉換成虛擬dom。
其實render函數的執行結果就是createElement()的執行結果,在這里就不詳細講解其過程。
接下來執行_updata()的方法,這個過程會先執行patch方法對新舊虛擬dom進行差異對比
5.patch對比
如果沒有新的虛擬dom,表示是第一次初始化程序,就直接將這個虛擬dom作為真實dom,否則就表示要更新,這個時候需要新舊虛擬dom對比,由__patch__()這個方法實現
打開vdom/patch.js,
patch算法通過同層的樹節點進行比較而非對樹進行逐層搜索遍歷的方式,所以時間復雜度只有O(n),是一種相當 高效的算法。
同層級只做三件事:增刪改。具體規則是:new VNode不存在就刪;old VNode不存在就增;都存在就 比較類型,類型不同直接替換、類型相同執行更新;
兩個VNode類型相同,就執行更新操作,包括三種類型操作:屬性更新PROPS、文本更新TEXT、子節點更新REORDER patchVnode具體規則如下:
1. 如果新舊VNode都是靜態的,那么只需要替換elm以及componentInstance即可。
2. 新老節點均有children子節點,則對子節點進行diff操作,調用updateChildren
3. 如果老節點沒有子節點而新節點存在子節點,先清空老節點DOM的文本內容,然后為當前DOM節 點加入子節點。
4. 當新節點沒有子節點而老節點有子節點的時候,則移除該DOM節點的所有子節點。
5. 當新老節點都無子節點的時候,只是文本的替換。
至此,vue的渲染過程執行完畢
總結:new Vue() 之后的關鍵步驟
1. 執行init方法進行初始化
。進行初始化生命周期、事件、 props、 methods、 data、 computed 與 watch
。initState()方法會進行data初始化,其實就是數據響應化 和 依賴收集 的過程
在圖中表現為這一部分
2.編譯
如果是運行時編譯,即不存在 render function 但是存在 template 的情況,需要調用第一個$mount() 方法進行「 編譯」步驟,並最終生成render函數。
編譯過程如下:
。解析 - parse
解析器將模板解析為抽象語法樹AST,只有將模板解析成AST后,才能基於它做優化或者生成代碼字符串。
。優化 - optimize
優化器的作用是在AST中找出靜態子樹並打上標記。靜態子樹是在AST中永遠不變的節點,如純文本節 點。
。代碼生成 - generate
將AST轉換成渲染函數中的內容,即代碼字符串。
在圖中表現為下面部分
3、生成VNode,再將VNode轉換成真實dom
調用第二個$mount()方法,通過_render() 方法將 render funtion 生成虛擬dom,再通過_patch() 方法,進行diff算法,把虛擬dom轉換成真實dom,顯示在頁面。在這個過程中會進行響應化處理,具體過程如下:
。再將render function轉換成VNode的過程中會讀取data里面設置的屬性,所以會觸發getter方法,進行依賴收集,並將觀察者 Watcher 對象存放到訂閱者 Dep 的 subs 中。如果數據發生變化就 會觸發setter方法,通知更新視圖,進行updata方法,這個方法會執行patch進行對比差異,然后更新。
在圖中表現為這部分:
下面是真實開發調用棧的執行結果(當時截圖軟件和某一個熱鍵沖突了截不了就直接拍照了)
下面是整個代碼執行思維導圖