當面試官讓我回答React和Vue框架的區別......

語言: CN / TW / HK

theme: channing-cyan

Vue 和 React 作為當前前端兩大火熱的框架,面試的時候自然不少被提及: - 請說一下你對react/vue框架的理解 - 請對比一下兩大框架的優缺點

其實react和vue大體上是相同的,比如都使用虛擬DOM高效的更新檢視,都提倡元件化,都實現了資料驅動檢視,都使用diff演算法,也都對diff演算法進行了優化,都有router庫實現url到元件的對映,都有狀態管理等等.....

但是在具體實現上又不盡相同,接下來就從元件化,虛擬DOM以及資料驅動檢視三個方面對比下vue和react框架的相同和不同之處。

1.對於元件化的理解,元件化帶來的好處

  1. 元件是獨立和可複用的程式碼組織單元,它使開發者使用小型、獨立和通常可複用的元件構建大型應用;
  2. 元件化開發能大幅提高應用開發效率、測試性、複用性等;
  3. 降低整個系統的耦合度,在保持介面不變的情況下,我們可以替換不同的元件快速完成需求,例如輸入框,可以替換為日曆、時間、範圍等元件作具體的實現
  4. 除錯方便,由於整個系統是通過元件組合起來的,在出現問題的時候,可以用排除法直接移除元件,或者根據報錯的元件快速定位問題,之所以能夠快速定位,是因為每個元件之間低耦合,職責單一,所以邏輯會比分析整個系統要簡單
  5. 提高可維護性,由於每個元件的職責單一,並且元件在系統中是被複用的,所以對程式碼進行優化可獲得系統的整體升級

react和vue中元件化的相同點

react和vue都推崇元件化,通過將頁面拆分成一個一個小的可複用單元來提高程式碼的複用率和開發效率。 在開發時react和vue有相同的套路,比如都有父子元件傳參,都有資料狀態管理,都有前端路由等。

react和vue元件化的差異

React推薦的做法是JSX + inline style, 也就是把 HTML 和 CSS 全都寫進 JavaScript 中,即 all in js;

Vue 推薦的做法是 template 的單檔案元件格式(簡單易懂,從傳統前端轉過來易於理解),即 html,css,JS 寫在同一個檔案(vue也支援JSX寫法)

2.虛擬DOM

什麼是虛擬DOM

虛擬 DOM(Virtual DOM)本質上是JS 和 DOM 之間的一個對映快取,它在形態上表現為一個能夠描述 DOM 結構及其屬性資訊的 JS 物件。它主要儲存在記憶體中。主要來說:

  • 虛擬dom是一個js物件,儲存在記憶體之中。
  • 虛擬dom能夠描述真實dom(存在一個對應關係)
  • 當資料變化的時候,生成新的DOM,對比新舊虛擬DOM的差異,將差異更新到真實DOM上

虛擬DOM的優點

  • 減少 DOM 操作:虛擬 DOM 可以將多次 DOM 操作合併為一次操作
  • 研發效率的問題:虛擬 DOM 的出現,為資料驅動檢視這一思想提供了高度可用的載體,使得前端開發能夠基於函式式 UI 的程式設計方式實現高效的宣告式程式設計。
  • 跨平臺的問題:虛擬 DOM 是對真實渲染內容的一層抽象。同一套虛擬 DOM,可以對接不同平臺的渲染邏輯,從而實現“一次編碼,多端執行”

react和vue中虛擬DOM的相同點

Vue與React都使用了 Virtual DOM + Diff演算法, 不管是Vue的Template模板+options api 寫法, 還是React的Class或者Function寫法,最後都是生成render函式,而render函式執行返回VNode(虛擬DOM的資料結構,本質上是棵樹)。

當每一次UI更新時,總會根據render重新生成最新的VNode,然後跟以前快取起來老的VNode進行比對,再使用Diff演算法(框架核心)去真正更新真實DOM(虛擬DOM是JS物件結構,同樣在JS引擎中,而真實DOM在瀏覽器渲染引擎中,所以操作虛擬DOM比操作真實DOM開銷要小的多)

vue&&reactVirtualDOM.png

兩者對diff演算法的優化基本上思路是相同的:

  • tag不同認為是不同節點
  • 只比較同一層級,不跨級比較
  • 同一層級的節點用key唯一標識,tag和key都相同則認為是同一節點

diff優化.png

diff 演算法原始碼實現相同之處

在處理老節點部分,都需要把節點處理 key - value 的 Map 資料結構,方便在往後的比對中可以快速通過節點的 key 取到對應的節點。同樣在比對兩個新老節點是否相同時,key 是否相同也是非常重要的判斷標準。所以不同是 React, 還是 Vue,在寫動態列表的時候,都需要設定一個唯一值 key,這樣在 diff 演算法處理的時候效能才最大化。

react和vue中虛擬DOM的差別

