React前端框架實(shí)現(xiàn)原理的理解
vdom
react 和 vue 都是基于 vdom 的前端框架,我們先聊下 vdom:
為什么 react 和 vue 都要基于 vdom 呢?直接操作真實(shí) dom 不行么?
考慮下這樣的場(chǎng)景:
渲染就是用 dom api 對(duì)真實(shí) dom 做增刪改,如果已經(jīng)渲染了一個(gè) dom,后來(lái)要更新,那就要遍歷它所有的屬性,重新設(shè)置,比如 id、clasName、onclick 等。
而 dom 的屬性是很多的:
有很多屬性根本用不到,但在更新時(shí)卻要跟著重新設(shè)置一遍。
能不能只對(duì)比我們關(guān)心的屬性呢?
把這些單獨(dú)摘出來(lái)用 JS 對(duì)象表示不就行了?
這就是為什么要有 vdom,是它的第一個(gè)好處。
而且有了 vdom 之后,就沒(méi)有和 dom 強(qiáng)綁定了,可以渲染到別的平臺(tái),比如 native、canvas 等等。
這是 vdom 的第二個(gè)好處。
我們知道了 vdom 就是用 JS 對(duì)象表示最終渲染的 dom 的,比如:
{ type: 'div', props: { id: 'aaa', className: ['bbb', 'ccc'], onClick: function() {} }, children: [] }
然后用渲染器把它渲染出來(lái)。
但是要讓開(kāi)發(fā)去寫(xiě)這樣的 vdom 么?
那肯定不行,這樣太麻煩了,大家熟悉的是 html 那種方式,所以我們要引入編譯的手段。
dsl 的編譯
dsl 是 domain specific language,領(lǐng)域特定語(yǔ)言的意思,html、css 都是 web 領(lǐng)域的 dsl。
直接寫(xiě) vdom 太麻煩了,所以前端框架都會(huì)設(shè)計(jì)一套 dsl,然后編譯成 render function,執(zhí)行后產(chǎn)生 vdom。
vue 和 react 都是這樣:
這套 dsl 怎么設(shè)計(jì)呢?
前端領(lǐng)域大家熟悉的描述 dom 的方式是 html,最好的方式自然是也設(shè)計(jì)成那樣。
所以 vue 的 template,react 的 jsx 就都是這么設(shè)計(jì)的。
vue 的 template compiler 是自己實(shí)現(xiàn)的,而 react 的 jsx 的編譯器是 babel 實(shí)現(xiàn)
編譯成 render function 后再執(zhí)行就是我們需要的 vdom。
接下來(lái)渲染器把它渲染出來(lái)就行了。
那渲染器怎么渲染 vdom 的呢?
渲染 vdom
渲染 vdom 也就是通過(guò) dom api 增刪改 dom。
比如一個(gè) div,那就要 document.createElement 創(chuàng)建元素,然后 setAttribute 設(shè)置屬性,addEventListener 設(shè)置事件監(jiān)聽(tīng)器。
如果是文本,那就要 document.createTextNode 來(lái)創(chuàng)建。
所以說(shuō)根據(jù) vdom 類(lèi)型的不同,寫(xiě)個(gè) if else,分別做不同的處理就行了。
沒(méi)錯(cuò),不管 vue 還是 react,渲染器里這段 if else 是少不了的:
switch (vdom.tag) { case HostComponent: // 創(chuàng)建或更新 dom case HostText: // 創(chuàng)建或更新 dom case FunctionComponent: // 創(chuàng)建或更新 dom case ClassComponent: // 創(chuàng)建或更新 dom }
react 里是通過(guò) tag 來(lái)區(qū)分 vdom 類(lèi)型的,比如 HostComponent 就是元素,HostText 就是文本,F(xiàn)unctionComponent、ClassComponent 就分別是函數(shù)組件和類(lèi)組件。
那么問(wèn)題來(lái)了,組件怎么渲染呢?
這就涉及到組件的原理了:
組件
我們的目標(biāo)是通過(guò) vdom 描述界面,在 react 里會(huì)使用 jsx。
這樣的 jsx 有的時(shí)候是基于 state 來(lái)動(dòng)態(tài)生成的。如何把 state 和 jsx 關(guān)聯(lián)起來(lái)呢?
封裝成 function、class 或者 option 對(duì)象的形式。然后在渲染的時(shí)候執(zhí)行它們拿到 vdom 就行了。
這就是組件的實(shí)現(xiàn)原理:
switch (vdom.tag) { case FunctionComponent: const childVdom = vdom.type(props); render(childVdom); //... case ClassComponent: const instance = new vdom.type(props); const childVdom = instance.render(); render(childVdom); //... }
如果是函數(shù)組件,那就傳入 props 執(zhí)行它,拿到 vdom 之后再遞歸渲染。
如果是 class 組件,那就創(chuàng)建它的實(shí)例對(duì)象,調(diào)用 render 方法拿到 vdom,然后遞歸渲染。
所以,大家猜到 vue 的 option 對(duì)象的組件描述方式怎么渲染了么?
{ data: {}, props: {} render(h) { return h('div', {}, ''); } }
沒(méi)錯(cuò),就是執(zhí)行下 render 方法就行:
const childVdom = option.render(); render(childVdom);
大家可能平時(shí)會(huì)寫(xiě)單文件組件 sfc 的形式,那個(gè)會(huì)有專(zhuān)門(mén)的編譯器,把 template 編譯成 render function,然后掛到 option 對(duì)象的 render 方法上:
所以組件本質(zhì)上只是對(duì)產(chǎn)生 vdom 的邏輯的封裝,函數(shù)的形式、option 對(duì)象的形式、class 的形式都可以。
就像 vue3 也有了函數(shù)組件一樣,組件的形式并不重要。
基于 vdom 的前端框架渲染流程都差不多,vue 和 react 很多方面是一樣的。但是管理狀態(tài)的方式不一樣,vue 有響應(yīng)式,而 react 則是 setState 的 api 的方式。
真說(shuō)起來(lái),vue 和 react 最大的區(qū)別就是狀態(tài)管理方式的區(qū)別,因?yàn)檫@個(gè)區(qū)別導(dǎo)致了后面架構(gòu)演變方向的不同。
狀態(tài)管理
react 是通過(guò) setState 的 api 觸發(fā)狀態(tài)更新的,更新以后就重新渲染整個(gè) vdom。
而 vue 是通過(guò)對(duì)狀態(tài)做代理,get 的時(shí)候收集以來(lái),然后修改狀態(tài)的時(shí)候就可以觸發(fā)對(duì)應(yīng)組件的 render 了。
有的同學(xué)可能會(huì)問(wèn),為什么 react 不直接渲染對(duì)應(yīng)組件呢?
想象一下這個(gè)場(chǎng)景:
父組件把它的 setState 函數(shù)傳遞給子組件,子組件調(diào)用了它。
這時(shí)候更新是子組件觸發(fā)的,但是要渲染的就只有那個(gè)組件么?
明顯不是,還有它的父組件。
同理,某個(gè)組件更新實(shí)際上可能觸發(fā)任意位置的其他組件更新的。
所以必須重新渲染整個(gè) vdom 才行。
那 vue 為啥可以做到精準(zhǔn)的更新變化的組件呢?
因?yàn)轫憫?yīng)式的代理呀,不管是子組件、父組件、還是其他位置的組件,只要用到了對(duì)應(yīng)的狀態(tài),那就會(huì)被作為依賴(lài)收集起來(lái),狀態(tài)變化的時(shí)候就可以觸發(fā)它們的 render,不管是組件是在哪里的。
這就是為什么 react 需要重新渲染整個(gè) vdom,而 vue 不用。
這個(gè)問(wèn)題也導(dǎo)致了后來(lái)兩者架構(gòu)上逐漸有了差異。
react 架構(gòu)的演變
react15 的時(shí)候,和 vue 的渲染流程還是很像的,都是遞歸渲染 vdom,增刪改 dom 就行。
但是因?yàn)闋顟B(tài)管理方式的差異逐漸導(dǎo)致了架構(gòu)的差異。
react 的 setState 會(huì)渲染整個(gè) vdom,而一個(gè)應(yīng)用的所有 vdom 可能是很龐大的,計(jì)算量就可能很大。
瀏覽器里 js 計(jì)算時(shí)間太長(zhǎng)是會(huì)阻塞渲染的,會(huì)占用每一幀的動(dòng)畫(huà)、重繪重排的時(shí)間,這樣動(dòng)畫(huà)就會(huì)卡頓。
作為一個(gè)有追求的前端框架,動(dòng)畫(huà)卡頓肯定是不行的。但是因?yàn)?setState 的方式只能渲染整個(gè) vdom,所以計(jì)算量大是不可避免的。
那能不能把計(jì)算量拆分一下,每一幀計(jì)算一部分,不要阻塞動(dòng)畫(huà)的渲染呢?
順著這個(gè)思路,react 就改造為了 fiber 架構(gòu)。
fiber 架構(gòu)
優(yōu)化的目標(biāo)是打斷計(jì)算,分多次進(jìn)行,但現(xiàn)在遞歸的渲染是不能打斷的,有兩個(gè)方面的原因?qū)е碌模?/p>
- 渲染的時(shí)候直接就操作了 dom 了,這時(shí)候打斷了,那已經(jīng)更新到 dom 的那部分怎么辦?
- 現(xiàn)在是直接渲染的 vdom,而 vdom 里只有 children 的信息,如果打斷了,怎么找到它的父節(jié)點(diǎn)呢?
第一個(gè)問(wèn)題的解決還是容易想到的:
渲染的時(shí)候不要直接更新到 dom 了,只找到變化的部分,打個(gè)增刪改的標(biāo)記,創(chuàng)建好 dom,等全部計(jì)算完了一次性更新到 dom 就好了。
所以 react 把渲染流程分為了兩部分: render 和 commit。
render 階段會(huì)找到 vdom 中變化的部分,創(chuàng)建 dom,打上增刪改的標(biāo)記,這個(gè)叫做 reconcile,調(diào)和。
reconcile 是可以打斷的,由 schedule 調(diào)度。
之后全部計(jì)算完了,就一次性更新到 dom,叫做 commit。
這樣,react 就把之前的和 vue 很像的遞歸渲染,改造成了 render(reconcile + schdule) + commit 兩個(gè)階段的渲染。
從此以后,react 和 vue 架構(gòu)上的差異才大了起來(lái)。
第二個(gè)問(wèn)題,如何打斷以后還能找到父節(jié)點(diǎn)、其他兄弟節(jié)點(diǎn)呢?
現(xiàn)有的 vdom 是不行的,需要再記錄下 parent、silbing 的信息。所以 react 創(chuàng)造了 fiber 的數(shù)據(jù)結(jié)構(gòu)。
除了 children 信息外,額外多了 sibling、return,分別記錄著兄弟節(jié)點(diǎn)、父節(jié)點(diǎn)的信息。
這個(gè)數(shù)據(jù)結(jié)構(gòu)也叫做 fiber。(fiber 既是一種數(shù)據(jù)結(jié)構(gòu),也代表 render + commit 的渲染流程)
react 會(huì)先把 vdom 轉(zhuǎn)換成 fiber,再去進(jìn)行 reconcile,這樣就是可打斷的了。
為什么這樣就可以打斷了呢?
因?yàn)楝F(xiàn)在不再是遞歸,而是循環(huán)了:
function workLoop() { while (wip) { performUnitOfWork(); } if (!wip && wipRoot) { commitRoot(); } }
react 里有一個(gè) workLoop 循環(huán),每次循環(huán)做一個(gè) fiber 的 reconcile,當(dāng)前處理的 fiber 會(huì)放在 workInProgress 這個(gè)全局變量上。
當(dāng)循環(huán)完了,也就是 wip 為空了,那就執(zhí)行 commit 階段,把 reconcile 的結(jié)果更新到 dom。
每個(gè) fiber 的 reconcile 是根據(jù)類(lèi)型來(lái)做的不同處理。當(dāng)處理完了當(dāng)前 fiber 節(jié)點(diǎn),就把 wip 指向 sibling、return 來(lái)切到下個(gè) fiber 節(jié)點(diǎn)。:
function performUnitOfWork() { const { tag } = wip; switch (tag) { case HostComponent: updateHostComponent(wip); break; case FunctionComponent: updateFunctionComponent(wip); break; case ClassComponent: updateClassComponent(wip); break; case Fragment: updateFragmentComponent(wip); break; case HostText: updateHostTextComponent(wip); break; default: break; } if (wip.child) { wip = wip.child; return; } let next = wip; while (next) { if (next.sibling) { wip = next.sibling; return; } next = next.return; } wip = null; }
函數(shù)組件和 class 組件的 reconcile 和之前講的一樣,就是調(diào)用 render 拿到 vdom,然后繼續(xù)處理渲染出的 vdom:
function updateClassComponent(wip) { const { type, props } = wip; const instance = new type(props); const children = instance.render(); reconcileChildren(wip, children); } function updateFunctionComponent(wip) { renderWithHooks(wip); const { type, props } = wip; const children = type(props); reconcileChildren(wip, children); }
循環(huán)執(zhí)行 reconcile,那每次處理之前判斷一下是不是有更高優(yōu)先級(jí)的任務(wù),就能實(shí)現(xiàn)打斷了。
所以我們?cè)诿看翁幚?fiber 節(jié)點(diǎn)的 reconcile 之前,都先調(diào)用下 shouldYield 方法:
function workLoop() { while (wip && shouldYield()) { performUnitOfWork(); } if (!wip && wipRoot) { commitRoot(); } }
shouldYiled 方法就是判斷待處理的任務(wù)隊(duì)列有沒(méi)有優(yōu)先級(jí)更高的任務(wù),有的話就先處理那邊的 fiber,這邊的先暫停一下。
這就是 fiber 架構(gòu)的 reconcile 可以打斷的原理。通過(guò) fiber 的數(shù)據(jù)結(jié)構(gòu),加上循環(huán)處理前每次判斷下是否打斷來(lái)實(shí)現(xiàn)的。
聊完了 render 階段(reconcile + schedule),接下來(lái)就進(jìn)入 commit 階段了。
前面說(shuō)過(guò),為了變?yōu)榭纱驍嗟?,reconcile 階段并不會(huì)真正操作 dom,只會(huì)創(chuàng)建 dom 然后打個(gè) effectTag 的增刪改標(biāo)記。
commit 階段就根據(jù)標(biāo)記來(lái)更新 dom 就可以了。
但是 commit 階段要再遍歷一次 fiber 來(lái)查找有 effectTag 的節(jié)點(diǎn),更新 dom 么?
這樣當(dāng)然沒(méi)問(wèn)題,但沒(méi)必要。完全可以在 reconcile 的時(shí)候把有 effectTag 的節(jié)點(diǎn)收集到一個(gè)隊(duì)列里,然后 commit 階段直接遍歷這個(gè)隊(duì)列就行了。
這個(gè)隊(duì)列叫做 effectList。
react 會(huì)在 commit 階段遍歷 effectList,根據(jù) effectTag 來(lái)增刪改 dom。
dom 創(chuàng)建前后就是 useEffect、useLayoutEffect 還有一些函數(shù)組件的生命周期函數(shù)執(zhí)行的時(shí)候。
useEffect 被設(shè)計(jì)成了在 dom 操作前異步調(diào)用,useLayoutEffect 是在 dom 操作后同步調(diào)用。
為什么這樣呢?
因?yàn)槎家僮?dom 了,這時(shí)候如果來(lái)了個(gè) effect 同步執(zhí)行,計(jì)算量很大,那不是把 fiber 架構(gòu)帶來(lái)的優(yōu)勢(shì)有毀了么?
所以 effect 是異步的,不會(huì)阻塞渲染。
而 useLayoutEffect,顧名思義是想在這個(gè)階段拿到一些布局信息的,dom 操作完以后就可以了,而且都渲染完了,自然也就可以同步調(diào)用了。
實(shí)際上 react 把 commit 階段也分成了 3 個(gè)小階段。
before mutation、mutation、layout。
mutation 就是遍歷 effectList 來(lái)更新 dom 的。
它的之前就是 before mutation,會(huì)異步調(diào)度 useEffect 的回調(diào)函數(shù)。
它之后就是 layout 階段了,因?yàn)檫@個(gè)階段已經(jīng)可以拿到布局信息了,會(huì)同步調(diào)用 useLayoutEffect 的回調(diào)函數(shù)。而且這個(gè)階段可以拿到新的 dom 節(jié)點(diǎn),還會(huì)更新下 ref。
至此,我們對(duì) react 的新架構(gòu),render、commit 兩大階段都干了什么就理清了。
總結(jié)
react 和 vue 都是基于 vdom 的前端框架,之所以用 vdom 是因?yàn)榭梢跃珳?zhǔn)的對(duì)比關(guān)心的屬性,而且還可以跨平臺(tái)渲染。
但是開(kāi)發(fā)不會(huì)直接寫(xiě) vdom,而是通過(guò) jsx 這種接近 html 語(yǔ)法的 DSL,編譯產(chǎn)生 render function,執(zhí)行后產(chǎn)生 vdom。
vdom 的渲染就是根據(jù)不同的類(lèi)型來(lái)用不同的 dom api 來(lái)操作 dom。
渲染組件的時(shí)候,如果是函數(shù)組件,就執(zhí)行它拿到 vdom。class 組件就創(chuàng)建實(shí)例然后調(diào)用 render 方法拿到 vdom。vue 的那種 option 對(duì)象的話,就調(diào)用 render 方法拿到 vdom。
組件本質(zhì)上就是對(duì)一段 vdom 產(chǎn)生邏輯的封裝,函數(shù)、class、option 對(duì)象甚至其他形式都可以。
react 和 vue 最大的區(qū)別在狀態(tài)管理方式上,vue 是通過(guò)響應(yīng)式,react 是通過(guò) setState 的 api。我覺(jué)得這個(gè)是最大的區(qū)別,因?yàn)樗鼘?dǎo)致了后面 react 架構(gòu)的變更。
react 的 setState 的方式,導(dǎo)致它并不知道哪些組件變了,需要渲染整個(gè) vdom 才行。但是這樣計(jì)算量又會(huì)比較大,會(huì)阻塞渲染,導(dǎo)致動(dòng)畫(huà)卡頓。
所以 react 后來(lái)改造成了 fiber 架構(gòu),目標(biāo)是可打斷的計(jì)算。
為了這個(gè)目標(biāo),不能變對(duì)比變更新 dom 了,所以把渲染分為了 render 和 commit 兩個(gè)階段,render 階段通過(guò) schedule 調(diào)度來(lái)進(jìn)行 reconcile,也就是找到變化的部分,創(chuàng)建 dom,打上增刪改的 tag,等全部計(jì)算完之后,commit 階段一次性更新到 dom。
打斷之后要找到父節(jié)點(diǎn)、兄弟節(jié)點(diǎn),所以 vdom 也被改造成了 fiber 的數(shù)據(jù)結(jié)構(gòu),有了 parent、sibling 的信息。
所以 fiber 既指這種鏈表的數(shù)據(jù)結(jié)構(gòu),又指這個(gè) render、commit 的流程。
reconcile 階段每次處理一個(gè) fiber 節(jié)點(diǎn),處理前會(huì)判斷下 shouldYield,如果有更高優(yōu)先級(jí)的任務(wù),那就先執(zhí)行別的。
commit 階段不用再次遍歷 fiber 樹(shù),為了優(yōu)化,react 把有 effectTag 的 fiber 都放到了 effectList 隊(duì)列中,遍歷更新即可。
在dom 操作前,會(huì)異步調(diào)用 useEffect 的回調(diào)函數(shù),異步是因?yàn)椴荒茏枞秩尽?/p>
在 dom 操作之后,會(huì)同步調(diào)用 useLayoutEffect 的回調(diào)函數(shù),并且更新 ref。
所以,commit 階段又分成了 before mutation、mutation、layout 這三個(gè)小階段,就對(duì)應(yīng)上面說(shuō)的那三部分。
我覺(jué)得理解了 vdom、jsx、組件本質(zhì)、fiber、render(reconcile + schedule) + commit(before mutation、mutation、layout)的渲染流程,就算是對(duì) react 原理有一個(gè)比較深的理解了。
以上就是React前端框架實(shí)現(xiàn)原理的理解的詳細(xì)內(nèi)容,更多關(guān)于React前端框架實(shí)現(xiàn)原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react-redux多個(gè)組件數(shù)據(jù)共享的方法
這篇文章主要介紹了react-redux多個(gè)組件數(shù)據(jù)共享的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08Rect Intersection判斷兩個(gè)矩形是否相交
這篇文章主要為大家介紹了Rect Intersection判斷兩個(gè)矩形是否相交的算法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06React函數(shù)組件與類(lèi)組件使用及優(yōu)劣對(duì)比
本文主要介紹了React函數(shù)組件與類(lèi)組件使用及優(yōu)劣對(duì)比,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04詳解在React項(xiàng)目中如何集成和使用web worker
在復(fù)雜的React應(yīng)用中,某些計(jì)算密集型或耗時(shí)操作可能會(huì)阻塞主線程,導(dǎo)致用戶(hù)界面出現(xiàn)卡頓或響應(yīng)慢的現(xiàn)象,為了優(yōu)化用戶(hù)體驗(yàn),可以采用Web Worker來(lái)在后臺(tái)線程中執(zhí)行這些操作,本文將詳細(xì)介紹在React項(xiàng)目中如何集成和使用Web Worker來(lái)改善應(yīng)用性能,需要的朋友可以參考下2023-12-12