越來越受歡迎的Vue想學么,90后小姐姐今兒來教你


摘要:Vue的相關技術原理成為了前端崗位面試中的必考知識點,掌握 Vue 對於前端工程師來說更像是一門“必修課”。

本文原作者為尹婷,擅長前端組件庫研發和微信機器人。

我們發現, Vue 越來越受歡迎了。

不管是BAT大廠,還是創業公司,Vue都被廣泛的應用。對比Angular 和 React,三者都是非常優秀的前端框架,但從 GitHub 上來看,Vue 已經達到了 170 萬的 Star。Vue的相關技術原理也成為了前端崗位面試中的必考知識點,掌握 Vue 對於前端工程師來說更像是一門“必修課”。為此,華為雲社區邀請了90后前端開發工程師尹婷帶來了《Vue3.0新特性介紹以及搭建一個vue組件庫》的分享。

了解Vue3.0先從六大特性說起

Vue.js 是一個JavaScriptMVVM庫,是一套構建用戶界面的漸進式框架。在2019年10月05日凌晨,Vue3的源代碼alpha。目前已經發布正式版,作者表示, Vue 3.0具有六大特性:Tree Shaking;Composition;Fragment;Teleport;Suspense;渲染Performance。渲染Performance主要是框架內部的性能優化,相對比較底層,本文會主要為大家介紹前四個特性的解讀。

Tree Shaking

大多數編譯器都會為我們的代碼進行一個死代碼的去除工作。首先我們要了解一下,什么是死代碼呢?

以下幾個特性的代碼,我們把它稱之為死代碼:代碼不會被執行,不可到達;代碼執行的結果不會被用到;代碼只會影響死變量(只寫不讀)。比如我們給一個變量賦值,但是並沒有去用這個變量,那么這就是一個死變量。這就是在我們定義階段會把它去除的一部分,比如說roll up消除死代碼的工作。

如上圖示例,左邊是開發的源碼提供的兩個函數,但最終只用到了baz函數。在最后打包的時候,會把foo函數去除掉,只把baz這個函數打包進瀏覽器里面運行。Tree Shaking是消除死代碼的一種方式,更關注於無用模塊的消除,消除那些引用了但並沒有被使用的模塊。

左邊這塊代碼,export有兩個函數,一個是post,一個是get,但是在我們生產里邊真正使用到只有post。那么rollup在打包之后,就會直接消除掉get的函數,然后只把post的函數打包進入我們的生產里。除了rollup支持這個特性外,webpack也支持。

接下來,我們看一下VUE3.0對Tree Shaking的支持都做了哪些事情?

首先以VUE2和VUE3對nextTick的使用進行對比:VUE2把nextTick掛載到VUE實例上的一個global API式;VUE3先把nextTick模塊剔除,在要使用的時候,再把這個模塊引入。

通過這個對比,我們可以看到使用VUE2的時候,即使沒有nextTick或者其他方法,但由於它是一個GLOBA API,它一定會被掛載到一個實例上,最后打包生產代碼的時候,會把這個函數給打包進去,這一段代碼進而也會影響到文件體積。在VUE3.0如果不需要這個模塊的話,最后打包的這個文件里邊就不會有這一塊代碼。通過這種方式就減少了最后生產代碼的體積。

當然,不只是nextTick,在VUE3.0內部也做了其他很多tree-shaking。例如:如果不使用keep-alive組件或v-show指令,它會少引入很多跟keep-alive或者v-show不相關的包。

上圖為Vue2.0的這段代碼,左邊是引入utils函數,然后把這個函數指為mixins。這一段代碼是在Vue2里邊是最常用到的,但這段代碼是有問題的。
如果對這個項目不熟悉,第一次看到這個代碼的時候,由於不知道這個utils里邊有哪些屬性和方法,也就是說這個mixins對於開發者就是個黑盒。很容易遇到一種場景:在開發組件初期,應用了mixins的一個方法,現在不需要使用該方法了,在刪除的過程發現不知道其他的地方是否引用過mixins其他的屬性和方法。

Composition

如果使用的是Vue3.0 的Composition,該怎么規避這個問題呢?如上圖所示,假設它是一個組件實例,我們使用useMouse函數並返回了X和Y兩個變量。從左邊代碼可以看到useMouse函數就是根,它監聽了鼠標的移動事件之后,返回了鼠標的XY坐標。通過這種方式來組織代碼,就可以很明確的知道這個函數返回的變量和改變的值。

接下來我們再看一個Composition的例子:左邊是在Vue2中最常用的一段代碼,首先在data里邊聲明first name和last name,然后在回帖的時候去請求接口,拿到接口返回到值,在computed之后獲取他的full Name。那么,這段代碼的問題是什么呢?

