當面試官讓我回答React和Vue框架的區(qū)別


Vue 和 React 作為當前前端兩大火熱的框架,面試的時候自然不少被提及:


請說一下你對react/vue框架的理解

請對比一下兩大框架的優(yōu)缺點

其實react和vue大體上是相同的,比如都使用虛擬DOM高效的更新視圖,都提倡組件化,都實現(xiàn)了數(shù)據(jù)驅動視圖,都使用diff算法,也都對diff算法進行了優(yōu)化,都有router庫實現(xiàn)url到組件的映射,都有狀態(tài)管理等等.....


但是在具體實現(xiàn)上又不盡相同,接下來就從組件化,虛擬DOM以及數(shù)據(jù)驅動視圖三個方面對比下vue和react框架的相同和不同之處。


1.對于組件化的理解,組件化帶來的好處

組件是獨立和可復用的代碼組織單元,它使開發(fā)者使用小型、獨立和通??蓮陀玫慕M件構建大型應用;

組件化開發(fā)能大幅提高應用開發(fā)效率、測試性、復用性等;

降低整個系統(tǒng)的耦合度,在保持接口不變的情況下,我們可以替換不同的組件快速完成需求,例如輸入框,可以替換為日歷、時間、范圍等組件作具體的實現(xiàn)

調試方便,由于整個系統(tǒng)是通過組件組合起來的,在出現(xiàn)問題的時候,可以用排除法直接移除組件,或者根據(jù)報錯的組件快速定位問題,之所以能夠快速定位,是因為每個組件之間低耦合,職責單一,所以邏輯會比分析整個系統(tǒng)要簡單

提高可維護性,由于每個組件的職責單一,并且組件在系統(tǒng)中是被復用的,所以對代碼進行優(yōu)化可獲得系統(tǒng)的整體升級

react和vue中組件化的相同點

react和vue都推崇組件化,通過將頁面拆分成一個一個小的可復用單元來提高代碼的復用率和開發(fā)效率。在開發(fā)時react和vue有相同的套路,比如都有父子組件傳參,都有數(shù)據(jù)狀態(tài)管理,都有前端路由等。


react和vue組件化的差異

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


Vue 推薦的做法是 template 的單文件組件格式(簡單易懂,從傳統(tǒng)前端轉過來易于理解),即 html,css,JS 寫在同一個文件(vue也支持JSX寫法)


2.虛擬DOM

什么是虛擬DOM

虛擬 DOM(Virtual DOM)本質上是JS 和 DOM 之間的一個映射緩存,它在形態(tài)上表現(xiàn)為一個能夠描述 DOM 結構及其屬性信息的 JS 對象。它主要存儲在內存中。主要來說:


虛擬dom是一個js對象,存儲在內存之中。

虛擬dom能夠描述真實dom(存在一個對應關系)

當數(shù)據(jù)變化的時候,生成新的DOM,對比新舊虛擬DOM的差異,將差異更新到真實DOM上

虛擬DOM的優(yōu)點

減少 DOM 操作:虛擬 DOM 可以將多次 DOM 操作合并為一次操作

研發(fā)效率的問題:虛擬 DOM 的出現(xiàn),為數(shù)據(jù)驅動視圖這一思想提供了高度可用的載體,使得前端開發(fā)能夠基于函數(shù)式 UI 的編程方式實現(xiàn)高效的聲明式編程。

跨平臺的問題:虛擬 DOM 是對真實渲染內容的一層抽象。同一套虛擬 DOM,可以對接不同平臺的渲染邏輯,從而實現(xiàn)“一次編碼,多端運行”

react和vue中虛擬DOM的相同點

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


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



vue&&reactVirtualDOM.png

兩者對diff算法的優(yōu)化基本上思路是相同的:

tag不同認為是不同節(jié)點

只比較同一層級,不跨級比較

同一層級的節(jié)點用key唯一標識,tag和key都相同則認為是同一節(jié)點


diff優(yōu)化.png

diff 算法源碼實現(xiàn)相同之處

在處理老節(jié)點部分,都需要把節(jié)點處理 key - value 的 Map 數(shù)據(jù)結構,方便在往后的比對中可以快速通過節(jié)點的 key 取到對應的節(jié)點。同樣在比對兩個新老節(jié)點是否相同時,key 是否相同也是非常重要的判斷標準。所以不同是 React, 還是 Vue,在寫動態(tài)列表的時候,都需要設置一個唯一值 key,這樣在 diff 算法處理的時候性能才最大化。