react和vue的虛擬dom都是用js物件來模擬真實DOM,用虛擬DOM的diff來最小化更新真實DOM,可以減小不必要的效能損耗,按顆粒度分為不同的型別比較同層級dom節點,進行增、刪、移的操作。

按顆粒度分為tree diff, component diff, element diff. tree diff 比較同層級dom節點,進行增、刪、移操作。如果遇到component, 就會重新tree diff流程。

參考連結

dom的更新策略不同

react 會自頂向下全diff。vue會跟蹤每一個元件的依賴關係,不需要重新渲染整個元件樹。

在react中,當狀態發生改變時,元件樹就會自頂向下的全diff, 重新render頁面, 重新生成新的虛擬dom tree, 新舊dom tree進行比較, 進行patch打補丁方式,區域性更新dom。所以react為了避免父元件更新而引起不必要的子元件更新, 可以在shouldComponentUpdate做邏輯判斷,減少沒必要的render, 以及重新生成虛擬dom,做差量對比過程。

在vue中, 通過Object.defineProperty 把 data 屬性全部轉為 getter/setter。同時watcher例項物件會在元件渲染時,將屬性記錄為dep, 當dep 項中的 setter被呼叫時,通知watch重新計算,使得關聯元件更新。

Diff 演算法藉助元素的 Key 判斷元素是新增、刪除、修改,從而減少不必要的元素重渲染。

diff 演算法原始碼實現不同之處

react的diff