這里的computed,因為我們不知道返回的full Name的邏輯是什么。在獲取了data之后,是希望通過data的返回值來拿到它的first name和last name,然后來獲取它的full name。但是這一段代碼的邏輯在獲取接口之后就已經斷掉,這就是Vue2.0 設計不合理的一個地方,導致我們的邏輯是分裂派的,分裂在個配置下。那么,如果用Composition的話,怎么樣實現呢?

請求接口之后,直接拿到它的返回數據,然后把這個返回數據的值賦給computed函數里,這里就可以拿到full Name。通過這段代碼可以看到,邏輯是更加的聚合了。

如何做到使用useMouse函數,里邊的變量也是可響應的。在Vue 3.0中提供了兩個函數:reactive和ref。reactive可以傳一個對象進去,然后這個函數返回之后的state,是可響應的;ref是直接傳一個值進去,然后返回到看法對象,它也是可響應的。如果我們在setup函數里邊返回一個可響應值的對象,是可以在字符串模板渲染的時候使用。比如,有時候我們直接在修改data的時候,視圖也會相應的改變。

Vue2中,一般會采用mixins來復用邏輯代碼,但存在一些問題:例如代碼來源不清晰、方法屬性等沖突。基於此,在vue3中引入了Composition API(組合API),使用純函數分隔復用代碼,和React中的hooks的概念很相似。

Composition的優點是暴露給模板的屬性來源清晰,它是從函數返回的;第二,可以進行邏輯重用;第三,返回值可以被任意的命名,不存在秘密空間的沖突;第四,沒有創建額外的組件實力帶來的性能損耗。

以前我們如果想要獲取一個響應式的data,我們必須要把這個data放在component里邊,然后在data里邊進行聲明,這樣的話才能使這個對象是可響應的,現在可直接使用reactive和ref函數就可以使被保變成可響應的。

Fragment

在書寫vue2時,由於組件必須只有一個根節點,很多時候會添加一些沒有意義的節點用於包裹。Fragment組件就是用於解決這個問題的(這和React中的Fragment組件是一樣的)。

Fragment其實就是在Vue2的一個組間里邊,它的template必須要有一個根的DIV把它包住,然后再寫里邊的you。在Vue3,我們就不需要這個根的DIV來把這個組件包住了。上圖就是2和3的對比。

Teleport

Teleport其實就是React中的Portal。Portal 提供了一種將子節點渲染到存在於父組件以外的 DOM 節點的優秀的方案。Teleport提供一個Teleport的組件,會指定一個目標的元素,比如說這里指定的是body,然后Teleport任何的內容都會渲染到這個目標元素中,也就是說下面的這一部分Teleport代碼,它會直接渲染到body。

那么關於Teleport應用的位置,我們可以為大家舉個例子來說明一下。比如說我們在做組件的時候,經常會實現一個dialog。dialog的背景是一個黑的鋪滿全屏DIV,我們對它的布局是position: absolute。如果父級元素是relative布局,我們的這個背景層就會受它的父元素的影響。那么此時,如果用Teleport直接把父組件定為body,這樣它就不會再受到副組件元素樣式的影響,就可以確認一個我們想要的黑色背景畫。

下面我寫一下react和vue的diff算法的比對,我是一邊寫代碼,一邊寫文章,整理一下思路。注:這里只討論tag屬性相同並且多個children的情況,不相同的tag直接替換,刪除,這沒啥好寫的。

用這個例子來說明:

簡單diff,把原有的刪掉,把更新后的插入。

變化前后的標簽都是li,所以只用比對vnodeData和children即可,復用原有的DOM。

先只從這個例子出發,我只用遍歷舊的vnode,然后把舊的vnode和新的vnode patch就行。

這樣就省掉移除和新增dom的開銷,現在的問題是,我的例子剛好是新舊vnode數量一樣,如果不一樣就有問題,示例改成這樣:

實現思路改成:先看看是舊的長度長,還是新的長,如果舊的長,我就遍歷新的,然后把多出來的舊節點刪掉,如果新的長,我就遍歷舊的,然后多出來的新vnode加上。

仍然有可優化的空間,還是下面這幅圖:

通過我們上面的diff算法,實現的過程會比對 preve vnode和next vnode,標簽相同,則只用比對vnodedata和children。發現

 標簽的子節點(文本節點a,b,c)不同,於是分別刪除文本節點a,b,c,然后重新生成新的文本節點c,b,a。但是實際上這幾個

 只是位置不同,那優化的方案就是復用已經生成的dom,把它移動到正確的位置。

