本篇還是以看得見的思考來分析整個 update 過程,後續還會有總結的文章。
基本的流程是:先用看的見的思考來看源碼,然後總結。
scopeId 都有啥用?
初始化邏輯搞定之後,接著就是看看更新邏輯了。
更新邏輯著可是核心中的核心,這個搞懂之後,在去給 vue 提 pr 那可就輕鬆多了。
好了, 廢話不多說了 先看源碼。
什麼時候會觸發 update 的邏輯?
首先第一個問題,我們需要考慮什麼時候會觸發 update 的邏輯。
先上應用層的代碼:
可以看這個組件,當我們點擊 button 的時候 視圖一定會刷新。
那麼在源碼裡面的流程是怎麼樣的呢?
還記得我們的 setupRenderEffect 邏輯嘛?
還是貼代碼再回顧一下吧:
重點來了,注意這裡的 effect。
我們是在 effect 裡面調用的 render 函數,
而當我們調用 render 函數的話,肯定會觸發響應式對象的 get ,這其實是關於 reactivity 的核心邏輯,如何收集依賴和如何觸發依賴的。
這裡我們暫時知道當我們調用 render 函數之後,會觸發依賴收集,收集的就是當前用 effect 包裹的這個 function,後面當我們的響應式數據變動的時候回再次調用這個 function。
比如上面使用層的代碼,點擊按鈕的時候 count 變了。
當 count 變了之後就會觸發依賴,也就調用了我們的這個 function,這也就是 update 邏輯的入口,
這個入口整明白了就和初始化的邏輯結合在一起了。
接著往下看:
現在可以直接掛住 else 裡面的邏輯了。
我先整理一下代碼:
這裡有個疑惑點是 Instance.next 是個什麼鬼,
先去查查注釋:
好吧,看著注釋也沒有太看明白。
先過,回頭再來看。
不過猜測一下的話,第一次 update 的時候這個 next 應該是個 null ,
那麼我先把涉及到處理 next 的邏輯先去掉。
咦,我發現在這裡給 next 賦值了。next 等於當前的 vnode 。
調用 renderComponentRoot
然後調用:
renderComponentRoot(instance),
簡化邏輯:
我們看看到底都做了啥:
再次調用 render 函數得到新的 vnode ,這裡命名為 result;
繼承之前 vnode 的父級 scopeId ?這裡的 scopeId 都有啥用呢?(記錄一下);
繼承 directives;
繼承 transition data;
繼承 ref。
稍微分析分析,其實呢,這裡就是再次調用 render 函數,然後返回出去,別的雜七雜八的事先不管。
再次回到 setupRenderEffect 繼續往下看:
這也是個關鍵邏輯,做數據的更替了:
把之前的 vnode 賦值給 prevTree,把現在的 vnode 賦值給 instance.subTree,
接著還需要更新一下 el(實際渲染出來的 element),
接著調用一些 hook:
beforeUpdate hook;
onVnodeBeforeUpdate.
接著就是重點啦,再次調用 patch:
只不過和我們初始化的時候對比,現在的 n1 是有值的了。
我們先把後面的邏輯看完,然後在看是如何在 patch 裡面對比兩個節點的:
因為 patch 完了之後有可能會生成一個新的 el ,所以需要把新的 el 賦值給新的 vnode 上:
這裡還是 hook 的調用:
updated hook;
onVnodeUpdated.
好,接著我們就進入重頭戲。
updateComponent
因為會再次調用 patch ,然後會進行 component 類型的處理,這裡當然是調用 updateComponent 啦,所以我們直接看著邏輯。
簡化邏輯:
這裡有幾個比較重要的邏輯函數:
shouldUpdateComponent 判斷到底需不需要更新;
如果需要更新的話,
調用 updateComponentPreRender ,
或者
invalidateJob 和 instance.update();
不需要更新的話直接把之前的屬性拿過來即可。
我們這裡主要分析的是 happy path ,所以只會執行到 else 裡面的邏輯,也就是調用 instance.update()。
而調用 update 的話,就會再次執行一遍 setupRenderEffect。
我們等等再來看,
先看下 shouldUpdateComponent.
shouldUpdateComponent
其實這個函數的回答的問題是,什麼情況下需要更新組件呢?
先簡化一下代碼:
逐個來分析的話:
如果有 dirs 和 transition 的話,會更新;
接著是判斷了 patchFlag ,這個 flag 也是個很值得一說的點,它是在編譯階段生成的,不同的模板類型會生成不一樣的值,這個可以單獨寫個專題來分析,暫時我們先知道有這個 flag 即可。
大概有這麼幾種情況都需要更新:
PatchFlags.DYNAMIC_SLOTS;
PatchFlags.FULL_PROPS
這種情況的話還會對比一下之前的 props 和現在的 props 有啥不一樣的,發現只要有一個 prop 不一樣就會更新;
PatchFlags.PROPS 這種情況是檢測動態的 props ,這裡主要要關注的邏輯點是 nextVNode.dynamicProps 是什麼時候給賦值的;
接著就是檢測如果有 chilren 的話,那麼也需要更新。以上分析的暫時也只是個簡單的分析,具體的情況到時候在具體的分析,在回顧一下我們的目標,是了解 update 的流程,暫時先不太關注於細節。
好,最後的結論是對比一下,發現需要更新的話 就返回 true。
這個還思考了一個問題,就是為什麼不需要對比 chilren 呢?
應該是因為 component 就是個虛擬的箱子,假如箱子的表面行為都有變動,那麼再繼續深入,下面要關注的點就是如何觸發 updateElement 的。
接下來的邏輯應該是進入了這個分支:
我們在重新讀一下 update ,這裡和第一次進入 update 時有一點不同,就是 vnode.next 是有值的。(我們在第一次執行 update 的時候給 vnode 賦值的,還記得嗎)
哈哈,暫時發現一個有趣的點,默認的代碼第一次讀的時候感覺好難好複雜,但是你多看它幾遍的話,也就那麼回事,所以以後看到複雜的代碼你就多看它幾遍哈哈。
因為現在 next 是有值的,所以應該會進入到 updateComponentPreRender 函數內。
updateComponentPreRender
簡化邏輯:
就是更新了 props 和 slots ,這裡細節咱就先不看了。
再繼續往下看,我們暫時的問題是怎麼更新到 component 內部的 element 的呢?
啊哦,並沒有發現多餘的線索。
看來我需要找個例子來 debug 一下。
這裡我選擇的策略是先從最簡單的邏輯看起來:
這種情況是當前的這個 div (element) 的 id 是動態的,然後 8 代表的是 PatchFlags.PROPS 在後面的數組["id"] 裡面是標記著動態的 prop 是什麼,這裡當然就是 id 了。
有一點要說明的是,我們這裡是直接寫死的,如果是利用 template 來寫的話,它會自動生成。
好,按照這個思路 我看看它是如何處理 component 類型的。
在這種情況下,Parent 裡面是嵌套了一個 Child 組件,然後我們是在 parent 的 render 函數內傳給 child 的 props ,也就是說當 cId 變化後,它應該是先影響到 Child 。
接著我們來分析一下它整個 runtime 的所有邏輯流程:
cId.value ++ 的時候觸發 Parent 的 update 邏輯;
然後再次調用 Parent 的 render 函數,獲取到 subTree;
接著會觸發 patch ,著時候的參數就是新得到的 subTree ,也就是 createVNode(Child);
因為這個 vnode 是 Child ,類型是 component 所以會走 processComponent 邏輯;
因為 n1 是肯定有值的,所以走 updateComponent 邏輯;
在接下來會觸發
shouldUpdateComponent 邏輯,比對兩個 vnode ,看看是否需要更新,這裡是肯定需要更新的;
然後又觸發了 instance.update(),注意一下這裡的 instance 可是 Child;
好,我們又一次來到了 instance.update 內,這時候會再次調用 Child.render(),也可以說現在拆箱 Child;
拆箱 Child 得到的 vnode 就是 element p 了;
接著用 p 的 vnode 來調用 patch;
會調用到 patchElement ,繼而對比 element。
至此,對於上面的問題,從 component 是如何調用更新到內部的 element 的,就有了答案。
當然我們到現在為止只是分析了兩個最簡單的更新:組件的更新、element 的更新。
接著把整個流程整理一下:
修改響應式的值,觸發 effect 的回調函數(觸發依賴);
再次調用 render 函數,獲取最新的 vnode 值;
把新的 vnode 和舊的 vnode ,交給 patch;
patch 來基於 vnode 的類型進行處理具體的 update 邏輯;
如果是 component 類型的話,會做一個 updateComponent() 的處理,檢測是否可更新,如果可以更新的話會再次調用 update;
如果是 element 類型的話,會調用 patchElement 來檢測更新;
接著就是遞歸的調用當前組件的 render,獲取到最新的 subTree(vnode);
重複上面的過程。
我們稍微隱喻一下,如果是 component 類型的話,我們就需要檢測要不要開箱,
當需要開箱的話,再處理箱子裡面的 element 或者 component ,
如果是 component 那麼就重複上面的過程。應該是遞歸的向下查,截止點就是當前的 component 能不能開箱。
好,這個流程整理完了,怎麼對比細節,我們先不管,先把整個流程在 mini-vue 裡面實現一遍,看看有沒有邏輯落下。
至此整個 update 的流程就都已經分析完了,剩下的就是針對細節來分析了。
後面的策略是基於特定的場景來分析對應的 patch 邏輯,不然的話,邏輯太多,容易在細節中迷路。