react和vue中虛擬DOM的差別

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


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


參考鏈接


dom的更新策略不同

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


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


在vue中, 通過Object.defineProperty 把 data 屬性全部轉為 getter/setter。同時watcher實例對象會在組件渲染時,將屬性記錄為dep, 當dep 項中的 setter被調用時,通知watch重新計算,使得關聯(lián)組件更新。


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








diff 算法源碼實現(xiàn)不同之處

react的diff

聲明newChildren就是即將更新的 JSX 對象


當newChildren類型為object、number、string,代表同級只有一個節(jié)點


存在:DOM節(jié)點是否可以復用(


通過tag和key進行判斷



不存在:新生成一個fiber節(jié)點并返回


可以:將上次更新的fiber節(jié)點副本作為本次新生成的fiber節(jié)點并返回

不可以:標記當前節(jié)點為待刪除節(jié)點,新生成一個fiber節(jié)點并返回

檢查上次更新時的fiber節(jié)點是否存在對應的DOM節(jié)點


當newChildren類型為Array,同級有多個節(jié)點,會進行兩次遍歷:


newChildren沒遍歷完,oldFiber遍歷完:遍歷余下的newChildren依次進行插入


newChildren遍歷完,oldFiber沒遍歷完:遍歷剩下的oldFiber依次進行刪除


newChildren與oldFiber都沒遍歷完:這意味著有節(jié)點在這次更新中改變了位置。



reactDiff.png


reactDiffExcle.png

如果當前節(jié)點在新集合中的位置比老集合中的位置靠前的話,是不會影響后續(xù)節(jié)點操作的,這里這時候節(jié)點不用動


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


diff過程如下:


節(jié)點B:此時 maxIndex=0,oldIndex=1;滿足 maxIndex< oldIndex,因此B節(jié)點不動,此時maxIndex= Math.max(oldIndex, maxIndex),就是1

節(jié)點A:此時maxIndex=1,oldIndex=0;不滿足maxIndex< oldIndex,因此A節(jié)點進行移動操作,此時maxIndex= Math.max(oldIndex, maxIndex),還是1

節(jié)點D:此時maxIndex=1, oldIndex=3;滿足maxIndex< oldIndex,因此D節(jié)點不動,此時maxIndex= Math.max(oldIndex, maxIndex),就是3

節(jié)點C:此時maxIndex=3,oldIndex=2;不滿足maxIndex< oldIndex,因此C節(jié)點進行移動操作,當前已經(jīng)比較完了 當ABCD節(jié)點比較完成后,diff過程還沒完,還會整體遍歷老集合中節(jié)點,看有沒有沒用到的節(jié)點,有的話,就刪除

當oldIndex>maxIndex時,將oldIndex的值賦值給maxIndex

當oldIndex=maxIndex時,不操作

當oldIndex<maxIndex時,將當前節(jié)點移動到index的位置

index:新集合的遍歷下標。

oldIndex:當前節(jié)點在老集合中的下標

maxIndex:在新集合訪問過的節(jié)點中,其在老集合的最大下標

遍歷newChildren,i = 0,將newChildren[i]與oldFiber比較,判斷DOM節(jié)點是否可復用。

如果可復用,i++,比較newChildren[i]與oldFiber.sibling是否可復用??梢詮陀脛t重復此步驟。

如果不可復用,立即跳出整個遍歷。

如果newChildren遍歷完或者oldFiber遍歷完(即oldFiber.sibling === null),跳出遍歷。

第一層遍歷:


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


第二輪遍歷:第二輪遍歷的時候會將剩余未比較的老節(jié)點和剩余未比較的新節(jié)點進行遍歷


更詳細的diff參考這里


vue的diff

patch函數(shù)會接受兩個參數(shù):oldVnode 和 vnode,其分別指舊的vnode和新的vnode


只有新節(jié)點

createElm 創(chuàng)建新的節(jié)點

只有舊節(jié)點

刪除舊節(jié)點

新舊節(jié)點都存在:通過 sameVnode 判斷節(jié)點是否一樣:

Vnode 是文本節(jié)點,則更新文本(文本節(jié)點不存在子節(jié)點)

Vnode 有子節(jié)點,則處理比較更新子節(jié)點

當新Vnode.text 存在,而且和 舊 VNode.text 不一樣時,直接更新這個 DOM 的 文本內容

新Vnode 的 text 為空,直接把 文本DOM 賦值給空

updateChildren 維持新舊節(jié)點首尾的四個指針進行遍歷對比,遵循的原則是:能不移動,盡量不移動。不行就移動,實在不行就新建

新舊節(jié)點都有子節(jié)點,而且不一樣,那就執(zhí)行updateChildren

只有新子節(jié)點:執(zhí)行創(chuàng)建

只有舊子節(jié)點:執(zhí)行刪除

一樣:直接調用 patchVnode 去處理這兩個節(jié)點

不一樣:直接創(chuàng)建新節(jié)點,刪除舊節(jié)點

為什么react不使用雙指針提升比較效率

react在源碼中注釋道:React 不能通過雙端對比進行 Diff 算法優(yōu)化是因為目前 Fiber 上沒有設置反向鏈表,而且想知道就目前這種方案能持續(xù)多久,如果目前這種模式不理想的話,那么也可以增加雙端對比算法。


也就是說雖然更新的JSX對象即newChildren為數(shù)組形式,但是和newChildren中每個值進行比較的是上次更新的Fiber節(jié)點,F(xiàn)iber節(jié)點的同級節(jié)點是由sibling指針鏈接形成的鏈表。


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


單鏈表無法使用雙指針,所以無法對算法使用雙指針優(yōu)化。


基于以上原因,Diff算法的整體邏輯會經(jīng)歷兩輪遍歷:


第一輪遍歷:處理更新的節(jié)點。

第二輪遍歷:處理剩下的不屬于更新的節(jié)點(新增、刪除、移動)。

總結

Vue2的核心Diff算法采用了雙端比較的算法,同時從新舊children的兩端開始進行比較,借助key值找到可復用的節(jié)點,再進行相關操作。相比React的Diff算法,同樣情況下可以減少移動節(jié)點次數(shù),減少不必要的性能損耗,更加的優(yōu)雅。


3.數(shù)據(jù)驅動視圖

數(shù)據(jù)驅動視圖:就是數(shù)據(jù)變化的時候,相應的視圖會得到更新。開發(fā)者只需要關注數(shù)據(jù)的變化而不用再去手動的操作DOM。


vue中的數(shù)據(jù)驅動視圖

Vuejs的數(shù)據(jù)驅動是通過MVVM這種框架來實現(xiàn)的。MVVM框架主要包含3個部分:model、view和 viewModel。


Model:指的是數(shù)據(jù)部分,對應到前端就是javascript對象

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

ViewModel:就是連接視圖與數(shù)據(jù)的中間件

ViewModel是實現(xiàn)數(shù)據(jù)驅動視圖的核心,當數(shù)據(jù)變化的時候,ViewModel能夠監(jiān)聽到這種變化,并及時的通知view做出修改。同樣的,當頁面有事件觸發(fā)時,ViewModel也能夠監(jiān)聽到事件,并通知model進行響應。ViewModel就相當于一個觀察者,監(jiān)控著雙方的動作,并及時通知對方進行相應的操作。


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


同時每一個實例對象都有一個watcher實例對象,他會在模板編譯的過程中,用getter去訪問data的屬性,watcher此時就會把用到的data屬性記為依賴,這樣就建立了視圖與數(shù)據(jù)之間的聯(lián)系。當之后我們渲染視圖的數(shù)據(jù)依賴發(fā)生改變(即數(shù)據(jù)的setter被調用)的時候,watcher會對比前后兩個的數(shù)值是否發(fā)生變化,然后確定是否通知視圖進行重新渲染。這樣就實現(xiàn)了所謂的數(shù)據(jù)對于視圖的驅動。


react的數(shù)據(jù)驅動視圖

首先了解一些列內容:


pending:當前所有等待更新的state隊列。

isBatchingUpdates:React中用于標識當前是否處理批量更新狀態(tài),默認false。

dirtyComponent:當前所有待更新state的組件隊列。

React通過setState實現(xiàn)數(shù)據(jù)驅動視圖,通過setState來引發(fā)一次組件的更新過程從而實現(xiàn)頁面的重新渲染(除非shouldComponentUpdate返回false)。


setState()首先將接收的第一個參數(shù)state存儲在pending隊列中;(state)

判斷當前React是否處于批量更新狀態(tài),是的話就將需要更新state的組件添加到dirtyComponents中;(組件)

不是的話,它會遍歷dirtyComponents的所有組件,調用updateComponent方法更新每個dirty組件(開啟批量更新事務)


作者:let_code

原文:https://juejin.cn/post/7144648542472044558



作者:let_code


歡迎關注微信公眾號 :深圳灣碼農