怎么移動?我們使用key來將新舊vnode做一次映射。

首先我們找到可以復用的vnode,可以做兩次遍歷,外層遍歷next vnode,內層遍歷prev vnode

如果next vnode和prev vnode只是位置移動,vnodedata和children沒有任何變動,調用patchVnode之后不會有任何dom操作。
接下來只需要把這個key相同的vnode移動到正確的位置即可。我們的問題變成了怎么移動。

首先需要知道兩個事情:

  • 每一個prev vnode都引用了一個真實dom節點,每個next vnode這個時候都沒有真實dom節點。
  • 調用patchVnode的時候會把prevVnode引用的真實Dom的引用賦值給nextVnode,就像這樣:

還是拿上面的例子,外層遍歷next vnode,遍歷第一個元素的時候, 第一個vnode是li©,然后去prev vnode里找,在最后一個節點找到了,這里外層是第一個元素,不做任何移動的操作,我們記錄一下這個vnode在prevVnode中的索引位置lastIndex,接下來在遍歷的時候,如果j<lastIndex,說明原本prevVnode在前面的元素,在nextVnode中變到了后面來了,那么我們就把prevVnode[j]放到nextVnode[i-1]的后面。

這里多說一句,dom操作的api里,只有insertBefore(),沒有insertAfter()。也就是說只有把某個dom插入到某個元素前面這個方法,沒有插入到某個元素后面這個方法,所以我們只能用insertBefore()。那么思路就變成了,當j<lastIndex的時候,把prevChildren[j]插入到nextVnode[i-1]的真實dom的后面元素的前面。

當j>=lastIndex的時候,說明這個順序是正確的的,不用移動,然后把lastIndex = j;
也就是說,只把prevVnode中后面的元素往前移動,原本順序是正確的就不變。
現在我們的diff的代碼變成了這樣:

同樣的問題,如果新舊vnode的元素數量一樣,那就已經可以工作了。接下來要做的就是新增節點和刪除節點。

首先是新增節點,整個框架中將vnode掛載到真實dom上都調用patch函數,patch里調用createElm來生成真實dom。按照上面的實現,如果nextVnode中有一個節點是prevVnode中沒有的,就有問題:

在prevVnode中找不到li(d),那我們需要調用createElm掛在這個新的節點,因為這里的節點需要超入到li(b)和li©之間,所以需要用insertBefore()。在每次遍歷nextVnode的時候用一個變量find=false表示是否能夠在prevVnode中找到節點,如果找到了就find=true。如果內層遍歷后find是false,那說明這是一個新的節點。

我們的createElm函數需要判斷一下第四個參數,如果沒有就是用appendChild直接把元素放到父節點的最后,如果有第四個參數,則需要調用insertBefore來插入到正確的位置。

接下來要做的是刪除prevVnode多余節點:

在nextVnode中已經沒有li(d)了,我們需要在執行完上面所講的所有流程后在遍歷一次prevVnode,然后拿到nextVnode里去找,如果找不到相同key的節點,那就說明這個節點已經被刪除了,我們直接用removeChild方法刪除Dom。

完整的代碼:https://github.com/TingYinHelen/tempo/blob/main/src/platforms/web/patch.js在react-diff分支(目前有可能代碼倉庫還沒有開源,等我實現更完善的時候會開源出來,項目結構可能有變化,看tempo倉庫就行)

這里我的代碼實現的diff算法很明顯看出來時間復雜度是O(n2)。那么這里在算法上依然又可以優化的空間,這里我把nextChildren和prevChildren都設計成了數組的類型,這里可以把nextChildren、prevChildren設計成對象類型,用戶傳入的key作為對象的key,把vnode作為對象的value,這樣就可以只循環nextChildren,然后通過prevChildren[key]的方式找到prevChidren中可復用的dom。這樣就可以把時間復雜度降到O(n)。

以上就是react的diff算法的實現。

vue的diff算法

先說一下上面代碼的問題,舉個例子,下面這個情況:

如果按照react的方法,整個過程會移動2次:
li©是第一個節點,不需要移動,lastIndex=2
li(b), j=1, j<lastIndex, 移動到li©后面 (第1次移動)
li(a), j=0, j<lastIndex, 移動到li(b)后面 (第2次移動)

但是通過肉眼來看,其實只用把li©移動到第一個就行,只需要移動1一次。
於是vue2這么來設計的:

