<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          看透 Vue3 重頭戲之 diff 算法

          共 6390字,需瀏覽 13分鐘

           ·

          2022-04-14 16:19

          點擊上方?前端Q,關注公眾號

          回復加群,加入前端Q技術交流群

          前言

          終于迎來了DOM diff流程的重頭戲:diff算法,前面的流程只能算是附加項,重要的是各種節(jié)點是如何進行對比,然后進行更新。下面就對每一種節(jié)點的對比流程進行分析。

          image.png

          vue3.2 初始化的時候做了什么?[1]文章的的末尾,提到了傳入effect的回調函數(shù)和響應式數(shù)據(jù)之前產(chǎn)生一個依賴關系,等同于產(chǎn)生了一個watcher。當數(shù)據(jù)發(fā)生變化的時候,會以參數(shù)二的方法執(zhí)行參數(shù)一,具體細節(jié)和調度器有關,以后再說,最終會進入componentUpdateFn函數(shù)中,我們就直接進入到更新階段的componentUpdateFn

          patch之前的處理

          image.png

          在開始執(zhí)行patch函數(shù)之前,會先執(zhí)行一些生命周期鉤子函數(shù),有beforeUpdateVNodehook:beforeUpdate

          image.png
          image.png

          最主要的一點,如果是父組件數(shù)據(jù)變化而導致的子組件更新,會多執(zhí)行一個東西,里面會進行更新propsslots以及換成新的VNode,做完這些之后可能會導致更新,需要在patch之前把它們執(zhí)行。(PS:更新propsslots流程可以看看我前面的文章《Vue3.2 vDOM diff 流程之一:插槽的初始化和更新》[2]《Vue3.2 vDOM diff流程分析之一:props和attrs的初始化和更新》[3])

          image.png

          做完這一些,就可以產(chǎn)生新的VNode,將新舊VNode傳入patch開始進行對比,SuspenseTeleportdiff已經(jīng)在前面的文章中說明,這里就不在提及。

          對比元素類型節(jié)點

          image.png
          image.png

          對比元素進入processElement,這次是進入更新流程,執(zhí)行patchElement。n1是舊VNode,n2是新VNode

          image.png

          函數(shù)開頭,需要重新通過舊節(jié)點的patchFlag重新確認新節(jié)點patchFlag,因為用戶可以克隆由complie產(chǎn)生的VNode,或許可能添加一些新的props,比如cloneVNode(vnode, {class: 'cloneVNode'}),它將選擇FULL_PROPS

          image.png

          緊接著執(zhí)行新的VNode自定義指令的beforeUpdate生命周期函數(shù),如果在dev模式下且HMR正在更新,則放棄優(yōu)化且把dynamicChildren清空,使用全量diff。這會影響后面diff,但是prod模式下一般都是優(yōu)化模式,使用areChildrenSVG是判斷新VNode是不是SVG。

          image.png

          這里分為優(yōu)化模式和非優(yōu)化模式,這里進入優(yōu)化模式的條件是dynamicChildren不為空,非優(yōu)化模式是optimizedtrue,但是這兩個是互斥的,一個存在另一個肯定不存在。

          優(yōu)化模式下進行diff

          image.png

          進入到這個函數(shù),他會遍歷新VNodedynamicChildren,并從舊的VNodedynamicChildren取出按索引順序一致的節(jié)點進行對比。

          在這之前,先要找到parent node,也就這一大坨的三元運算符,不要慌張,逐個逐個條件分析,oldVNode.el是為了在異步組件的情況下確保元素節(jié)點的真實DOM要存在。

          oldVNode.el存在的情況下,并且符合以下三個條件中的其中一個:1. oldVNode的節(jié)點類型是Fragment、2.oldVNodenewVNode不是同一種元素(key值不一樣也算)、3.oldVNode是組件,就組件而言,它可以包含任何東西。container就是oldVNode.elparent。不然在其他的情況下,實際上沒有父容器,因此傳遞一個block元素,避免parentNode,就是傳遞fallbackContainer(是n2的真實DOM),

          確認好container就和oldVNodenewVNode再次傳遞給patch,接下來就要根據(jù)newVNode的節(jié)點類型從而確定走哪個分支進行diff

          image.png

          diff流程結束之后還需要做一件事,在dev模式下,如果parentComponent存在并且parentComponent啟用 HMR,需要遞歸尋找或者是定位舊的el 以便在更新節(jié)點進行引用 防止更新階段會拋出el is null。優(yōu)化模式分析完畢。

          非優(yōu)化模式下進行全量diff

          image.png

          非優(yōu)化模式下交給patchChildren處理,在diff之前先要拿到一些東西:n1、n2的Children和n2的shapeFlag。接下來的流程分為很多種情況,一一分析。

          快速diff

          首先根據(jù)n2的patchFlag判斷能不能快速更新,也就是“靶向更新”,進入之后又分為兩種情況,是否鍵控(是否綁定了key),鍵控可以是完全鍵控也可以是混合鍵控(一部分帶key,一部分不帶key),分別交給patchKeyedChildrenpatchUnKeyedChildren處理。

          不帶有key的對比
          image.png

          由于帶有key的對比有點復雜,我放的后面說,這里先看沒有帶key。沒有帶key的對比簡單粗暴,因為不確保n1和n2都有children列表,沒有就默認給一個空數(shù)組。需要注意這里獲取長度,從新舊children列表兩個列表長度中取出長度的最小的作為基準,接下來的對比最多只會對比到這個位置。具體用圖解釋。

          image.png

          如圖所示,舊children列表長度是5,新children列表長度是3,取小的也就是3,代表在循環(huán)一對一對比中只會對比前三個,剩下會交給下面的流程。

          image.png

          剩下流程分為兩種情況,在循環(huán)對比后,如果是新children列表比舊children列表長度長說明有新節(jié)點,就會去掛載新節(jié)點,反之說明有不需要的舊節(jié)點,就會去卸載。流程結束。

          key的對比

          回到patchChildren中,我們看帶有key是如何對比,將會結合圖一步步分析。

          image.png

          這里先拿到一些東西,l2是新children列表的長度,e1是舊children列表中最后一位的索引,e2是新children列表最后一位的索引。i這里有特殊意義,代表對比的開始索引。帶有key的對比主要有五個流程,

          假如有如下新舊children列表,可以準確看出只有2移動了位置,下面就看經(jīng)過五個流程是如何進行對比的。

          image.png

          1.流程一:對比開始位置

          image.png

          在這一階段會遍歷新舊children列表,只有新舊節(jié)點是用一種元素才會交給patch函數(shù)對比,每過一對新舊子節(jié)點,i就會加一,如果有一方遍歷到最后一個就會結束或者是遍歷到兩個是不同元素。例子中,前面沒有相同的節(jié)點,所以不會有任何操作

          image.png

          2.流程二:對比末尾位置

          image.png

          在這一階段一樣會遍歷新舊children列表,和階段一一樣,新舊節(jié)點是同一種元素才會交給patch函數(shù)對比,不同的是從末尾開始對比子節(jié)點,每過一對子節(jié)點,新舊最大位置索引同時會減一。例子中,從末尾的3、4、5是相同元素可以排除。

          image.png

          走完前面的兩個,說明新舊children列表中首尾的相同節(jié)點已經(jīng)被處理了,就剩下中間的部分,接下來的三個流程是掛載列表中的新節(jié)點和卸載不需要的舊節(jié)點以及無序對比。

          但這三個流程中只會執(zhí)行其中一個或者都不執(zhí)行,總共有三種情況:1. 只需要安裝新節(jié)點、 2. 只需要卸載舊節(jié)點、 3. 無序。這和前面的講到的全量diff和像,這就要看i了,如果i大于e1并且小于或者等于e2說明有新節(jié)點,執(zhí)行流程三,如果i是大于e2說明有不需要的舊節(jié)點,執(zhí)行流程四。都不符合執(zhí)行流程五

          3.流程三:掛載新節(jié)點(此流程不一定執(zhí)行)

          image.png

          nextPos是用來確定新增節(jié)點的位置,一般到了這一階段e2是沒有處理的新節(jié)點列表的最大索引,要加一是因為vue新增節(jié)點的方式了,vue新增元素是通過insert,實現(xiàn)原理是insertBefore,所以這里會拿到將要插入元素的位置的后一個。具體看下面的示意圖。(ps:紅色框內是被處理過的)

          image.png

          在這個案例中,6是新增的節(jié)點,因為經(jīng)過了流程一和二的處理,i變成了5,e2變成5,e2正好是節(jié)點6的索引,如果我們需要把它插入列表中,我們需要知道他的后一個節(jié)點是誰,以便做為瞄點,這就要加一后去新children列表中找。

          image.png

          但是還有第二種情況,如果新增的節(jié)點是新children列表中的最后一個,那么加一就會超出其長度,那么就會把parentAnchor作為瞄點,parentAnchor是當前列表的父容器中的最后一個節(jié)點,一般都是空字符串,(注意:這里是節(jié)點,不是元素節(jié)點)。例子中不符合,不會執(zhí)行該流程

          4.流程四:卸載不需要的舊節(jié)點(此流程不一定執(zhí)行)

          image.png

          卸載舊節(jié)點的操作就比較簡單了,每卸載一個i就加一,通過unmount方法進行卸載,實現(xiàn)原理是通過找到要卸載的節(jié)點的父節(jié)點,調用removeChildren進行卸載。前提是i大于e2但小于等于e1。例子中不符合,不會執(zhí)行該流程

          5.流程五:無序對比(此流程不一定執(zhí)行)

          如果到了流程五,說明children列表中有一部分是無序的,前面的流程無法處理,需要進行無序對比。這流程五分為三部分。

          這第一部分是為了產(chǎn)生index和新children列表中的key的映射圖,它會拿i作為新舊children列表的開始索引,當找到newChildren,準確來說是找到newChild身上的key,就會連同i一起保存進keyToNewIndexMap中。

          code.png

          這第二部分是循環(huán)舊節(jié)點列表 以匹配需要更新的節(jié)點和刪除不需要的節(jié)點,先提前創(chuàng)建一個數(shù)組(newIndexToOldIndexMap),長度是還需要進行對比(toBePatched)的數(shù)量,作為新舊索引對應的存放(默認全部都是0)

          開始循環(huán)舊children列表,當patched大于toBePatched時就都是卸載節(jié)點,但是一開始patched是0并不會大于,繼續(xù)往下走,開始找newIndex,先從在前面保存的key:index的映射圖中找,沒找到就嘗試在舊children列表中定位同一種類型沒有key的節(jié)點的索引。還是沒有就只能undefined

          最后,如果newIndexundefined,說明舊節(jié)點沒有對應的新節(jié)點直接卸載,不然,會修改newIndexToOldIndexMap中對應索引位置,如果newIndex小于新節(jié)點最大位置(maxNewIndexSoFar),說明這個節(jié)點移動了,不然maxNewIndexSoFar就賦值成newIndex。過了這么多,終于可以傳遞給patch進行對比,patched也會加一。

          這最后一部分,主要是為了移動節(jié)點和新增節(jié)點,如果有需要移動節(jié)點它會先根據(jù)新舊節(jié)點索引的映射產(chǎn)生一個最長遞增子序列。而從最后開始循環(huán)也便于我們可以使用最后一個修補的節(jié)點作為瞄點,找出新節(jié)點中的最長遞增子序列,移動不在這個范圍內的節(jié)點,如果映射的oldIndex是0說明是新增節(jié)點,需要進行掛載。在例子中,就會移動1。

          image.png

          這流程五是最復雜的,其中不僅包含了掛載和卸載,還包含了移動節(jié)點,提高了對節(jié)點利用,到此patchKeyedChildren流程結束。

          其他情況

          回到patchChildren中,繼續(xù)看patchFlag不存在如何進行對比,這要根據(jù)新舊節(jié)點的情況進行更新

          code.png

          看起來復雜其實很簡單,先說如果新節(jié)點是TEXT_CHILDREN,如果舊節(jié)點是ARRAY_CHILDREN,會先卸載所有舊節(jié)點,再掛載新節(jié)點,舊節(jié)點也是TEXT_CHILDREN需要和新節(jié)點對比確認不同后再更新。

          如果兩個都是ARRAY_CHILDREN,需要走patchKeyedChldren,但也有可能只是卸掉舊的并沒有新節(jié)點,卸載所有舊節(jié)點。

          當舊節(jié)點是TEXT_CHILDREN新節(jié)點是ARRAY_CHILDREN時,會先將其變?yōu)榭兆址龠M行掛載新節(jié)點。

          后面對比props的部分,在我之前的文章Vue3.2 vDOM diff流程分析之一:props和attrs的初始化和更新[4]中講過,感興趣可以去看看,到這里對比元素的流程結束。

          對比組件類型節(jié)點

          image.png

          patch函數(shù)中,對比組件分支執(zhí)行的是processComponent,最終會執(zhí)行updateComponent,組件更新新的會繼承舊的實例。

          更新前他會執(zhí)行shouldUpdateComponent判斷是否需要更新。但是屬實是情況太多,這里就不一一列舉了,具體可以到源碼中查看`shouldUpdateComponent`[5]函數(shù)。

          image.png

          進入需要更新的流程,他是會優(yōu)先處理Suspense(存在asyncDepasyncResolved不存在),不是Suspense就正常更新,把新VNode(instance.next)賦值成n2,如果當前組件已經(jīng)在更新隊列中,請將它移除,避免重復更新同一組件,然后就可以調用實例上的更新器進行更新了。

          image.png

          注意這里的instance.next,如果這個存在,在調用componentUpdateFn中會調用updateComponentPreRender函數(shù),這是因為組件數(shù)據(jù)變化導致其子組件更新,所以需要去更新實例中的VNode以及propsslots,順帶把更新props導致的更新執(zhí)行了。如果只是單純的數(shù)據(jù)變化,沒有影響到子組件,那next就會是原本實例上的VNode

          后面的就是正常調用生命周期函數(shù)和鉤子函數(shù),產(chǎn)生新的VNode和舊的VNode一起交給patch進行對比,后面的就要看組件里面是啥東西然后走哪個流程。

          對比文本類型、注釋類型、靜態(tài)節(jié)點類型節(jié)點

          • 文本類型
          image.png

          文本類型節(jié)點的更新在processText中,會先進行對比,不同才會更新文本

          • 注釋類型
          image.png

          注釋節(jié)點的更新在processCommentNode中,但是因為不支持動態(tài)更新注釋,所以是直接拿以前的。

          • 靜態(tài)節(jié)點類型
          image.png

          靜態(tài)節(jié)點的更新執(zhí)行的是patchStaticNode,因為vue會把靜態(tài)節(jié)點進行序列化成字符串所以可以直接進行字符串對比,相同只會賦值以前的elanchor,不同會先循環(huán)移除舊的,連帶著anchor一起移除,再掛載新的靜態(tài)節(jié)點。

          總結

          本篇文章分析了vue中diff算法的處理,清楚vue中diff算法的處理流程,知道每一個節(jié)點對比如何進行,如何書寫模板可以進行最優(yōu)的對比、復用節(jié)點,從而提高性能,在列表對比中,優(yōu)化模式只會對比dynmaicChildren中的節(jié)點,也就是動態(tài)節(jié)點,非優(yōu)化模式下,雖然說是全量diff但是可以復用節(jié)點也不會損耗太多性能。

          好了,到了文章的最后,還是希望各位哥哥姐姐能指導指導。有說錯或者遺漏的歡迎在評論區(qū)講解,謝謝。


          關于本文

          作者:咸魚是如何練成的

          https://juejin.cn/post/7072321805792313357


          往期推薦


          秒啊!答好這5個問題,就入門Docker了
          (字節(jié)/華為/美團)前端面經(jīng)記錄冷冷清清的金三銀四
          最新 955 不加班的公司名單(2022版)

          最后


          • 歡迎加我微信,拉你進技術群,長期交流學習...

          • 歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...

          點個在看支持我吧
          瀏覽 22
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  三区麻豆传媒视频 | 亚洲AV永久无码精品久久麻豆 | 97A片在线观看播放 | 91人妻人人爽 | 青青操青青射 |