當(dāng)面試官讓我回答React和Vue框架的區(qū)別
Vue 和 React 作為當(dāng)前前端兩大火熱的框架,面試的時(shí)候自然不少被提及:
請(qǐng)說(shuō)一下你對(duì)react/vue框架的理解
請(qǐng)對(duì)比一下兩大框架的優(yōu)缺點(diǎn)
其實(shí)react和vue大體上是相同的,比如都使用虛擬DOM高效的更新視圖,都提倡組件化,都實(shí)現(xiàn)了數(shù)據(jù)驅(qū)動(dòng)視圖,都使用diff算法,也都對(duì)diff算法進(jìn)行了優(yōu)化,都有router庫(kù)實(shí)現(xiàn)url到組件的映射,都有狀態(tài)管理等等.....
但是在具體實(shí)現(xiàn)上又不盡相同,接下來(lái)就從組件化,虛擬DOM以及數(shù)據(jù)驅(qū)動(dòng)視圖三個(gè)方面對(duì)比下vue和react框架的相同和不同之處。
1.對(duì)于組件化的理解,組件化帶來(lái)的好處
組件是獨(dú)立和可復(fù)用的代碼組織單元,它使開發(fā)者使用小型、獨(dú)立和通??蓮?fù)用的組件構(gòu)建大型應(yīng)用;
組件化開發(fā)能大幅提高應(yīng)用開發(fā)效率、測(cè)試性、復(fù)用性等;
降低整個(gè)系統(tǒng)的耦合度,在保持接口不變的情況下,我們可以替換不同的組件快速完成需求,例如輸入框,可以替換為日歷、時(shí)間、范圍等組件作具體的實(shí)現(xiàn)
調(diào)試方便,由于整個(gè)系統(tǒng)是通過(guò)組件組合起來(lái)的,在出現(xiàn)問(wèn)題的時(shí)候,可以用排除法直接移除組件,或者根據(jù)報(bào)錯(cuò)的組件快速定位問(wèn)題,之所以能夠快速定位,是因?yàn)槊總€(gè)組件之間低耦合,職責(zé)單一,所以邏輯會(huì)比分析整個(gè)系統(tǒng)要簡(jiǎn)單
提高可維護(hù)性,由于每個(gè)組件的職責(zé)單一,并且組件在系統(tǒng)中是被復(fù)用的,所以對(duì)代碼進(jìn)行優(yōu)化可獲得系統(tǒng)的整體升級(jí)
react和vue中組件化的相同點(diǎn)
react和vue都推崇組件化,通過(guò)將頁(yè)面拆分成一個(gè)一個(gè)小的可復(fù)用單元來(lái)提高代碼的復(fù)用率和開發(fā)效率。在開發(fā)時(shí)react和vue有相同的套路,比如都有父子組件傳參,都有數(shù)據(jù)狀態(tài)管理,都有前端路由等。
react和vue組件化的差異
React推薦的做法是JSX + inline style, 也就是把 HTML 和 CSS 全都寫進(jìn) JavaScript 中,即 all in js;
Vue 推薦的做法是 template 的單文件組件格式(簡(jiǎn)單易懂,從傳統(tǒng)前端轉(zhuǎn)過(guò)來(lái)易于理解),即 html,css,JS 寫在同一個(gè)文件(vue也支持JSX寫法)
2.虛擬DOM
什么是虛擬DOM
虛擬 DOM(Virtual DOM)本質(zhì)上是JS 和 DOM 之間的一個(gè)映射緩存,它在形態(tài)上表現(xiàn)為一個(gè)能夠描述 DOM 結(jié)構(gòu)及其屬性信息的 JS 對(duì)象。它主要存儲(chǔ)在內(nèi)存中。主要來(lái)說(shuō):
虛擬dom是一個(gè)js對(duì)象,存儲(chǔ)在內(nèi)存之中。
虛擬dom能夠描述真實(shí)dom(存在一個(gè)對(duì)應(yīng)關(guān)系)
當(dāng)數(shù)據(jù)變化的時(shí)候,生成新的DOM,對(duì)比新舊虛擬DOM的差異,將差異更新到真實(shí)DOM上
虛擬DOM的優(yōu)點(diǎn)
減少 DOM 操作:虛擬 DOM 可以將多次 DOM 操作合并為一次操作
研發(fā)效率的問(wèn)題:虛擬 DOM 的出現(xiàn),為數(shù)據(jù)驅(qū)動(dòng)視圖這一思想提供了高度可用的載體,使得前端開發(fā)能夠基于函數(shù)式 UI 的編程方式實(shí)現(xiàn)高效的聲明式編程。
跨平臺(tái)的問(wèn)題:虛擬 DOM 是對(duì)真實(shí)渲染內(nèi)容的一層抽象。同一套虛擬 DOM,可以對(duì)接不同平臺(tái)的渲染邏輯,從而實(shí)現(xiàn)“一次編碼,多端運(yùn)行”
react和vue中虛擬DOM的相同點(diǎn)
Vue與React都使用了 Virtual DOM + Diff算法, 不管是Vue的Template模板+options api 寫法, 還是React的Class或者Function寫法,最后都是生成render函數(shù),而render函數(shù)執(zhí)行返回VNode(虛擬DOM的數(shù)據(jù)結(jié)構(gòu),本質(zhì)上是棵樹)。
當(dāng)每一次UI更新時(shí),總會(huì)根據(jù)render重新生成最新的VNode,然后跟以前緩存起來(lái)老的VNode進(jìn)行比對(duì),再使用Diff算法(框架核心)去真正更新真實(shí)DOM(虛擬DOM是JS對(duì)象結(jié)構(gòu),同樣在JS引擎中,而真實(shí)DOM在瀏覽器渲染引擎中,所以操作虛擬DOM比操作真實(shí)DOM開銷要小的多)
vue&&reactVirtualDOM.png
兩者對(duì)diff算法的優(yōu)化基本上思路是相同的:
tag不同認(rèn)為是不同節(jié)點(diǎn)
只比較同一層級(jí),不跨級(jí)比較
同一層級(jí)的節(jié)點(diǎn)用key唯一標(biāo)識(shí),tag和key都相同則認(rèn)為是同一節(jié)點(diǎn)
diff優(yōu)化.png
diff 算法源碼實(shí)現(xiàn)相同之處
在處理老節(jié)點(diǎn)部分,都需要把節(jié)點(diǎn)處理 key - value 的 Map 數(shù)據(jù)結(jié)構(gòu),方便在往后的比對(duì)中可以快速通過(guò)節(jié)點(diǎn)的 key 取到對(duì)應(yīng)的節(jié)點(diǎn)。同樣在比對(duì)兩個(gè)新老節(jié)點(diǎn)是否相同時(shí),key 是否相同也是非常重要的判斷標(biāo)準(zhǔn)。所以不同是 React, 還是 Vue,在寫動(dòng)態(tài)列表的時(shí)候,都需要設(shè)置一個(gè)唯一值 key,這樣在 diff 算法處理的時(shí)候性能才最大化。
react和vue中虛擬DOM的差別
react和vue的虛擬dom都是用js對(duì)象來(lái)模擬真實(shí)DOM,用虛擬DOM的diff來(lái)最小化更新真實(shí)DOM,可以減小不必要的性能損耗,按顆粒度分為不同的類型比較同層級(jí)dom節(jié)點(diǎn),進(jìn)行增、刪、移的操作。
按顆粒度分為tree diff, component diff, element diff. tree diff 比較同層級(jí)dom節(jié)點(diǎn),進(jìn)行增、刪、移操作。如果遇到component, 就會(huì)重新tree diff流程。
參考鏈接
dom的更新策略不同
react 會(huì)自頂向下全diff。vue會(huì)跟蹤每一個(gè)組件的依賴關(guān)系,不需要重新渲染整個(gè)組件樹。
在react中,當(dāng)狀態(tài)發(fā)生改變時(shí),組件樹就會(huì)自頂向下的全diff, 重新render頁(yè)面, 重新生成新的虛擬dom tree, 新舊dom tree進(jìn)行比較, 進(jìn)行patch打補(bǔ)丁方式,局部更新dom。所以react為了避免父組件更新而引起不必要的子組件更新, 可以在shouldComponentUpdate做邏輯判斷,減少?zèng)]必要的render, 以及重新生成虛擬dom,做差量對(duì)比過(guò)程。
在vue中, 通過(guò)Object.defineProperty 把 data 屬性全部轉(zhuǎn)為 getter/setter。同時(shí)watcher實(shí)例對(duì)象會(huì)在組件渲染時(shí),將屬性記錄為dep, 當(dāng)dep 項(xiàng)中的 setter被調(diào)用時(shí),通知watch重新計(jì)算,使得關(guān)聯(lián)組件更新。
Diff 算法借助元素的 Key 判斷元素是新增、刪除、修改,從而減少不必要的元素重渲染。
diff 算法源碼實(shí)現(xiàn)不同之處
react的diff
聲明newChildren就是即將更新的 JSX 對(duì)象
當(dāng)newChildren類型為object、number、string,代表同級(jí)只有一個(gè)節(jié)點(diǎn)
存在:DOM節(jié)點(diǎn)是否可以復(fù)用(
通過(guò)tag和key進(jìn)行判斷
)
不存在:新生成一個(gè)fiber節(jié)點(diǎn)并返回
可以:將上次更新的fiber節(jié)點(diǎn)副本作為本次新生成的fiber節(jié)點(diǎn)并返回
不可以:標(biāo)記當(dāng)前節(jié)點(diǎn)為待刪除節(jié)點(diǎn),新生成一個(gè)fiber節(jié)點(diǎn)并返回
檢查上次更新時(shí)的fiber節(jié)點(diǎn)是否存在對(duì)應(yīng)的DOM節(jié)點(diǎn)
當(dāng)newChildren類型為Array,同級(jí)有多個(gè)節(jié)點(diǎn),會(huì)進(jìn)行兩次遍歷:
newChildren沒(méi)遍歷完,oldFiber遍歷完:遍歷余下的newChildren依次進(jìn)行插入
newChildren遍歷完,oldFiber沒(méi)遍歷完:遍歷剩下的oldFiber依次進(jìn)行刪除
newChildren與oldFiber都沒(méi)遍歷完:這意味著有節(jié)點(diǎn)在這次更新中改變了位置。
reactDiff.png
reactDiffExcle.png
如果當(dāng)前節(jié)點(diǎn)在新集合中的位置比老集合中的位置靠前的話,是不會(huì)影響后續(xù)節(jié)點(diǎn)操作的,這里這時(shí)候節(jié)點(diǎn)不用動(dòng)
操作過(guò)程中只比較oldIndex和maxIndex,規(guī)則如下:
diff過(guò)程如下:
節(jié)點(diǎn)B:此時(shí) maxIndex=0,oldIndex=1;滿足 maxIndex< oldIndex,因此B節(jié)點(diǎn)不動(dòng),此時(shí)maxIndex= Math.max(oldIndex, maxIndex),就是1
節(jié)點(diǎn)A:此時(shí)maxIndex=1,oldIndex=0;不滿足maxIndex< oldIndex,因此A節(jié)點(diǎn)進(jìn)行移動(dòng)操作,此時(shí)maxIndex= Math.max(oldIndex, maxIndex),還是1
節(jié)點(diǎn)D:此時(shí)maxIndex=1, oldIndex=3;滿足maxIndex< oldIndex,因此D節(jié)點(diǎn)不動(dòng),此時(shí)maxIndex= Math.max(oldIndex, maxIndex),就是3
節(jié)點(diǎn)C:此時(shí)maxIndex=3,oldIndex=2;不滿足maxIndex< oldIndex,因此C節(jié)點(diǎn)進(jìn)行移動(dòng)操作,當(dāng)前已經(jīng)比較完了 當(dāng)ABCD節(jié)點(diǎn)比較完成后,diff過(guò)程還沒(méi)完,還會(huì)整體遍歷老集合中節(jié)點(diǎn),看有沒(méi)有沒(méi)用到的節(jié)點(diǎn),有的話,就刪除
當(dāng)oldIndex>maxIndex時(shí),將oldIndex的值賦值給maxIndex
當(dāng)oldIndex=maxIndex時(shí),不操作
當(dāng)oldIndex<maxIndex時(shí),將當(dāng)前節(jié)點(diǎn)移動(dòng)到index的位置
index:新集合的遍歷下標(biāo)。
oldIndex:當(dāng)前節(jié)點(diǎn)在老集合中的下標(biāo)
maxIndex:在新集合訪問(wèn)過(guò)的節(jié)點(diǎn)中,其在老集合的最大下標(biāo)
遍歷newChildren,i = 0,將newChildren[i]與oldFiber比較,判斷DOM節(jié)點(diǎn)是否可復(fù)用。
如果可復(fù)用,i++,比較newChildren[i]與oldFiber.sibling是否可復(fù)用??梢詮?fù)用則重復(fù)此步驟。
如果不可復(fù)用,立即跳出整個(gè)遍歷。
如果newChildren遍歷完或者oldFiber遍歷完(即oldFiber.sibling === null),跳出遍歷。
第一層遍歷:
由上述,第一次遍歷完可能存在以下2種情況: a. 若是因?yàn)?不可復(fù)"用導(dǎo)致的跳出遍歷:newChildren沒(méi)有遍歷完,oldFiber也沒(méi)有遍歷完。b. 若是因?yàn)?newChildren遍歷完或者oldFiber遍歷完"導(dǎo)致的跳出遍歷:可能newChildren遍歷完,或oldFiber遍歷完,或他們同時(shí)遍歷完。(帶著第一輪遍歷的結(jié)果去進(jìn)行第二輪的遍歷)
第二輪遍歷:第二輪遍歷的時(shí)候會(huì)將剩余未比較的老節(jié)點(diǎn)和剩余未比較的新節(jié)點(diǎn)進(jìn)行遍歷
更詳細(xì)的diff參考這里
vue的diff
patch函數(shù)會(huì)接受兩個(gè)參數(shù):oldVnode 和 vnode,其分別指舊的vnode和新的vnode
只有新節(jié)點(diǎn)
createElm 創(chuàng)建新的節(jié)點(diǎn)
只有舊節(jié)點(diǎn)
刪除舊節(jié)點(diǎn)
新舊節(jié)點(diǎn)都存在:通過(guò) sameVnode 判斷節(jié)點(diǎn)是否一樣:
Vnode 是文本節(jié)點(diǎn),則更新文本(文本節(jié)點(diǎn)不存在子節(jié)點(diǎn))
Vnode 有子節(jié)點(diǎn),則處理比較更新子節(jié)點(diǎn)
當(dāng)新Vnode.text 存在,而且和 舊 VNode.text 不一樣時(shí),直接更新這個(gè) DOM 的 文本內(nèi)容
新Vnode 的 text 為空,直接把 文本DOM 賦值給空
updateChildren 維持新舊節(jié)點(diǎn)首尾的四個(gè)指針進(jìn)行遍歷對(duì)比,遵循的原則是:能不移動(dòng),盡量不移動(dòng)。不行就移動(dòng),實(shí)在不行就新建
新舊節(jié)點(diǎn)都有子節(jié)點(diǎn),而且不一樣,那就執(zhí)行updateChildren
只有新子節(jié)點(diǎn):執(zhí)行創(chuàng)建
只有舊子節(jié)點(diǎn):執(zhí)行刪除
一樣:直接調(diào)用 patchVnode 去處理這兩個(gè)節(jié)點(diǎn)
不一樣:直接創(chuàng)建新節(jié)點(diǎn),刪除舊節(jié)點(diǎn)
為什么react不使用雙指針提升比較效率
react在源碼中注釋道:React 不能通過(guò)雙端對(duì)比進(jìn)行 Diff 算法優(yōu)化是因?yàn)槟壳?Fiber 上沒(méi)有設(shè)置反向鏈表,而且想知道就目前這種方案能持續(xù)多久,如果目前這種模式不理想的話,那么也可以增加雙端對(duì)比算法。
也就是說(shuō)雖然更新的JSX對(duì)象即newChildren為數(shù)組形式,但是和newChildren中每個(gè)值進(jìn)行比較的是上次更新的Fiber節(jié)點(diǎn),F(xiàn)iber節(jié)點(diǎn)的同級(jí)節(jié)點(diǎn)是由sibling指針鏈接形成的鏈表。
即 newChildren[0]與oldFiber比較,newChildren[1]與oldFiber.sibling比較。
單鏈表無(wú)法使用雙指針,所以無(wú)法對(duì)算法使用雙指針優(yōu)化。
基于以上原因,Diff算法的整體邏輯會(huì)經(jīng)歷兩輪遍歷:
第一輪遍歷:處理更新的節(jié)點(diǎn)。
第二輪遍歷:處理剩下的不屬于更新的節(jié)點(diǎn)(新增、刪除、移動(dòng))。
總結(jié)
Vue2的核心Diff算法采用了雙端比較的算法,同時(shí)從新舊children的兩端開始進(jìn)行比較,借助key值找到可復(fù)用的節(jié)點(diǎn),再進(jìn)行相關(guān)操作。相比React的Diff算法,同樣情況下可以減少移動(dòng)節(jié)點(diǎn)次數(shù),減少不必要的性能損耗,更加的優(yōu)雅。
3.數(shù)據(jù)驅(qū)動(dòng)視圖
數(shù)據(jù)驅(qū)動(dòng)視圖:就是數(shù)據(jù)變化的時(shí)候,相應(yīng)的視圖會(huì)得到更新。開發(fā)者只需要關(guān)注數(shù)據(jù)的變化而不用再去手動(dòng)的操作DOM。
vue中的數(shù)據(jù)驅(qū)動(dòng)視圖
Vuejs的數(shù)據(jù)驅(qū)動(dòng)是通過(guò)MVVM這種框架來(lái)實(shí)現(xiàn)的。MVVM框架主要包含3個(gè)部分:model、view和 viewModel。
Model:指的是數(shù)據(jù)部分,對(duì)應(yīng)到前端就是javascript對(duì)象
View:指的是視圖部分,對(duì)應(yīng)前端就是dom
ViewModel:就是連接視圖與數(shù)據(jù)的中間件
ViewModel是實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)視圖的核心,當(dāng)數(shù)據(jù)變化的時(shí)候,ViewModel能夠監(jiān)聽到這種變化,并及時(shí)的通知view做出修改。同樣的,當(dāng)頁(yè)面有事件觸發(fā)時(shí),ViewModel也能夠監(jiān)聽到事件,并通知model進(jìn)行響應(yīng)。ViewModel就相當(dāng)于一個(gè)觀察者,監(jiān)控著雙方的動(dòng)作,并及時(shí)通知對(duì)方進(jìn)行相應(yīng)的操作。
首先,vuejs在實(shí)例化的過(guò)程中,會(huì)對(duì)遍歷傳給實(shí)例化對(duì)象選項(xiàng)中的data 選項(xiàng),遍歷其所有屬性并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。
同時(shí)每一個(gè)實(shí)例對(duì)象都有一個(gè)watcher實(shí)例對(duì)象,他會(huì)在模板編譯的過(guò)程中,用getter去訪問(wèn)data的屬性,watcher此時(shí)就會(huì)把用到的data屬性記為依賴,這樣就建立了視圖與數(shù)據(jù)之間的聯(lián)系。當(dāng)之后我們渲染視圖的數(shù)據(jù)依賴發(fā)生改變(即數(shù)據(jù)的setter被調(diào)用)的時(shí)候,watcher會(huì)對(duì)比前后兩個(gè)的數(shù)值是否發(fā)生變化,然后確定是否通知視圖進(jìn)行重新渲染。這樣就實(shí)現(xiàn)了所謂的數(shù)據(jù)對(duì)于視圖的驅(qū)動(dòng)。
react的數(shù)據(jù)驅(qū)動(dòng)視圖
首先了解一些列內(nèi)容:
pending:當(dāng)前所有等待更新的state隊(duì)列。
isBatchingUpdates:React中用于標(biāo)識(shí)當(dāng)前是否處理批量更新狀態(tài),默認(rèn)false。
dirtyComponent:當(dāng)前所有待更新state的組件隊(duì)列。
React通過(guò)setState實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)視圖,通過(guò)setState來(lái)引發(fā)一次組件的更新過(guò)程從而實(shí)現(xiàn)頁(yè)面的重新渲染(除非shouldComponentUpdate返回false)。
setState()首先將接收的第一個(gè)參數(shù)state存儲(chǔ)在pending隊(duì)列中;(state)
判斷當(dāng)前React是否處于批量更新狀態(tài),是的話就將需要更新state的組件添加到dirtyComponents中;(組件)
不是的話,它會(huì)遍歷dirtyComponents的所有組件,調(diào)用updateComponent方法更新每個(gè)dirty組件(開啟批量更新事務(wù))
作者:let_code
原文:https://juejin.cn/post/7144648542472044558
作者:let_code
歡迎關(guān)注微信公眾號(hào) :深圳灣碼農(nóng)