首先找到四個節點vnode:prev的第一個,next的第一個,prev的最后一個,next的最后一個,然后分別把這四個節點作比對:1. 把prev的第一個節點和next的第一個比對;2. 把prev的最后一個和next的最后一個比對;3.prev的第一個和next的最后一個;4. next的第一個和prev的最后一個。如果找到相同key的vnode,就做移動,移動后把前面的指針往后移動,后面的指針往前移動,直到前后的指針重合,如果key不相同就只patch更新vnodedata和children。下面來走一下流程:

  1. li(a)和li(b),key不同,只patch,不移動
  2. li(d)和li©,key不同,只patch,不移動
  3. li(a)和li©,key不同,只patch,不移動
  4. li(d)和li(d),key相同,先patch,需要移動移動,移動的方法就是把prev的li(d)移動到li(a)的前面。然后移動指針,因為prev的最后一個做了移動,所以把prev的指向后面的指針往前移動一個,因為next的第一個vnode已經找到了對應的dom,所以next的前面的指針往后移動一個。

現在比對的圖變成了下面這樣:

這個時候的真實DOM:

繼續比對

  1. li(a)和li(b),key不同,只patch,不移動。
  2. li©和li©,相同相同,先patch,因為next的最后一個元素也剛好是prev的最后一個,所以不移動,prev和next都往前移動指針。

這個時候真實DOM:

現在最新的比對圖:

繼續比對

  1. li(a)和li(b),key不同,只patch,不移動。
  2. li(b)和li(a),key不同,只patch,不移動。
  3. li(a) 和li (a),key相同,patch,把prev的li(a)移動到next的后面指針的元素的后面。

真實的DOM變成了這樣:

比對的圖變成這樣:

繼續比對:
li(b)和li(b)的key相同,patch,都是前指針相同所以不移動,移動指針
這個時候前指針就在后指針后面了,這個比對就結束了。

這就完成了常規的比對,還有不常規的,如下圖:

經過1,2,3,4次比對后發現,沒有相同的key值能夠移動。

這種情況我們沒有辦法,只有用老辦法,用newStartIndex的key拿去依次到prev里的vnode,直到找到相同key值的老的vnode,先patch,然后獲取真實dom移動到正確的位置(放到oldStartIndex前面),然后在prevChildren中把移動過后的vnode設置為undefined,在下次指針移動到這里的時候直接跳過,並且next的start指針向右移動。

function updateChildren (elm, prevChildren, nextChildren) {
  let oldStartIndex = 0;
  let oldEndIndex = prevChildren.length - 1;
  let newStartIndex = 0;
  let newEndIndex = nextChildren.length - 1;

  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    let oldStartVnode = prevChildren[oldStartIndex];
    let oldEndVnode = prevChildren[oldEndIndex];
    let newStartVnode = nextChildren[newStartIndex];
    let newEndVnode = nextChildren[newEndIndex];

    if (oldStartVnode === undefined) {
      oldStartVnode = prevChildren[++oldStartIndex];
    }
    if (oldEndVnode === undefined) {
      oldEndVnode = prevChildren[--oldEndIndex];
    }

    if (oldStartVnode.key === newStartVnode.key) {
      patchVnode(newStartVnode, oldStartVnode);
      oldStartIndex++;
      newStartIndex++;
    } else if (oldEndVnode.key === newEndVnode.key) {
      patchVnode(newEndVnode, oldEndVnode);
      oldEndIndex--;
      newEndIndex--;
    } else if (oldStartVnode.key === newEndVnode.key) {
      patchVnode(newEndVnode, oldStartVnode);
      elm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      newEndIndex--;
      oldStartIndex++;
    } else if (oldEndVnode.key === newStartVnode.key) {
      patchVnode(newStartVnode, oldEndVnode);
      elm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      oldEndIndex--;
      newStartIndex++;
    } else {
      const idxInOld = prevChildren.findIndex(child => child.key === newStartVnode.key);
      if (idxInOld >= 0) {
        elm.insertBefore(prevChildren[idxInOld].elm, oldStartVnode.elm);
        prevChildren[idxInOld] = undefined;
        newStartIndex++;
      }
    }
  }
}

接下來就是新增節點:

這種排列方法,按照上面的方法,經過1,2,3,4比對后找不到相同key,然后然后用newStartIndex到老的vnode中去找,仍然找不着,這個時候說明是一個新節點,把它插入到oldStartIndex前面

最后是刪除節點,我把他作為課后作業,同學可以自己實現最后的刪除的算法。

完整代碼在 https://github.com/TingYinHelen/ tempo的vue分支。

PS.本文部分內容參考自《比對一下react,vue2.x,vue3.x的diff算法》。

 

點擊關注,第一時間了解華為雲新鮮技術~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM