<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>

          Vue3diff算法原理和優(yōu)化

          共 15168字,需瀏覽 31分鐘

           ·

          2021-02-28 21:22


          關(guān)注前端公眾號 【前端每日一博】

          前言

          面試官:

          你知道 Vue3.0 Object.difineProperty和vue3.0的proxy的區(qū)別嗎?

          你知道 Vue3.0 diff算法原理和它有什么優(yōu)化嗎?

          你知道 Vue3.0 proxy是如何通知相關(guān)組件進(jìn)行重新渲染嗎?

          ...

          你:。。。

          隨著vue3.0beta版本的發(fā)布,vue3.0正式版本相信不久就會(huì)與我們相遇。尤雨溪在直播中也說了vue3.0的新特性typescript強(qiáng)烈支持,proxy響應(yīng)式原理,重新虛擬dom,優(yōu)化diff算法性能提升等等??上攵?,Vue3.0時(shí)代有一大波我們需要了解的東西。

          「廢話不多說,今天的主題是和大家分享一下Vue 虛擬Dom、Diff算法原理以及源碼解析、以及Vue3.0的diff算法是如何進(jìn)行優(yōu)化的?!?/strong>

          其他系列在此:

          「Vue3.0時(shí)代你必須了解的:響應(yīng)式原理」

          「Vue3.0時(shí)代你必須了解的:compositon api 用法和注意事項(xiàng)」

          「基于項(xiàng)目實(shí)戰(zhàn)闡述vue3.0新型狀態(tài)管理和邏輯復(fù)用方式」

          目錄

          1. virtual dom
          2. diff算法原理
          3. Vue3.0 diff算法優(yōu)化

          virtual dom

          如果咱們不了解virtual dom 的話,要理解DIff算法是比較困難的。

          概念:

          1. template
          2. 渲染函數(shù)
          3. vnode(virtual dom)
          4. patch(diff算法)
          5. view
          • Vue.js通過編譯將template 模板轉(zhuǎn)換成渲染函數(shù)(render ) ,執(zhí)行渲染函數(shù)就可以得到一個(gè)虛擬節(jié)點(diǎn)樹

          • VNode 虛擬節(jié)點(diǎn):它可以代表一個(gè)真實(shí)的 dom 節(jié)點(diǎn)。通過 createElement 方法能將 VNode 渲染成 dom 節(jié)點(diǎn)。簡單地說,vnode可以理解成節(jié)點(diǎn)描述對象,它描述了應(yīng)該怎樣去創(chuàng)建真實(shí)的DOM節(jié)點(diǎn)。

          • patch(也叫做patching算法):虛擬DOM最核心的部分,它可以將vnode渲染成真實(shí)的DOM,這個(gè)過程是對比新舊虛擬節(jié)點(diǎn)之間有哪些不同,然后根據(jù)對比結(jié)果找出需要更新的的節(jié)點(diǎn)進(jìn)行更新。這點(diǎn)我們從單詞含義就可以看出, patch本身就有補(bǔ)丁、修補(bǔ)的意思,其實(shí)際作用是在現(xiàn)有DOM上進(jìn)行修改來實(shí)現(xiàn)更新視圖的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的實(shí)現(xiàn),并在些基礎(chǔ)上作了很多的調(diào)整和改進(jìn)。

          什么是virtual dom

          通俗易懂的來說就是用一個(gè)簡單的對象去代替復(fù)雜的dom對象。

          如果你去打印一下一個(gè)真實(shí)的DOM節(jié)點(diǎn),就會(huì)發(fā)現(xiàn)DOM節(jié)點(diǎn)上有很多屬性,如果Vue每次都生成一個(gè)新的真實(shí)DOM節(jié)點(diǎn),對性能是巨大的浪費(fèi)。

          Virtual DOM 實(shí)際上就是以JavaScript對象(VNode )為基礎(chǔ)形成一棵樹,對真實(shí)DOM的一層抽象。Vue最終的工作就是通過這棵樹批量生成真實(shí)的DOM節(jié),可以說兩者存在一層映射關(guān)系。

          簡單來說,可以把Virtual DOM 理解為一個(gè)簡單的JS對象,并且最少包含標(biāo)簽名( tag)、屬性(attrs)和子元素對象( children)三個(gè)屬性。不同的框架對這三個(gè)屬性的命名會(huì)有點(diǎn)差別。

          對于虛擬DOM,咱們來看一個(gè)簡單的實(shí)例,就是下圖所示的這個(gè),詳細(xì)的闡述了模板 → 渲染函數(shù) → 虛擬DOM樹 → 真實(shí)DOM的一個(gè)過程

          其實(shí)虛擬DOM在Vue.js主要做了兩件事:

          • 提供與真實(shí)DOM節(jié)點(diǎn)所對應(yīng)的虛擬節(jié)點(diǎn)vnode
          • 將虛擬節(jié)點(diǎn)vnode和舊虛擬節(jié)點(diǎn)oldVnode進(jìn)行對比(diff算法),然后更新視圖

          總結(jié):「vdom是為了減輕性能壓力。dom是昂貴的,昂貴的一方面在dom本身的重量,dom節(jié)點(diǎn)在js里的描述是個(gè)非常復(fù)雜屬性很多原型很長的超級對象,另一方面是瀏覽器渲染進(jìn)程和js進(jìn)程是獨(dú)立分離的,操作dom時(shí)的通信和瀏覽器本身需要重繪的時(shí)耗都是很高的。所以大家機(jī)智的搞了個(gè)輕量的vdom去模擬dom,vdom每個(gè)節(jié)點(diǎn)都只掛載js操作的必要屬性,每次組件update時(shí)都先操作vdom,通過vdom的比對,得到一個(gè)真實(shí)dom的需要操作的集合。整個(gè)機(jī)制是在JavaScript層面計(jì)算,在完成之前并不會(huì)操作DOM,等數(shù)據(jù)穩(wěn)定之后再實(shí)現(xiàn)精準(zhǔn)的修改?!?/strong>

          分析diff算法

          由上我們知道了,新的虛擬DOM和舊的虛擬DOm是通過diff算法進(jìn)行比較之后更新的。

          Vue2.x diff算法

          Vue2.x diff算法原理

          傳統(tǒng)diff算法通過循環(huán)遞歸對節(jié)點(diǎn)進(jìn)行依次對比效率低下,算法復(fù)雜度達(dá)到O(N^3),主要原因在于其追求完全比對和最小修改,而React、Vue則是放棄了完全比對及最小修改,才實(shí)現(xiàn)從O(N^3) => O(N)。

          優(yōu)化措施有:

          • 「分層diff」:不考慮跨層級移動(dòng)節(jié)點(diǎn),讓新舊兩個(gè)VDOM樹的比對無需循環(huán)遞歸(復(fù)雜度大幅優(yōu)化,直接下降一個(gè)數(shù)量級的首要條件)。這個(gè)前提也是Web UI 中 DOM 節(jié)點(diǎn)跨層級的移動(dòng)操作特別少,可以忽略不計(jì)。

          在同層節(jié)點(diǎn)中,采用了「雙端比較的算法」過程可以概括為:oldCh和newCh各有兩個(gè)頭尾的變量StartIdx和EndIdx,它們的2個(gè)變量相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設(shè)置了key,就會(huì)用key進(jìn)行比較,在比較的過程中,變量會(huì)往中間靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一個(gè)已經(jīng)遍歷完了,就會(huì)結(jié)束比較;

          當(dāng)發(fā)生以下情況則跳過比對,變?yōu)椴迦牖騽h除操作:

          • 「組件的Type(Tagname)不一致」,原因是絕大多數(shù)情況擁有相同type的兩個(gè)組件將會(huì)生成相似的樹形結(jié)構(gòu),擁有不同type的兩個(gè)組件將會(huì)生成不同的樹形結(jié)構(gòu),所以type不一致可以放棄繼續(xù)比對。

          • 「列表組件的Key不一致」,舊樹中無新Key或反之。畢竟key是元素的身份id,能直接對應(yīng)上是否是同一個(gè)節(jié)點(diǎn)。

          • 對觸發(fā)了getter/setter 的組件進(jìn)行diff,精準(zhǔn)減少diff范圍

          Vue3.0 diff

          diff痛點(diǎn)

          vue2.x中的虛擬dom是進(jìn)行「全量的對比」,在運(yùn)行時(shí)會(huì)對所有節(jié)點(diǎn)生成一個(gè)虛擬節(jié)點(diǎn)樹,當(dāng)頁面數(shù)據(jù)發(fā)生變更好,會(huì)遍歷判斷virtual dom所有節(jié)點(diǎn)(包括一些不會(huì)變化的節(jié)點(diǎn))有沒有發(fā)生變化;雖然說diff算法確實(shí)減少了多DOM節(jié)點(diǎn)的直接操作,但是這個(gè)「減少是有成本的」,如果是復(fù)雜的大型項(xiàng)目,必然存在很復(fù)雜的父子關(guān)系的VNode,「而Vue2.x的diff算法,會(huì)不斷地遞歸調(diào)用 patchVNode,不斷堆疊而成的幾毫秒,最終就會(huì)造成 VNode 更新緩慢」。

          那么Vue3.0是如何解決這些問題的呢

          動(dòng)靜結(jié)合 PatchFlag

          來個(gè)??:

          <div>
            <div>{msg}</div>
            <div>靜態(tài)文字</div>
          </div>

          在Vue3.0中,在這個(gè)模版編譯時(shí),編譯器會(huì)在動(dòng)態(tài)標(biāo)簽?zāi)┪布由?/* Text*/ PatchFlag。「也就是在生成VNode的時(shí)候,同時(shí)打上標(biāo)記,在這個(gè)基礎(chǔ)上再進(jìn)行核心的diff算法」并且 PatchFlag 會(huì)標(biāo)識動(dòng)態(tài)的屬性類型有哪些,比如這里 的TEXT 表示只有節(jié)點(diǎn)中的文字是動(dòng)態(tài)的。而patchFlag的類型也很多。這里直接引用一張圖片。

          其中大致可以分為兩類:

          • 當(dāng) patchFlag 的值「大于」 0 時(shí),代表所對應(yīng)的元素在 patchVNode 時(shí)或 render 時(shí)是可以被優(yōu)化生成或更新的。

          • 當(dāng) patchFlag 的值「小于」 0 時(shí),代表所對應(yīng)的元素在 patchVNode 時(shí),是需要被 full diff,即進(jìn)行遞歸遍歷 VNode tree 的比較更新過程。

          看源碼:

          export function render(_ctx, _cache, $props$setup$data$options) {
           return (_openBlock(), _createBlock("div", null, [
            _createVNode("p", null, "'HelloWorld'"),
            _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
           
           ]))
          }
          ****

          這里的_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)就是對變量節(jié)點(diǎn)進(jìn)行標(biāo)記。

          總結(jié):「Vue3.0對于不參與更新的元素,做靜態(tài)標(biāo)記并提示,只會(huì)被創(chuàng)建一次,在渲染時(shí)直接復(fù)用。」

          其中還有cacheHandlers(事件偵聽器緩存),這里就不講了。

          diff算法源碼解析

          以數(shù)組為栗子:

          newNode:[a,b,c,d,e,f,g] 

          oldNode:[a,b,c,h,i,j,f,g]

          步驟1:「從首部比較new vnode 和old vnode」,如果碰到不同的節(jié)點(diǎn),跳出循環(huán),否則繼續(xù),直到一方遍歷完成;

          由此我們得到newNode和oldNode首部相同的片段為 a,b,c,

          源碼:

          const patchKeyedChildren = (
              c1,
              c2,
              container,
              parentAnchor,
              parentComponent,
              parentSuspense,
              isSVG,
              optimized
          ) => {
              let i = 0;  
              const l2 = c2.length
              let e1 = c1.length - 1
              let e2 = c2.length - 1

              
              while (i <= e1 && i <= e2) {
                const n1 = c1[i]
                const n2 = c2[i]
                if (isSameVNodeType(n1, n2)) {
                  patch(
                    n1,
                    n2,
                    container,
                    parentAnchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                  )
                } else {
                  break
                }
                i++
              }
              
              //這里的isSameVNodeType
                export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
                // 比較類型和key是否一致()
                return n1.type === n2.type && n1.key === n2.key
                }

          「Tip」:這里的isSameVNodeType從「type和key」,因此key作為唯一值是非常重要的,這也就解釋了 v-for循環(huán)遍歷不能用index作為key的原因。

          步驟2:「從尾部比較new vnode 和old vnode」,如果碰到不同的節(jié)點(diǎn),跳出循環(huán),否則繼續(xù),直到一方遍歷完成;

          由此我們得到newNode和oldNode尾部相同的片段為 f,g

          while (i <= e1 && i <= e2) {
                const n1 = c1[e1]
                const n2 = c2[e2]
                if (isSameVNodeType(n1, n2)) {
                  patch(
                    n1,
                    n2,
                    container,
                    parentAnchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                  )
                } else {
                  break
                }
                e1--
                e2--
              }
          }

          在遍歷過程中滿足i > e1 && i < e2,說明 「僅有節(jié)點(diǎn)新增」

          if (i > e1) {
              if (i <= e2) {
                  const nextPos = e2 + 1;
                  const anchor = nextPos < l2 ? c2[nextPos] : parentAnchor
                  while (i <= e2) {
                      patch(
                          null,
                          c2[i],
                          container,
                          anchor,
                          parentComponent,
                          parentSuspense,
                          isSVG
                      )
                      i++
                  }
              }
          else if {
              ...
          else {
             ...
          }

          在遍歷過程中滿足i > e1 && i > e2,說明 「僅有節(jié)點(diǎn)移除」

          if (i > e1) {
            //
          else if (i > e2) {
              while (i <= e1) {
                  unmount(c1[i], parentComponent, parentSuspense, true)
                  i++
              }
          else {
              //
          }

          步驟3: 「節(jié)點(diǎn)移動(dòng)、新增或刪除」

          經(jīng)過以上步驟,剩下的就是不確定的元素,那么diff算法將遍歷 所有的new node,將key和索引存在keyToNewIndexMap中,為map解構(gòu),

          if (i > e1) {
            //
          else if (i > e2) {
            //
          else {
              const s1 = i
              const s2 = i

              const keyToNewIndexMap = new Map()
              for (i = s2; i <= e2; i++) {
                  const nextChild = c2[i]
                  if (nextChild.key !== null) {
                      keyToNewIndexMap.set(nextChild.key, i)
                  }
              }
          }

          接下來

           for (i = s1; i <= e1; i++) { /* 開始遍歷老節(jié)點(diǎn) */
                  const prevChild = c1[i]
                  if (patched >= toBePatched) { /* 已經(jīng)patch數(shù)量大于等于, */
                    /* ① 如果 toBePatched新的節(jié)點(diǎn)數(shù)量為0 ,那么統(tǒng)一卸載老的節(jié)點(diǎn) */
                    unmount(prevChild, parentComponent, parentSuspense, true)
                    continue
                  }
                  let newIndex
                   /* ② 如果,老節(jié)點(diǎn)的key存在 ,通過key找到對應(yīng)的index */
                  if (prevChild.key != null) {
                    newIndex = keyToNewIndexMap.get(prevChild.key)
                  } else { /*  ③ 如果,老節(jié)點(diǎn)的key不存在 */
                    for (j = s2; j <= e2; j++) { /* 遍歷剩下的所有新節(jié)點(diǎn) */
                      if (
                        newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新節(jié)點(diǎn)沒有被patch */
                        isSameVNodeType(prevChild, c2[j] as VNode)
                      ) { /* 如果找到與當(dāng)前老節(jié)點(diǎn)對應(yīng)的新節(jié)點(diǎn)那么 ,將新節(jié)點(diǎn)的索引,賦值給newIndex  */
                        newIndex = j
                        break
                      }
                    }
                  }
                  if (newIndex === undefined) { /* ①?zèng)]有找到與老節(jié)點(diǎn)對應(yīng)的新節(jié)點(diǎn),刪除當(dāng)前節(jié)點(diǎn),卸載所有的節(jié)點(diǎn) */
                    unmount(prevChild, parentComponent, parentSuspense, true)
                  } else {
                    /* ②把老節(jié)點(diǎn)的索引,記錄在存放新節(jié)點(diǎn)的數(shù)組中, */
                    newIndexToOldIndexMap[newIndex - s2] = i + 1
                    if (newIndex >= maxNewIndexSoFar) {
                      maxNewIndexSoFar = newIndex
                    } else {
                      /* 證明有節(jié)點(diǎn)已經(jīng)移動(dòng)了   */
                      moved = true
                    }
                    /* 找到新的節(jié)點(diǎn)進(jìn)行patch節(jié)點(diǎn) */
                    patch(
                      prevChild,
                      c2[newIndex] as VNode,
                      container,
                      null,
                      parentComponent,
                      parentSuspense,
                      isSVG,
                      optimized
                    )
                    patched++
                  }
           }

          之后通過 計(jì)算出「最長遞增子序列」 減少Dom元素的移動(dòng),達(dá)到最少的 dom 操作。至于什么是最長遞增子序列,有興趣的小伙伴可以自信查閱。

          最后

          Vue3.0 的diff原理和優(yōu)化也講完了,如有錯(cuò)誤,歡迎指出。

          瀏覽 77
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                    久久久精品偷拍 | 牛牛精品视频 | 欧美激情亚洲五码 | 国产黄色A片三级熟女 | 超碰人人在线 |