宣告newChildren就是即將更新的 JSX 物件

  1. 當newChildren型別為object、number、string,代表同級只有一個節點

    • 檢查上次更新時的fiber節點是否存在對應的DOM節點
      • 存在:DOM節點是否可以複用(通過tag和key進行判斷
        • 可以:將上次更新的fiber節點副本作為本次新生成的fiber節點並返回
        • 不可以:標記當前節點為待刪除節點,新生成一個fiber節點並返回
      • 不存在:新生成一個fiber節點並返回
  2. 當newChildren型別為Array,同級有多個節點,會進行兩次遍歷:

    • 第一層遍歷:

      • 遍歷newChildren,i = 0,將newChildren[i]與oldFiber比較,判斷DOM節點是否可複用。
      • 如果可複用,i++,比較newChildren[i]與oldFiber.sibling是否可複用。可以複用則重複此步驟。
      • 如果不可複用,立即跳出整個遍歷。
      • 如果newChildren遍歷完或者oldFiber遍歷完(即oldFiber.sibling === null),跳出遍歷。

        由上述,第一次遍歷完可能存在以下2種情況: a. 若是因為"不可復"用導致的跳出遍歷:newChildren沒有遍歷完,oldFiber也沒有遍歷完。 b. 若是因為"newChildren遍歷完或者oldFiber遍歷完"導致的跳出遍歷:可能newChildren遍歷完,或oldFiber遍歷完,或他們同時遍歷完。 (帶著第一輪遍歷的結果去進行第二輪的遍歷)

    • 第二輪遍歷:第二輪遍歷的時候會將剩餘未比較的老節點和剩餘未比較的新節點進行遍歷

      • newChildren沒遍歷完,oldFiber遍歷完:遍歷餘下的newChildren依次進行插入
      • newChildren遍歷完,oldFiber沒遍歷完:遍歷剩下的oldFiber依次進行刪除
      • newChildren與oldFiber都沒遍歷完:這意味著有節點在這次更新中改變了位置。

        reactDiff.png

        reactDiffExcle.png

        • index: 新集合的遍歷下標。
        • oldIndex:當前節點在老集合中的下標
        • maxIndex:在新集合訪問過的節點中,其在老集合的最大下標

        如果當前節點在新集合中的位置比老集合中的位置靠前的話,是不會影響後續節點操作的,這裡這時候節點不用動

        操作過程中只比較oldIndex和maxIndex,規則如下:

        • 當oldIndex>maxIndex時,將oldIndex的值賦值給maxIndex
        • 當oldIndex=maxIndex時,不操作
        • 當oldIndex<maxIndex時,將當前節點移動到index的位置

        diff過程如下:

        • 節點B:此時 maxIndex=0,oldIndex=1;滿足 maxIndex< oldIndex,因此B節點不動,此時maxIndex= Math.max(oldIndex, maxIndex),就是1
        • 節點A:此時maxIndex=1,oldIndex=0;不滿足maxIndex< oldIndex,因此A節點進行移動操作,此時maxIndex= Math.max(oldIndex, maxIndex),還是1
        • 節點D:此時maxIndex=1, oldIndex=3;滿足maxIndex< oldIndex,因此D節點不動,此時maxIndex= Math.max(oldIndex, maxIndex),就是3
        • 節點C:此時maxIndex=3,oldIndex=2;不滿足maxIndex< oldIndex,因此C節點進行移動操作,當前已經比較完了 當ABCD節點比較完成後,diff過程還沒完,還會整體遍歷老集合中節點,看有沒有沒用到的節點,有的話,就刪除

更詳細的diff參考這裡

vue的diff

patch函式會接受兩個引數:oldVnode 和 vnode,其分別指舊的vnode和新的vnode

  1. 只有新節點
    • createElm 建立新的節點
  2. 只有舊節點
    • 刪除舊節點
  3. 新舊節點都存在:通過 sameVnode 判斷節點是否一樣:
    • 一樣:直接呼叫 patchVnode 去處理這兩個節點
      • Vnode 是文字節點,則更新文字(文字節點不存在子節點)
        • 當新Vnode.text 存在,而且和 舊 VNode.text 不一樣時,直接更新這個 DOM 的 文字內容
        • 新Vnode 的 text 為空,直接把 文字DOM 賦值給空
      • Vnode 有子節點,則處理比較更新子節點
        • 新舊節點都有子節點,而且不一樣,那就執行updateChildren
          • updateChildren 維持新舊節點首尾的四個指標進行遍歷對比,遵循的原則是:能不移動,儘量不移動。不行就移動,實在不行就新建
        • 只有新子節點:執行建立
        • 只有舊子節點:執行刪除
    • 不一樣:直接建立新節點,刪除舊節點
為什麼react不使用雙指標提升比較效率

react在原始碼中註釋道:React 不能通過雙端對比進行 Diff 演算法優化是因為目前 Fiber 上沒有設定反向連結串列,而且想知道就目前這種方案能持續多久,如果目前這種模式不理想的話,那麼也可以增加雙端對比演算法。

也就是說雖然更新的JSX物件即newChildren為陣列形式,但是和newChildren中每個值進行比較的是上次更新的Fiber節點,Fiber節點的同級節點是由sibling指標連結形成的連結串列。

即 newChildren[0]與oldFiber比較,newChildren[1]與oldFiber.sibling比較。

單鏈表無法使用雙指標,所以無法對演算法使用雙指標優化。

基於以上原因,Diff演算法的整體邏輯會經歷兩輪遍歷: - 第一輪遍歷:處理更新的節點。 - 第二輪遍歷:處理剩下的不屬於更新的節點(新增、刪除、移動)。

總結

Vue2的核心Diff演算法採用了雙端比較的演算法,同時從新舊children的兩端開始進行比較,藉助key值找到可複用的節點,再進行相關操作。相比React的Diff演算法,同樣情況下可以減少移動節點次數,減少不必要的效能損耗,更加的優雅。

3.資料驅動檢視

資料驅動檢視:就是資料變化的時候,相應的檢視會得到更新。開發者只需要關注資料的變化而不用再去手動的操作DOM。

vue中的資料驅動檢視

Vuejs的資料驅動是通過MVVM這種框架來實現的。MVVM框架主要包含3個部分:model、view和 viewModel。

  • Model:指的是資料部分,對應到前端就是javascript物件

  • View:指的是檢視部分,對應前端就是dom

  • ViewModel:就是連線檢視與資料的中介軟體

ViewModel是實現資料驅動檢視的核心,當資料變化的時候,ViewModel能夠監聽到這種變化,並及時的通知view做出修改。同樣的,當頁面有事件觸發時,ViewModel也能夠監聽到事件,並通知model進行響應。ViewModel就相當於一個觀察者,監控著雙方的動作,並及時通知對方進行相應的操作。

首先,vuejs在例項化的過程中,會對遍歷傳給例項化物件選項中的data 選項,遍歷其所有屬性並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。

同時每一個例項物件都有一個watcher例項物件,他會在模板編譯的過程中,用getter去訪問data的屬性,watcher此時就會把用到的data屬性記為依賴,這樣就建立了檢視與資料之間的聯絡。當之後我們渲染檢視的資料依賴發生改變(即資料的setter被呼叫)的時候,watcher會對比前後兩個的數值是否發生變化,然後確定是否通知檢視進行重新渲染。這樣就實現了所謂的資料對於檢視的驅動。

react的資料驅動檢視

首先了解一些列內容: - pending:當前所有等待更新的state佇列。 - isBatchingUpdates:React中用於標識當前是否處理批量更新狀態,預設false。 - dirtyComponent:當前所有待更新state的元件佇列

React通過setState實現資料驅動檢視,通過setState來引發一次元件的更新過程從而實現頁面的重新渲染(除非shouldComponentUpdate返回false)。

  • setState()首先將接收的第一個引數state儲存在pending佇列中;(state)
  • 判斷當前React是否處於批量更新狀態,是的話就將需要更新state的元件新增到dirtyComponents中;(元件)
  • 不是的話,它會遍歷dirtyComponents的所有元件,呼叫updateComponent方法更新每個dirty元件(開啟批量更新事務)