国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

圖文并茂,一文讀懂 React 組件渲染核心原理

共 12594字,需瀏覽 26分鐘

 ·

2021-10-30 20:33

這是我們團(tuán)隊楊勁松、楊杰強同學(xué)做的內(nèi)部分享,文章從聲明式渲染的基本原理開始,逐步深入講解 React 渲染與節(jié)點掛載的基本流程與源碼,適合初中階同學(xué)閱讀。

PS:我們是字節(jié)游戲中臺前端團(tuán)隊,日常學(xué)習(xí)氛圍濃厚,目前還有大量 HC,歡迎自薦。


引言

相信大家對 React 都已經(jīng)非常熟悉了,像 React,Vue 這樣的現(xiàn)代前端框架已經(jīng)是我們?nèi)粘i_發(fā)離不開的工具了,這篇文章主要是從源碼的角度剖析 React 的核心渲染原理。我們將從用戶編寫的組件代碼開始,一步一步分析 React 是如何將它們變成真實 DOM ,這個過程主要可以分成兩個階段:render 階段和 commit 階段。文章的核心內(nèi)容也正是對這兩個階段的分析。

一、前置知識

聲明式渲染

  • 『聲明式渲染』,顧名思義,就是讓使用者只需要「聲明或描述」我需要渲染的東西是什么,然后就把具體的渲染工作交給機器去做,與之相對的是『命令式渲染』。
  • 『命令式渲染』則是由用戶去一步一步地命令機器下一步該怎么做。

舉個簡單的例子:

如果我們需要在網(wǎng)頁上渲染一個有三個節(jié)點的列表,命令式的做法是手動操作 dom,首先創(chuàng)建一個容器節(jié)點,再利用循環(huán)每次先創(chuàng)建一個新節(jié)點,填充內(nèi)容,然后將新節(jié)點新增到容器節(jié)點下,最后再將容器節(jié)點新增到 body 標(biāo)簽下:

const?list?=?[1,2,3];
const?container?=?document.createElement('div');
for?(let?i?=?0;?i?????const?newDom?=?document.createElement('div');
????newDom.innerHTML?=?list[i];
????container.appendChild(newDom);
}
document.body.appendChild(container);

而聲明式的做法應(yīng)該是:

const?list?=?[1,2,3];
const?container?=?document.createElement('div');

const?Demo?=?()?=>
(<div>
????{list.map((item)?=>?<div>{item}div>
)}
div>)

ReactDom.render(<Demo?/>,?container);

可以看到在這個例子中,聲明式寫法以 HTML 語法直接告訴機器,我需要的視圖應(yīng)該是長這個樣子,然后具體的 DOM 操作全部交由機器去完成。開發(fā)者只需要專注于業(yè)務(wù)邏輯的實現(xiàn)。

這便是聲明式渲染。

聲明式渲染是現(xiàn)代前端框架的比較普遍的設(shè)計思路。

JSX 和 ReactElement

相信大家最初學(xué) React 的時候都有這樣的疑問,為什么我們能夠以類似 HTML 的語法編寫組件,這個東西又是怎么轉(zhuǎn)換成 JavaScript 語法的?答案就是 Babel。根據(jù)官網(wǎng)介紹,這種語法被稱為 JSX,是一個 JavaScript 的語法擴(kuò)展。能夠被 Babel 編譯成 React.createElement 方法。舉個例子:

通過查閱源碼我們可以看到 「React.createElement」 方法

export?function?createElement(type,?config,?children)?{
??let?propName;

??//?Reserved?names?are?extracted
??const?props?=?{};

??let?key?=?null;
??let?ref?=?null;
??let?self?=?null;
??let?source?=?null;
??...
??return?ReactElement(
????type,
????key,
????ref,
????self,
????source,
????ReactCurrentOwner.current,
????props,
??);
}

const?ReactElement?=?function(type,?key,?ref,?self,?source,?owner,?props)?{
??const?element?=?{
????//?This?tag?allows?us?to?uniquely?identify?this?as?a?React?Element
????$typeof:?REACT_ELEMENT_TYPE,

????//?Built-in?properties?that?belong?on?the?element
????type:?type,
????key:?key,
????ref:?ref,
????props:?props,

????//?Record?the?component?responsible?for?creating?this?element.
????_owner:?owner,
??};
??...
??return?element;
}

可以看到 React 是使用了 element 這種結(jié)構(gòu)來代表一個節(jié)點,里面就只有簡單的 6 個字段。我們可以看個實際的例子,下面 Count 組件對應(yīng)的 element 數(shù)據(jù)結(jié)構(gòu):

function?Count({count,?onCountClick})?{
??return?<div?onClick={()?=>?{?onCountClick()}}>
??count:?{count}
??div>

}


可以看到,element 結(jié)構(gòu)只能反映出 jsx 節(jié)點的層級結(jié)構(gòu),而組件里的各種狀態(tài)或者返回 jsx 等都是不會記錄在 element 中。

目前我們知道,我們編寫的 jsx 會首先被處理成 element 結(jié)構(gòu)。

jsx -> element

那 React 又是如何處理 element 的,如剛剛說的,element 里包含的信息太少,只靠 element 顯然是不足以映射到所有真實 DOM 的,因此我們還需要更精細(xì)的結(jié)構(gòu)。

Fiber 樹結(jié)構(gòu)

Fiber 這個單詞相信大家多多少少都有聽過,它是在 React 16 被引入,關(guān)于 Fiber 如何實現(xiàn)任務(wù)調(diào)度在這篇文章不會涉及,但是 Fiber 的引入不僅僅帶來了任務(wù)調(diào)度方面的能力,整個 React 實現(xiàn)架構(gòu)也因此重構(gòu)了一遍,而我們之前經(jīng)常提到的虛擬 DOM 樹在新的 React 架構(gòu)下被稱為 Fiber 樹,上面提到的每個 element 都有一個所屬的 Fiber。

首先我們先看看源碼中 Fiber 的構(gòu)造函數(shù):

function?FiberNode(
??tag:?WorkTag,
??pendingProps:?mixed,
??key:?null?|?string,
??mode:?TypeOfMode,
)?
{
??//?Instance
??this.tag?=?tag;????????????//?標(biāo)識節(jié)點類型,例如函數(shù)組件、類組件、普通標(biāo)簽等
??this.key?=?key;
??this.elementType?=?null;??//?標(biāo)識具體?jsx?標(biāo)簽名
??this.type?=?null;????????//?類似?elementType
??this.stateNode?=?null;??//?對應(yīng)的真實?DOM?節(jié)點

??//?Fiber
??this.return?=?null;????//?父節(jié)點
??this.child?=?null;?????//?第一個子節(jié)點
??this.sibling?=?null;???//?第一個兄弟節(jié)點
??this.index?=?0;

??this.ref?=?null;

??this.pendingProps?=?pendingProps;??//?傳入的?props
??this.memoizedProps?=?null;????
??this.updateQueue?=?null;???//?狀態(tài)更新相關(guān)
??this.memoizedState?=?null;
??this.dependencies?=?null;

??this.mode?=?mode;

??//?Effects
??this.flags?=?NoFlags;
??this.subtreeFlags?=?NoFlags;
??this.deletions?=?null;

??this.lanes?=?NoLanes;
??this.childLanes?=?NoLanes;

??this.alternate?=?null;
??...
}

可以看到 Fiber 節(jié)點中的屬性很多,其中不僅僅包含了 element 相關(guān)的實例信息,還包含了組成 Fiber 樹所需的一些“指針”,組件內(nèi)部的狀態(tài)(memorizedState),用于操作真實 DOM 的副作用(effects)等等。

我們以上面的 Count 組件為例看一下它對應(yīng)的 Fiber 結(jié)構(gòu):

這里我們先主要介紹一下與形成 Fiber 樹相關(guān)的三個屬性:child, sibling 和 return。他們分別指向 Fiber 的第一個子 Fiber,下一個兄弟 Fiber 和父 Fiber。

以下面的 jsx 代碼為例:


?????
//?App.jsx????
????<div>
??????<header>
????????<img?/>
????????<p>
??????????text
????????p>

????????<Count?count={count}?onCountClick={handleCLick}?/>
??????header>
????div>
????
//?Count.jsx
<div>
div>

最終形成的 Fiber 樹結(jié)構(gòu)為:

總結(jié)一下,我們編寫的 jsx 首先會形成 element ,然后在 render 過程中每個 element 都會生成對應(yīng)的 Fiber,最終形成 Fiber 樹。

jsx -> element -> Fiber

下面我們正式介紹一下 render 的過程,看看 Fiber 是如何生成并形成 Fiber 樹的。

二、渲染(render)過程

核心流程

通常 React 運行時會有兩個 Fiber 樹,一個是根據(jù)當(dāng)前最新組件狀態(tài)構(gòu)建出來的,另一個則是上一次構(gòu)建出來的 Fiber 樹,當(dāng)然如果是首次渲染就沒有上一次的 Fiber 樹,這時就只有一個了。簡單來說,render 過程就是 React 「對比舊 Fiber 樹和新的 element」 然后「為新的 element 生成新 Fiber 樹」的一個過程。

從源碼中看,React 的整個核心流程開始于 「performSyncWorkOnRoot」 函數(shù),在這個函數(shù)里會先后調(diào)用 「renderRootSync」 函數(shù)和 「commitRoot」 函數(shù),它們兩個就是分別就是我們上面提到的 render 和 commit 過程。來看 renderRootSync 函數(shù),在 「renderRootSync」 函數(shù)里會先調(diào)用 「prepareFreshStack」 ,從函數(shù)名字我們不難猜出它主要就是為接下來的工作做前置準(zhǔn)備,初始化一些變量例如 workInProgress(當(dāng)前正在處理的 Fiber 節(jié)點) 等,接著會調(diào)用 「workLoopSync」 函數(shù)。(這里僅討論傳統(tǒng)模式,concurrent 模式留給 Fiber 任務(wù)調(diào)度分享),而在 「workLoopSync」 完成之后,「renderRootSync」 也基本上完成了,接下來就會調(diào)用 commitRoot 進(jìn)入 commit 階段。

因此整個 render 過程的重點在 「workLoopSync」 中,從 「workLoopSync」 簡單的函數(shù)定義里我們可以看到,這里用了一個循環(huán)來不斷調(diào)用 「performUnitOfWork」 方法,直到 workInProgress 為 null。

function?workLoopSync()?{
??//?Already?timed?out,?so?perform?work?without?checking?if?we?need?to?yield.
??while?(workInProgress?!==?null)?{
????performUnitOfWork(workInProgress);
??}
}

「performUnitOfWork」 函數(shù)做的事情也很簡單,簡單來說就是為傳進(jìn)來的 workInProgress 生成下一個 Fiber 節(jié)點然后賦值給 workInProgress。通過不斷的循環(huán)調(diào)用 「performUnitOfWork」,直到把所有的 Fiber 都生成出來并連接成 Fiber 樹為止。

現(xiàn)在我們來看 「performUnitOfWork」 具體是如何生成 Fiber 節(jié)點的。

前面介紹 Fiber 結(jié)構(gòu)的時候說過,F(xiàn)iber 是 React 16 引入用于任務(wù)調(diào)度提升用戶體驗的,而在此之前,render 過程是遞歸實現(xiàn)的,顯然遞歸是沒有辦法中斷的,因此 React 需要使用循環(huán)來模擬遞歸過程。

「performUnitOfWork」 正是使用了 「beginWork」「completeUnitOfWork」 來分別模擬這個“遞”和“歸”的過程。

render 過程是深度優(yōu)先的遍歷,「beginWork」 函數(shù)則會為遍歷到的每個 Fiber 節(jié)點生成他的所有子 Fiber 并返回第一個子 Fiber ,這個子 Fiber 將賦值給 workInProgress,在下一輪循環(huán)繼續(xù)處理,直到遍歷到葉子節(jié)點,這時候就需要“歸”了。

「completeUnitOfWork」 就會為葉子節(jié)點做一些處理,然后把葉子節(jié)點的兄弟節(jié)點賦值給 workInProgress 繼續(xù)“遞”操作,如果連兄弟節(jié)點也沒有的話,就會往上處理父節(jié)點。

同樣以上面的 Fiber 樹例子來看,其中的 Fiber 節(jié)點處理順序應(yīng)該如下:

beginWork

在介紹概覽的時候說過,React 通常會同時存在兩個 Fiber 樹,一個是當(dāng)前視圖對應(yīng)的,一個則是根據(jù)最新狀態(tài)正在構(gòu)建中的。這兩棵樹的節(jié)點一一對應(yīng),我們用 current 來代表前者,我們不難發(fā)現(xiàn),當(dāng)首次渲染的時候,current 必然指向 null。實際上在代碼中也確實都是通過這個來判斷當(dāng)前是首次渲染還是更新。

「beginWork」 的目的很簡單:

  • 更新當(dāng)前節(jié)點(workInProgress),獲取新的 children。
  • 為新的 children 生成他們對應(yīng)的 Fiber,并「最終返回第一個子節(jié)點(child)」

「beginWork」 執(zhí)行中,首先會判斷當(dāng)前是否是首次渲染。

  • 如果是首次渲染:
    • 則下來會根據(jù)當(dāng)前正在構(gòu)建的節(jié)點的組件類型做不同的處理,源碼中這塊邏輯使用了大量的 switch case。
switch?(workInProgress.tag)?{
????case?FunctionComponent:?{
??????...
????}
????case?ClassComponent:?{
??????...
????}
????case?HostRoot:?{
??????...
????}
????case?HostComponent:?{
??????...
????}
????...
??}
  • 如果非首次渲染:
    • React 會使用一些優(yōu)化手段,而符合優(yōu)化的條件則是「當(dāng)前節(jié)點對應(yīng)組件的 props 和 context 沒有發(fā)生變化」并且**當(dāng)前節(jié)點的更新優(yōu)先級不夠,**如果這兩個條件均滿足的話可以直接復(fù)制 current 的子節(jié)點并返回。如果不滿足則同首次渲染走一樣的邏輯。
if?(current?!==?null)?{
????//?這里處理一些依賴
????if?(
??????enableLazyContextPropagation?&&
??????!includesSomeLane(renderLanes,?updateLanes)
????)?{
??????const?dependencies?=?current.dependencies;
??????if?(dependencies?!==?null?&&?checkIfContextChanged(dependencies))?{
????????updateLanes?=?mergeLanes(updateLanes,?renderLanes);
??????}
????}

????const?oldProps?=?current.memoizedProps;
????const?newProps?=?workInProgress.pendingProps;

????if?(
??????oldProps?!==?newProps?||
??????hasLegacyContextChanged()?||
??????//?Force?a?re-render?if?the?implementation?changed?due?to?hot?reload:
??????(__DEV__???workInProgress.type?!==?current.type?:?false)
????)?{
??????//?如果?props?或者?context?變了
??????didReceiveUpdate?=?true;
????}?else?if?(!includesSomeLane(renderLanes,?updateLanes))?{
??????didReceiveUpdate?=?false;
??????//?走到這里則說明符合優(yōu)化條件
??????switch?(workInProgress.tag)?{
????????case?HostRoot:
??????????...
??????????break;
????????case?HostComponent:
??????????...
??????????break;
????????case?ClassComponent:?{
??????????...
??????????break;
????????}
????????case?HostPortal:
??????????...
??????????break;
????????case?ContextProvider:?{
??????????...
??????????break;
????????}
????????...
????????
??????}
??????return?bailoutOnAlreadyFinishedWork(current,?workInProgress,?renderLanes);
????}?else?{
??????...
??????didReceiveUpdate?=?false;
????}
??}?else?{
????didReceiveUpdate?=?false;
??}

更新優(yōu)化策略應(yīng)用

開發(fā)過程中我們常常希望利用 React 非首次渲染的優(yōu)化策略來提升性能,如下代碼,B 組件是個純展示組件且內(nèi)部沒有依賴任何 Demo 組件的數(shù)據(jù),因此有些同學(xué)可能會想當(dāng)然認(rèn)為當(dāng) Demo 重新渲染時這個 B 組件是符合 React 優(yōu)化條件的。但結(jié)果是,每次 Demo 重新渲染都會導(dǎo)致 B 組件重新渲染。每次渲染時 B 組件的 props 看似沒發(fā)生變化,但由于 Demo 重新執(zhí)行后會生成全新的 B 組件(下面會介紹),所以新舊 B 組件的 props 肯定也是不同的。

function?App()?{
????return?<Demo?/>
}

function?Demo()?{
????const?[v,?setV]?=?useState();
????return?(
????????<div>
????????????<A?value={v}?/>
????????????<B?/>
????????div>
??
????);
}

那有什么辦法可以保持住 B 組件不變嗎,答案是肯定的,我們可以把 B 組件放到 Demo 組件外層,這樣一來,B 組件是在 App 組件中生成并作為 props 傳入 Demo 的,因為不管 Demo 組件狀態(tài)怎么變化都不會影響到 App 組件,因此 App 和 B 組件就只會在首次渲染時會執(zhí)行一遍,也就是說 Demo 獲取到的 props.children 的引用一直都是指向同一個對象,這樣一來 B 組件的 props 也就不會變化了。

function?App()?{
????return?<Demo>
????????<B?/>
????Demo>

}

function?Demo(props)?{
????const?[v,?setV]?=?useState();
????return?(
????????<div>
????????????<A?value={v}?/>
???????????{props.children}
????????div>
??
????);
}

更新當(dāng)前節(jié)點

通過上面的解析我們知道,當(dāng)不走優(yōu)化邏輯時 「beginWork」 使用大量的 switch...case 來分別處理不同類型的組件,下來我們以我們熟悉的 Function Component 為例。

「核心就是通過調(diào)用函數(shù)組件,得到組件的返回的 element?!?/strong>

類似地,對于類組件,則是調(diào)用組件實例的 render 方法得到 element。

而對于我們普通的組件,例如

,則是直接取 props.children 即可。

function?updateFunctionComponent(
??current,
??workInProgress,
??Component,
??nextProps:?any,
??renderLanes,
)?
{
??let?context;
??if?(!disableLegacyContext)?{
????const?unmaskedContext?=?getUnmaskedContext(workInProgress,?Component,?true);
????context?=?getMaskedContext(workInProgress,?unmaskedContext);
??}
??let?nextChildren;
??prepareToReadContext(workInProgress,?renderLanes);
??//?執(zhí)行組件函數(shù)獲取返回的?element
??nextChildren?=?renderWithHooks(
????current,
????workInProgress,
????Component,
????nextProps,
????context,
????renderLanes,
??);
??
??//?React?DevTools?reads?this?flag.
??workInProgress.flags?|=?PerformedWork;
??reconcileChildren(current,?workInProgress,?nextChildren,?renderLanes);
??return?workInProgress.child;
}

得到組件返回的 element(s) 之后,下一步就是為他們生成 Fiber,我們查看源碼可以看到,不論是函數(shù)組件或是類組件或是普通組件,最后返回的 element(s) 都會作為參數(shù)傳入到 「reconcileChildren」 中。

介紹 「reconcileChildren」 之前我們先用一張圖總結(jié)一下 「beginWork」 的大致流程:

生成子節(jié)點

經(jīng)過上一步得到 workInProgress 的 children 之后,接下來需要為這些 children element 生成 Fiber ,這就是 「reconcileChildFibers」 函數(shù)做的事情,這也是我們經(jīng)常提到的 diff 的過程。

這個函數(shù)里主要分兩種情況處理,如果是 newChild(即 children element)是 object 類型,則進(jìn)入單節(jié)點 diff 過程(「reconcileSingleElement」),如果是數(shù)組類型,則進(jìn)入多節(jié)點 diff 過程(「reconcileChildrenArray」

function?reconcileChildFibers(
????returnFiber:?Fiber,
????currentFirstChild:?Fiber?|?null,
????newChild:?any,
????lanes:?Lanes,
??
):?Fiber?|?null?
{
????if?(typeof?newChild?===?'object'?&&?newChild?!==?null)?{
??????switch?(newChild.$typeof)?{
????????case?REACT_ELEMENT_TYPE:
??????????return?placeSingleChild(
????????????reconcileSingleElement(
??????????????returnFiber,
??????????????currentFirstChild,
??????????????newChild,
??????????????lanes,
????????????),
??????????);
????????...
??????}
??????if?(isArray(newChild))?{
????????return?reconcileChildrenArray(
??????????returnFiber,
??????????currentFirstChild,
??????????newChild,
??????????lanes,
????????);
??????}
??????throwOnInvalidObjectType(returnFiber,?newChild);
????}
????
}???

單節(jié)點diff

function?reconcileSingleElement(
????returnFiber:?Fiber,
????currentFirstChild:?Fiber?|?null,
????element:?ReactElement,
????lanes:?Lanes,
??
):?Fiber?
{
????const?key?=?element.key;
????let?child?=?currentFirstChild;
????while?(child?!==?null)?{
????
??????//?首先比較?key?是否相同
??????if?(child.key?===?key)?{
????????const?elementType?=?element.type;
????????...
???????????//?然后比較?elementType?是否相同
??????????if?(child.elementType?===?elementType)?{
????????????deleteRemainingChildren(returnFiber,?child.sibling);
????????????const?existing?=?useFiber(child,?element.props);
????????????existing.ref?=?coerceRef(returnFiber,?child,?element);
????????????existing.return?=?returnFiber;
????????????return?existing;
??????????}
????????
????????//?Didn't?match.
????????deleteRemainingChildren(returnFiber,?child);
????????break;
??????}?else?{
????????deleteChild(returnFiber,?child);
??????}
??????//?遍歷兄弟節(jié)點,看能不能找到?key?相同的節(jié)點
??????child?=?child.sibling;
????}

????if?(element.type?===?REACT_FRAGMENT_TYPE)?{
??????const?created?=?createFiberFromFragment(
????????element.props.children,
????????returnFiber.mode,
????????lanes,
????????element.key,
??????);
??????created.return?=?returnFiber;
??????return?created;
????}?else?{
??????const?created?=?createFiberFromElement(element,?returnFiber.mode,?lanes);
??????created.ref?=?coerceRef(returnFiber,?currentFirstChild,?element);
??????created.return?=?returnFiber;
??????return?created;
????}
??}
??

本著盡可能復(fù)用舊節(jié)點的原則,在單節(jié)點 diff 在這里,我們會遍歷舊節(jié)點,對每個遍歷到的節(jié)點會做一下兩個判斷:

  • key 是否相同
  • key 相同的情況下,elementType 是否相同

延伸下來有三種情況:

  • 如果 key 不相同,則直接調(diào)用 「deleteChild」 將這個 child 標(biāo)記為刪除,但是我們不用灰心,可能只是我們還沒有找到那個對的節(jié)點,所以要繼續(xù)執(zhí)行child = child.sibling;遍歷兄弟節(jié)點,直到找到那個對的節(jié)點。
  • 如果 key 相同,elementType 相同,那就是最理想的情況,找到了可以復(fù)用的節(jié)點,直接調(diào)用 「deleteRemainingChildren」 把剩余的兄弟節(jié)點標(biāo)記刪除,然后直接復(fù)用 child 返回。
  • 如果 key 相同,但 elementType 不同,這是最悲情的情況,我們找到了那個節(jié)點,可惜的是這個節(jié)點的 elementType 已經(jīng)變了,那我們也不需要再找了,把 child 及其所有兄弟節(jié)點標(biāo)記刪除,跳出循環(huán)。直接創(chuàng)建一個新的節(jié)點。

多節(jié)點diff

function?reconcileChildrenArray(
????returnFiber:?Fiber,
????currentFirstChild:?Fiber?|?null,
????newChildren:?Array<*>,
????lanes:?Lanes,
)?
{
????let?resultingFirstChild:?Fiber?|?null?=?null;
????let?previousNewFiber:?Fiber?|?null?=?null;

????let?oldFiber?=?currentFirstChild;
????let?lastPlacedIndex?=?0;
????let?newIdx?=?0;
????let?nextOldFiber?=?null;
????for?(;?oldFiber?!==?null?&&?newIdx?????????const?newFiber?=?updateSlot(
????????returnFiber,
????????oldFiber,
????????newChildren[newIdx],
????????lanes,
????????);
????????if?(newFiber?===?null)?{
??????????break;
????????}
??????????lastPlacedIndex?=?placeChild(newFiber,?lastPlacedIndex,?newIdx);
??????????if?(previousNewFiber?===?null)?{
????????????resultingFirstChild?=?newFiber;
??????????}?else?{
????????????previousNewFiber.sibling?=?newFiber;
??????????}
??????????previousNewFiber?=?newFiber;
??????????oldFiber?=?nextOldFiber;
????}
????if?(newIdx?===?newChildren.length)?{
????????...
????}
????if?(oldFiber?===?null)?{
????????...
????}
????for?(;?newIdx?????????...
????}
????return?resultingFirstChild;
}


function?updateSlot(
????returnFiber:?Fiber,
????oldFiber:?Fiber?|?null,
????newChild:?any,
????lanes:?Lanes,
??
):?Fiber?|?null?
{
????const?key?=?oldFiber?!==?null???oldFiber.key?:?null;
????...
????if?(newChild.key?===?key)?{
??????return?updateElement(returnFiber,?oldFiber,?newChild,?lanes);
????}?else?{
??????return?null;
????}
}

從源碼我們可以看到,在 「reconcileChildrenArray」 中,出現(xiàn)了兩個循環(huán)。

第一輪循環(huán)中邏輯如下:

  1. 同時遍歷 oldFiber 鏈和 newChildren,判斷 oldFiber 和 newChild 的 key 是否相同。
  2. 如果 key 相同。
    1. 判斷雙方 elementType 是否相同。
    2. 如果相同則復(fù)用 oldFiber 返回。
    3. 如果不同則新建 Fiber 返回。
  3. 如果 key 不同則直接跳出循環(huán)。

可以看到第一輪循環(huán)只要碰到新舊的 key 不一樣時就會跳出循環(huán),換句話說,第一輪循環(huán)里做的事情都是基于 key 相同,主要就是「更新」的工作。

跳出循環(huán)后,要先執(zhí)行兩個判斷

  • newChildren 已經(jīng)遍歷完了:這種情況說明新的 children 全都已經(jīng)處理完了,只要把 oldFiber 和他所有剩余的兄弟節(jié)點刪除然后返回頭部的 Fiber 即可。
  • 已經(jīng)沒有 oldFiber :這種情況說明 children 有新增的節(jié)點,給這些新增的節(jié)點逐一構(gòu)建 Fiber 并鏈接上,然后返回頭部的 Fiber 即可。

如果以上兩種情況都不是,則進(jìn)入第二輪循環(huán)。

在執(zhí)行第二輪循環(huán)之前,先把剩下的舊節(jié)點和他們對應(yīng)的 key 或者 index 做成映射,方便查找。

第二輪循環(huán)沿用了第一輪循環(huán)的 newIdx 變量,說明第二輪循環(huán)是在第一輪循環(huán)結(jié)束的地方開始再次遍歷剩下的 newChildren。

????const?existingChildren?=?mapRemainingChildren(returnFiber,?oldFiber);
????for?(;?newIdx???????const?newFiber?=?updateFromMap(
????????existingChildren,
????????returnFiber,
????????newIdx,
????????newChildren[newIdx],
????????lanes,
??????);
??????if?(newFiber?!==?null)?{
????????if?(shouldTrackSideEffects)?{
??????????if?(newFiber.alternate?!==?null)?{
????????????existingChildren.delete(
??????????????newFiber.key?===?null???newIdx?:?newFiber.key,
????????????);
??????????}
????????}
????????lastPlacedIndex?=?placeChild(newFiber,?lastPlacedIndex,?newIdx);
????????if?(previousNewFiber?===?null)?{
??????????resultingFirstChild?=?newFiber;
????????}?else?{
??????????previousNewFiber.sibling?=?newFiber;
????????}
????????previousNewFiber?=?newFiber;
??????}
????}
????
????
????function?placeChild(
????newFiber:?Fiber,
????lastPlacedIndex:?number,
????newIndex:?number,
??
):?number?
{
????newFiber.index?=?newIndex;
????if?(!shouldTrackSideEffects)?{
??????//?Noop.
??????return?lastPlacedIndex;
????}
????const?current?=?newFiber.alternate;
????if?(current?!==?null)?{
??????const?oldIndex?=?current.index;
??????if?(oldIndex?????????//?This?is?a?move.
????????newFiber.flags?|=?Placement;
????????return?lastPlacedIndex;
??????}?else?{
????????//?This?item?can?stay?in?place.
????????return?oldIndex;
??????}
????}?else?{
??????//?This?is?an?insertion.
??????newFiber.flags?|=?Placement;
??????return?lastPlacedIndex;
????}
??}

第二輪循環(huán)主要調(diào)用了 「updateFromMap」 來處理節(jié)點,在這里需要用 newChild 的 key 去 existingChildren 中找對應(yīng)的 Fiber。

  • 能找到 key 相同的,則說明這個節(jié)點只是位置變了,是可以復(fù)用的。
  • 找不到 key 相同的,則說明這個節(jié)點應(yīng)該是新增的。

不管是復(fù)用還是新增,「updateFromMap」 都會返回一個 newFiber,然后我們需要為這個 newFiber 更新一下它的位置(index),但是僅僅更新這個 Fiber 的 index 還不夠,因為這個 Fiber 有可能是復(fù)用的,如果是復(fù)用的就意味著它已經(jīng)有對應(yīng)的真實 DOM 節(jié)點了,我們還需要復(fù)用它的真實 DOM,因此需要對應(yīng)更新這個 Fiber 的 flag,但是真的需要對每個 Fiber 都去設(shè)置 flag 嗎,我們舉個例子:

//?舊
[<div?key='a'?/>,?'b'?/>,?'c'?/>]

//?新
[<div?key='c'?/>,?'a'?/>,?'b'?/>]

如果按照我們剛剛說的做法,這里的 a, b, c 都會被打上 flag,這樣一來,在 commit 階段,這三個 DOM 都會被移動,可是我們知道,這里顯然只需要移動一個節(jié)點即可,退一萬步說我們移動兩個節(jié)點也比移動所有節(jié)點要來的聰明。

其實在這個問題上主要就是我們得區(qū)分一下到底哪個節(jié)點才是移動了的,這就需要一個參照點,我們要保證在參照點左邊都是已經(jīng)排好順序了的。而這個參照點就是 lastPlacedIndex。有了它,我們在遍歷 newChildren 的時候可能會出現(xiàn)下面兩種情況:

  • 生成(或復(fù)用)的 Fiber 對應(yīng)的老 index < lastPlacedIndex,這就說明這個 Fiber 的位置不對,因為 lastPlacedIndex 左邊的應(yīng)該全是已經(jīng)遍歷過的 newChild 生成的 Fiber。因此這個 Fiber 是需要被移動的,打上 flag。
  • 如果 Fiber 對應(yīng)的老 index >= lastPlacedIndex,那就說明這個 Fiber 的相對位置是 ok 的,可以不用移動,但是我們需要更新一下參照點,把參照點更新成這個 Fiber 對應(yīng)的老 index。

我們舉一個例子:

//?舊
[<div?key='a'?/>,?'b'?/>,?'c'?/>,?'d'?/>]

//?新
[<div?key='c'?/>,?'a'?/>,?'b'?/>,?'d'?/>,?'e'?/>]

lastPlacedIndex 初始值為 0,

首先處理第一個節(jié)點 c,給節(jié)點 c 的 index 賦值為最新值 0,c.index = 0。

可以看到 c 的 oldIndex 為 2,此時 oldIndex > lastPlacedIndex,無需對 c 做移動,將 lastPlacedIndex 賦值為 2。

此時 lastPlacedIndex = 2。

然后處理節(jié)點 a,a.index = 1。

a 的 oldIndex 為 0,此時 oldIndex < lastPlacedIndex,因此需要對 a 打上 Placement 標(biāo)記,lastPlacedIndex 維持不變。

此時 lastPlacedIndex 仍然等于 2。

然后處理節(jié)點 b,b.index = 2。

b 的 oldIndex 為 1,此時 oldIndex < lastPlacedIndex,需要對 b 打上 Placement 標(biāo)記,將 lastPlacedIndex 維持不變。

此時 lastPlacedIndex 仍然等于 2。

然后處理節(jié)點 d,b.index = 3。

d 的 oldIndex 為 3,此時 oldIndex > lastPlacedIndex,無需對 d 做移動,將 lastPlacedIndex 賦值為 3。

此時 lastPlacedIndex = 3。

然后處理節(jié)點 e,e.index = 4。

由于 e 是新建節(jié)點,所以 e 的 oldIndex 為 0,此時 oldIndex < lastPlacedIndex,因此需要對 e 打上 Placement 標(biāo)記,lastPlacedIndex 維持不變。

因此最終需要變動位置的節(jié)點是 a b e。

這里可以看到其實最高效的改動是移動 c 和 e,但是 React 的 diff 邏輯選擇了固定住 c,移動 a b,因此我們平時寫代碼的時候盡量避免把節(jié)點從后面提到前面的操作。

為 newChildren 里的所有 element 都生成了 Fiber 并連接好之后,返回第一個 child ,至此生成子節(jié)點的步驟就完成了。

completeUnitOfWork

在核心流程里我們說到,當(dāng) beginWork 處理到葉子節(jié)點,返回 null 的時候就會調(diào)用 「completeUnitOfWork」 函數(shù)。

「completeUnitOfWork」 主要做的事情有兩件:

  • 處理當(dāng)前節(jié)點
  • “歸”操作

處理當(dāng)前節(jié)點

「completeUnitOfWork」 里主要調(diào)用了 「completeWork」 來處理當(dāng)前節(jié)點,而在 completeWork 中則是使用了 switch...case... 來處理不同類型的節(jié)點,這里我們主要以最常見的 HostComponent 為例。分成首次渲染和非首次渲染兩種情況討論。

mount

當(dāng)是首次渲染時,這里要做的事情主要是:

  1. 創(chuàng)建真實 DOM。
  2. 如果有子節(jié)點的話將子節(jié)點的真實 DOM 插入到剛剛創(chuàng)建的 DOM 中。
  3. 處理真實 DOM 的 props 等。
const?currentHostContext?=?getHostContext();
//?為fiber創(chuàng)建對應(yīng)DOM節(jié)點
const?instance?=?createInstance(
????type,
????newProps,
????rootContainerInstance,
????currentHostContext,
????workInProgress,
??);
//?將子孫DOM節(jié)點插入剛生成的DOM節(jié)點中
appendAllChildren(instance,?workInProgress,?false,?false);
//?DOM節(jié)點賦值給fiber.stateNode
workInProgress.stateNode?=?instance;

//?處理props
if?(
??finalizeInitialChildren(
????instance,
????type,
????newProps,
????rootContainerInstance,
????currentHostContext,
??)
)?{
??markUpdate(workInProgress);
}

update

當(dāng) update 時,F(xiàn)iber 節(jié)點已經(jīng)存在對應(yīng) DOM 節(jié)點,所以不需要生成 DOM 節(jié)點。需要做的主要是處理DOM 節(jié)點的 props,這里主要就是一些真實 DOM 的 onClick、onChange等回調(diào)函數(shù)的注冊,style 等,這些處理完之后的 props 也會記錄到 workInProgress.updateQueue 中,并在 commit 階段更新到 DOM 節(jié)點上。

if?(current?!==?null?&&?workInProgress.stateNode?!=?null)?{
??//?update的情況
??updateHostComponent(
????current,
????workInProgress,
????type,
????newProps,
????rootContainerInstance,
??);
}

“歸”

剛剛說到,當(dāng) 「beginWork」 返回值為 null 的時候會進(jìn)入 「completeUnitOfWork」 中,可是我們知道 beginWork 是深度優(yōu)先的更新,也就意味著進(jìn)入 「completeUnitOfWork」 之后必然還需要回到 beginWork 中繼續(xù)處理其他的節(jié)點。

????...
????const?siblingFiber?=?completedWork.sibling;
????if?(siblingFiber?!==?null)?{
??????//?If?there?is?more?work?to?do?in?this?returnFiber,?do?that?next.
??????workInProgress?=?siblingFiber;
??????return;
????}
????//?Otherwise,?return?to?the?parent
????completedWork?=?returnFiber;
????//?Update?the?next?thing?we're?working?on?in?case?something?throws.
????workInProgress?=?completedWork;

可以看到,當(dāng)處理完當(dāng)前節(jié)點之后,React 會判斷當(dāng)前節(jié)點是否具有兄弟節(jié)點,如果有的話則將兄弟節(jié)點設(shè)置為當(dāng)前的 workInProgress 回到主流程繼續(xù) 「beginWork?!?/strong>

而如果沒有兄弟節(jié)點的話,就意味著同父節(jié)點下的所有子節(jié)點都已經(jīng)處理完畢,則接下來就會處理他們的父節(jié)點。

大致流程就是:「beginWork」 執(zhí)行到當(dāng)前節(jié)點沒有 child 的時候,進(jìn)入 「completeUnitOfWork」 處理當(dāng)前節(jié)點,處理完后如果當(dāng)前節(jié)點有兄弟節(jié)點則回到 「beginWork」 繼續(xù)處理兄弟節(jié)點,如果沒有兄弟節(jié)點則繼續(xù)在 「completeUnitOfWork」 處理當(dāng)前節(jié)點的父節(jié)點,直到“歸”到根結(jié)點上。

三、掛載過程(commitRoot)

effect list

render階段的一個主要工作是收集需要執(zhí)行的 DOM 操作,然后交給 commit階段 來處理,而這些 DOM 操作的具體類型都會保存在 Fiber 節(jié)點的 effectTag 屬性上。

部分 DOM 操作的類型:

//?插入?DOM
export?const?Placement?=?/*????????????????*/?0b00000000000010;
//?更新?DOM
export?const?Update?=?/*???????????????????*/?0b00000000000100;
//?插入并更新?DOM
export?const?PlacementAndUpdate?=?/*???????*/?0b00000000000110;
//?刪除?DOM?節(jié)點
export?const?Deletion?=?/*?????????????????*/?0b00000000001000;

使用二進(jìn)制來表示可以方便地用位運算給 effectTag 帶上多個副作用(effect),也可以方便地判斷是否存在某個副作用。例如有個 fiber 節(jié)點 effectTagPlacementAndUpdate (0b00000000000110),可以通過按位與運算來判斷是否存在Placement

const?effectTag?=?PlacementAndUpdate;
console.log(effectTag?&?Placement?!==?0);?//?=>?true

commit階段 可以像 render階段 那樣遍歷所有 fiber 節(jié)點找出其中的 effectTag,但這樣效率比較低,所以在 render階段completeUnitOfWork 中會把具有 effectTag 的 fiber 節(jié)點連接起來,形成 effectList 鏈表。例如我們有這樣的代碼:

function?App()?{
??const?[count,?setCount]?=?useState(0);
??return?(
????<div?onClick={()?=>?setCount(count?+?1)}>
??????<p>{count}p>

??????<span>{count}span>
????div>
??);
};

我們在 performSyncWorkOnRoot 方法的末尾的 finishedWork 打個斷點,然后點擊 div 觸發(fā)一次 setCount 更新:

當(dāng) react 執(zhí)行到斷點這個地方時,我們在控制臺打印一下 finishedWork

所以此時對應(yīng)的 effectList是:

這里 spanp 節(jié)點有 effectTag 是因為 {count},div 節(jié)點有 effectTag 是因為重新生成的 onClick 函數(shù)。

commit 階段

render階段結(jié)束后,會在performSyncWorkOnRoot()finishConcurrentRender()中把 fiberRootNode 傳給 commitRoot 方法,開啟 commit階段

performSyncWorkOnRoot為例:

function?performSyncWorkOnRoot(root)?{
????//?render階段的入口函數(shù)
????renderRootSync(root,?lanes);
????//?...
????//?commitRoot函數(shù)調(diào)用
????const?finishedWork?=?root.current.alternate;
????root.finishedWork?=?finishedWork;
????root.finishedLanes?=?lanes;
????commitRoot(root);
????//?...省略代碼
}

commit階段的一個主要工作就是遍歷 effectList 并執(zhí)行對應(yīng)的 DOM 操作。commit階段又分為三個子階段:

  • before mutation階段
  • mutation階段
  • layout階段

下面來看看 commit階段 具體發(fā)生了什么。

進(jìn)入 commitRootImpl 方法時,會先判斷 rootWithPendingPassiveEffects 是否為 null,如果不為 null 就會執(zhí)行 flushPassiveEffects。

function?commitRootImpl(root,?renderPriorityLevel)?{
??do?{
????flushPassiveEffects();
??}?while?(rootWithPendingPassiveEffects?!==?null);

??//?...
}

rootWithPendingPassiveEffects 中的PassiveEffect 是什么意思呢?我們知道如果一個 fiber 節(jié)點的 dom 節(jié)點需要被插入到頁面中,那 fiber.effectTag 就會帶上 Placement effect,類似的,如果一個 FunctionComponent 有 useEffect 需要被執(zhí)行,那它就會帶上 Passive effect。

所以這里的意思是進(jìn)入 commitRoot 時先判斷當(dāng)前是否還有未執(zhí)行的 useEffect,如果有,就執(zhí)行它,也就是說在開啟新一輪的 commit 階段時會先等待上一輪的 useEffect 執(zhí)行完。這其實在官方文檔里也有一些說明:

接著會重置 render階段使用到的一些全局變量:

function?commitRootImpl(root,?renderPriorityLevel)?{
??do?{
????flushPassiveEffects();
??}?while?(rootWithPendingPassiveEffects?!==?null);

??//?...

??if?(root?===?workInProgressRoot)?{
????workInProgressRoot?=?null;
????workInProgress?=?null;
????workInProgressRootRenderLanes?=?NoLanes;
??}
??
??//?...
}

處理 effect list

function?commitRootImpl(root,?renderPriorityLevel)?{
??//?...

??let?firstEffect;
??if?(finishedWork.effectTag?>?PerformedWork)?{
????if?(finishedWork.lastEffect?!==?null)?{
??????finishedWork.lastEffect.nextEffect?=?finishedWork;
??????firstEffect?=?finishedWork.firstEffect;
????}?else?{
??????firstEffect?=?finishedWork;
????}
??}?else?{
????firstEffect?=?finishedWork.firstEffect;
??}
??
??//?...
}

上面說過 render階段已經(jīng)把帶有 effectTag 的 fiber 節(jié)點連接形成一條鏈表了,這里再次處理 effect list 是因為這條鏈表目前只有子節(jié)點,并沒有掛載根節(jié)點。如果根節(jié)點也存在 effectTag,那么就需要把根節(jié)點拼接到鏈表的末尾,形成一條完整的 effect list

同時上面的代碼也會取出 firstEffect,也就是第一個需要被處理的 fiber 節(jié)點。接著判斷如果存在 firstEffect,會將 firstEffect 賦值給 nextEffect,開始三個子階段的工作。

function?commitRootImpl(root,?renderPriorityLevel)?{
??//?...

??if?(firstEffect?!==?null)?{
????nextEffect?=?firstEffect;
????
????// beforeMutation 階段:
????//?執(zhí)行?commitBeforeMutationEffects
????
????// mutation 階段:
????//?執(zhí)行?commitMutationEffects
????
????// layout 階段:
????//?執(zhí)行?commitLayoutEffects
??}
??
??//?...
}

beforeMutation階段

beforeMutation階段會執(zhí)行 commitBeforeMutationEffects 方法

function?commitBeforeMutationEffects()?{
??while?(nextEffect?!==?null)?{
????//?...
????
????const?effectTag?=?nextEffect.effectTag;
????if?((effectTag?&?Snapshot)?!==?NoEffect)?{
??????//?...
??????commitBeforeMutationEffectOnFiber(current,?nextEffect);
????}

????//?...
????nextEffect?=?nextEffect.nextEffect;
??}
}

執(zhí)行 getSnapshotBeforeUpdate

commitBeforeMutationEffects這個方法中會遍歷帶有 effectTag 的 fiber 節(jié)點,如果判斷有 Snapshot effectTag 就會調(diào)用 ClassComponent 的 getSnapshotBeforeUpdate 生命周期方法:

function?commitBeforeMutationLifeCycles(
??current:?Fiber?|?null,
??finishedWork:?Fiber,
):?void?{
??switch?(finishedWork.tag)?{
????case?FunctionComponent:
????case?ForwardRef:
????case?SimpleMemoComponent:
????case?Block:?{
??????return;
????}
????case?ClassComponent:?{
??????if?(finishedWork.effectTag?&?Snapshot)?{
????????if?(current?!==?null)?{
??????????//?...
??????????const?snapshot?=?instance.getSnapshotBeforeUpdate(/**?...?*/);
??????????//?...
????????}
??????}
??????return;
????}
????case?HostRoot:?{
??????//?...
??????return;
????}
????case?HostComponent:
????case?HostText:
????case?HostPortal:
????case?IncompleteClassComponent:
??????//?Nothing?to?do?for?these?component?types
??????return;
??}
}

調(diào)度 useEffect

再看下 commitBeforeMutationEffects 的剩余部分:

function?commitBeforeMutationEffects()?{
??while?(nextEffect?!==?null)?{
????//?...
????
????if?((effectTag?&?Passive)?!==?NoEffect)?{
??????if?(!rootDoesHavePassiveEffects)?{
????????rootDoesHavePassiveEffects?=?true;
????????scheduleCallback(NormalSchedulerPriority,?()?=>?{
??????????flushPassiveEffects();
??????????return?null;
????????});
??????}
????}
????nextEffect?=?nextEffect.nextEffect;
??}
}

上面提到 flushPassiveEffects 用于執(zhí)行 useEffect 的回調(diào)函數(shù),而這里并不會立即執(zhí)行它,而是把它放在 scheduleCallback 的回調(diào)當(dāng)中,scheduleCallback 方法會以一個優(yōu)先級異步執(zhí)行它的回調(diào)函數(shù)。

所以這段代碼的意思是,如果存在 Passive effect,則把 rootDoesHavePassiveEffects 置為 true,并且調(diào)度 flushPassiveEffects,而整個 commit階段「同步執(zhí)行」的,所以 useEffect 的回調(diào)函數(shù)其實會在 commit階段「完成后」再異步執(zhí)行。

這也跟官方文檔說的對應(yīng)上了:

總結(jié)

beforeMutation階段 會:

  • 執(zhí)行getSnapshotBeforeUpdate生命周期方法
  • 「調(diào)度」useEffect

mutation階段

mutation階段會執(zhí)行 commitMutationEffects 方法:

function?commitMutationEffects(root:?FiberRoot,?renderPriorityLevel)?{
??while?(nextEffect?!==?null)?{
????const?effectTag?=?nextEffect.effectTag;
????
????//?如果有?ContentReset,會重置文本節(jié)點
????if?(effectTag?&?ContentReset)?{
??????commitResetTextContent(nextEffect);
????}
????
????//?如果有?Ref,會執(zhí)行?ref?相關(guān)的更新
????if?(effectTag?&?Ref)?{
??????//?...
????}

????const?primaryEffectTag?=
??????effectTag?&?(Placement?|?Update?|?Deletion?|?Hydrating);
????switch?(primaryEffectTag)?{
??????//?如果需要插入節(jié)點,會執(zhí)行?commitPlacement
??????case?Placement:?{
????????commitPlacement(nextEffect);
????????nextEffect.effectTag?&=?~Placement;
????????break;
??????}
??????//?如果需要更新節(jié)點,會執(zhí)行?commitWork
??????case?Update:?{
????????const?current?=?nextEffect.alternate;
????????commitWork(current,?nextEffect);
????????break;
??????}
??????//?如果需要刪除節(jié)點,會執(zhí)行?commitDeletion
??????case?Deletion:?{
????????commitDeletion(root,?nextEffect,?renderPriorityLevel);
????????break;
??????}
??????//?...
????}
????
????//?取出下一個?fiber?節(jié)點,進(jìn)入下一次循環(huán)
????nextEffect?=?nextEffect.nextEffect;
??}
}

在這個方法中會遍歷帶有 effectTag 的 fiber 節(jié)點,

  • 如果有 ContentReset,會重置文本節(jié)點
  • 如果有 Ref,會執(zhí)行 ref 相關(guān)的操作
  • 增刪改
    • 如果需要插入節(jié)點,會執(zhí)行 commitPlacement
    • 如果需要更新節(jié)點,會執(zhí)行 commitWork
    • 如果需要刪除節(jié)點,會執(zhí)行 commitDeletion
    • ...

commitPlacement

commitPlacement 方法中,會先找到距離最近的 host 類型父節(jié)點和距離最近的 host 類型兄弟節(jié)點,然后根據(jù) host 父 fiber 節(jié)點的類型取出對應(yīng)的 DOM 節(jié)點,接著根據(jù)是否 container 來執(zhí)行 insertOrAppendPlacementNodeIntoContainerinsertOrAppendPlacementNode

function?commitPlacement(finishedWork:?Fiber):?void?{
??//?找到?host?父?fiber?節(jié)點
??const?parentFiber?=?getHostParentFiber(finishedWork);

??let?parent;
??let?isContainer;
??const?parentStateNode?=?parentFiber.stateNode;
??
??//?根據(jù)?host?父?fiber?節(jié)點的類型,取出對應(yīng)的?DOM?節(jié)點
??switch?(parentFiber.tag)?{
????case?HostComponent:
??????parent?=?parentStateNode;
??????isContainer?=?false;
??????break;
????case?HostRoot:
??????parent?=?parentStateNode.containerInfo;
??????isContainer?=?true;
??????break;
????case?HostPortal:
??????parent?=?parentStateNode.containerInfo;
??????isContainer?=?true;
??????break;
????//?...
??}
??//?...
??//?獲取?host?兄弟節(jié)點
??const?before?=?getHostSibling(finishedWork);
??
??//?根據(jù)是否?container?來決定執(zhí)行哪個方法
??if?(isContainer)?{
????insertOrAppendPlacementNodeIntoContainer(finishedWork,?before,?parent);
??}?else?{
????insertOrAppendPlacementNode(finishedWork,?before,?parent);
??}
}

為什么需要先找到 host 父 fiber 節(jié)點和 host 兄弟 fiber 節(jié)點?

我們知道在DOM中插入一個節(jié)點有兩種方式:

  • parentNode.appendChild(newNode)
  • parentNode.insertBefore(newNode, referenceNode)

無論哪種方式都需要找到它的父 DOM 節(jié)點,而如果需要 insertBefore 則還需要找到它的兄弟 DOM 節(jié)點。

另外,為什么需要找 host 類型的父節(jié)點和兄弟節(jié)點呢?這是因為最近的父 「fiber」 節(jié)點不一定就是最近的父 「DOM」 節(jié)點,同理,最近的兄弟 「fiber」 節(jié)點不一樣是最近的兄弟 「DOM」 節(jié)點。例如,我們的代碼長這樣:

function?Item()?{
??return?


}

function?App()?{
??return?(
????

??????
??????
????

??);
}

它對應(yīng)的 fiber 樹和 dom 樹分別是:

可以看到, fiber 樹和 don 樹并不是一一對應(yīng)的:

  • p 的 fiber 節(jié)點的父節(jié)點是 Item,而 p 的 dom 節(jié)點的父節(jié)點是 div
  • p 的 fiber 節(jié)點沒有兄弟節(jié)點,而 p 的 dom 節(jié)點有個兄弟節(jié)點是 span

上面的insertOrAppendPlacementNodeIntoContainerinsertOrAppendPlacementNode做的事情差不多,都會判斷是否有 before,如果有,則執(zhí)行 insertBefore,沒有則執(zhí)行 appendChild

function?insertOrAppendPlacementNode(
??node:?Fiber,
??before:??Instance,
??parent:?Instance,
):?void?{
??//?...
??if?(before)?{
????insertBefore(parent,?stateNode,?before);
??}?else?{
????appendChild(parent,?stateNode);
??}
??//?...
}

appendChildinsertBefore 都來自于 ReactFiberHostConfig。

這里的 ReactFiberHostConfig 在源碼里其實只是一個空殼,最終需要被特定環(huán)境的 renderer 來填充,例如在我們平常使用 ReactDOM 時,ReactFiberHostConfig 會被 ReactDOM 的 ReactDOMHostConfig 來填充:

export?function?appendChild(
??parentInstance:?Instance,
??child:?Instance?|?TextInstance,
):?void?{
??parentInstance.appendChild(child);
}

export?function?insertBefore(
??parentInstance:?Instance,
??child:?Instance?|?TextInstance,
??beforeChild:?Instance?|?TextInstance?|?SuspenseInstance,
):?void?{
??parentInstance.insertBefore(child,?beforeChild);
}

可以看到,其實最終就是執(zhí)行 dom 節(jié)點的 appendChildinsertBefore 方法。

?

React 倉庫其他 renderer 的 hostConfig

?

BTW,umijs/hox(https://github.com/umijs/hox) 是一個基于 hooks 的全局狀態(tài)管理工具,它跟其他基于 hooks 的狀態(tài)管理工具很大的一個不同點是, hox 不需要我們手動掛載 就能直接使用 model:

//?counterModel.js
import?{?useState?}?from?"react";
import?{?createModel?}?from?"hox";
function?useCounter()?{
??const?[count,?setCount]?=?useState(0);
??const?decrement?=?()?=>?setCount(count?-?1);
??const?increment?=?()?=>?setCount(count?+?1);
??return?{
????count,
????decrement,
????increment
??};
}
export?default?createModel(useCounter);

//?index.jsx
import?useCounterModel?from?"./counterModel";
function?App(props)?{
??const?counter?=?useCounterModel();
??return?(
????

??????

{counter.count}


??????Increment
????

??);
}

其實它是在調(diào)用 createModel 時把 hooks 的執(zhí)行掛在了它自定義的一個 renderer 里,對應(yīng)的 hostConfig 都是空函數(shù),因為它并不需要執(zhí)行真正的渲染,只是用來執(zhí)行 hooks 而已:

import?ReactReconciler?from?"react-reconciler";
import?{?ReactElement?}?from?"react";

const?hostConfig?=?{
??now:?Date.now,
??getRootHostContext:?()?=>?({}),
??prepareForCommit:?()?=>?{},
??resetAfterCommit:?()?=>?{},
??getChildHostContext:?()?=>?({}),
??shouldSetTextContent:?()?=>?true,
??createInstance:?()?=>?{},
??createTextInstance:?()?=>?{},
??appendInitialChild:?()?=>?{},
??appendChild:?()?=>?{},
??finalizeInitialChildren:?()?=>?{},
??supportsMutation:?true,
??appendChildToContainer:?()?=>?{},
??prepareUpdate:?()?=>?true,
??commitUpdate:?()?=>?{},
??commitTextUpdate:?()?=>?{},
??removeChild:?()?=>?{}
};

const?reconciler?=?ReactReconciler(hostConfig?as?any);

export?function?render(reactElement:?ReactElement)?{
??const?container?=?reconciler.createContainer(null,?false,?false);
??return?reconciler.updateContainer(reactElement,?container,?null,?null);
}

commitWork

commitWork 用于更新節(jié)點。在這個方法中會根據(jù) fiber 節(jié)點的類型進(jìn)行不同的操作:

function?commitWork(current:?Fiber?|?null,?finishedWork:?Fiber):?void?{
??switch?(finishedWork.tag)?{
????case?FunctionComponent:
????case?ForwardRef:
????case?MemoComponent:
????case?SimpleMemoComponent:
????case?Block:?{
??????//?...
??????return;
????}
????case?ClassComponent:?{
??????return;
????}
????case?HostComponent:?{
??????//?...
??????return;
????}
????case?HostText:?{
??????//?...
??????return;
????}
????//?case...
??}
}

對于 FunctionComponent,更新時會執(zhí)行 commitHookEffectListUnmount(``HookLayout | HookHasEffect, finishedWork)

function?commitHookEffectListUnmount(tag:?number,?finishedWork:?Fiber)?{
??const?updateQueue?=?finishedWork.updateQueue;
??const?lastEffect?=?updateQueue?!==?null???updateQueue.lastEffect?:?null;
??if?(lastEffect?!==?null)?{
????const?firstEffect?=?lastEffect.next;
????let?effect?=?firstEffect;
????do?{
??????if?((effect.tag?&?tag)?===?tag)?{
????????const?destroy?=?effect.destroy;
????????effect.destroy?=?undefined;
????????if?(destroy?!==?undefined)?{
??????????destroy();
????????}
??????}
??????effect?=?effect.next;
????}?while?(effect?!==?firstEffect);
??}
}

里面會遍歷 effect list,判斷 effect.tag 是否存在參數(shù)傳入的 tag 類型,在這個場景里被傳入 tag 參數(shù)是 HookLayout,也就是 useLayoutEffect 對應(yīng)的 effect tag。所以這里的意思是遍歷 effect list,如果存在 useLayoutEffect 的 effect tag,則執(zhí)行它的銷毀函數(shù)(即 useLayoutEffect 的回調(diào)函數(shù)的返回值)。

如果 fiber 節(jié)點的類型是 HostComponent,也就是 dom 節(jié)點對應(yīng)的 fiber 節(jié)點,更新時會執(zhí)行 commitUpdate 方法:

function?commitWork(current:?Fiber?|?null,?finishedWork:?Fiber):?void?{
??switch?(finishedWork.tag)?{
????//?...
????case?HostComponent:?{
??????const?instance:?Instance?=?finishedWork.stateNode;
??????if?(instance?!=?null)?{
????????//?...
????????const?updatePayload:?null?|?UpdatePayload?=?(finishedWork.updateQueue:?any);
????????//?...
????????if?(updatePayload?!==?null)?{
??????????commitUpdate(
????????????instance,
????????????updatePayload,
????????????type,
????????????oldProps,
????????????newProps,
????????????finishedWork,
??????????);
????????}
????????if?(enableDeprecatedFlareAPI)?{
??????????const?prevListeners?=?oldProps.DEPRECATED_flareListeners;
??????????const?nextListeners?=?newProps.DEPRECATED_flareListeners;
??????????if?(prevListeners?!==?nextListeners)?{
????????????updateDeprecatedEventListeners(nextListeners,?finishedWork,?null);
??????????}
????????}
??????}
??????return;
????}
????//?...
??}
}

commitUpdate 方法接收的參數(shù)中有個 updatePayload,它來自于 fiber.updateQueue 屬性,對于類型為 HostComponent 的 fiber 節(jié)點來說,它的 updateQueue 屬性是一個數(shù)組,表示這個 dom 節(jié)點的屬性變更,例如一個 dom 節(jié)點在某次更新中它的 a 屬性需要從 react 更新為 vue,b 屬性需要從 byte 更新為 dance,那這個 dom 節(jié)點的 fiber 節(jié)點的 updateQueue 就長這樣 ['a', 'vue', 'b', 'dance'],也就是說第 i 項是屬性 key,第 i + 1 項是屬性 value。

commitUpdate 方法同樣來自 ReactFiberHostConfig

export?function?commitUpdate(
??domElement:?Instance,
??updatePayload:?Array,
??type:?string,
??oldProps:?Props,
??newProps:?Props,
??internalInstanceHandle:?Object,
):?void?{
??//?...
??updateProperties(domElement,?updatePayload,?type,?oldProps,?newProps);
}

它最終會調(diào)用 updateDOMProperties 來更新 dom 屬性:

function?updateDOMProperties(
??domElement:?Element,
??updatePayload:?Array,
??wasCustomComponentTag:?boolean,
??isCustomComponentTag:?boolean,
):?void?{
??for?(let?i?=?0;?i?????const?propKey?=?updatePayload[i];
????const?propValue?=?updatePayload[i?+?1];
????if?(propKey?===?STYLE)?{
??????setValueForStyles(domElement,?propValue);
????}?else?if?(propKey?===?DANGEROUSLY_SET_INNER_HTML)?{
??????setInnerHTML(domElement,?propValue);
????}?else?if?(propKey?===?CHILDREN)?{
??????setTextContent(domElement,?propValue);
????}?else?{
??????setValueForProperty(domElement,?propKey,?propValue,?isCustomComponentTag);
????}
??}
}

這里的 ii + 1 就對應(yīng)著上面提到的 updateQueue 的數(shù)據(jù)結(jié)構(gòu),第 i 項是屬性 key,第 i + 1 項是屬性 value。

commitDeletion

commitDeletion 用于執(zhí)行刪除操作:

function?commitDeletion(
??finishedRoot:?FiberRoot,
??current:?Fiber,
??renderPriorityLevel:?ReactPriorityLevel,
):?void?{
??//?...
??unmountHostComponents(finishedRoot,?current,?renderPriorityLevel);
??//?...
}

unmountHostComponents 中核心是遍歷節(jié)點調(diào)用 commitUnmount 方法。在這個方法中會根據(jù) fiber 節(jié)點的類型做不同的處理。

對于 FunctionComponent,會注冊它的 useEffect 銷毀函數(shù),其實就是把這個 effect 推進(jìn) pendingPassiveHookEffectsUnmount 這個數(shù)組中,便于「后續(xù)」取出來執(zhí)行銷毀函數(shù)。

function?commitUnmount(
??finishedRoot:?FiberRoot,
??current:?Fiber,
??renderPriorityLevel:?ReactPriorityLevel,
):?void?{
??switch?(current.tag)?{
????case?FunctionComponent:?{
??????//?...
??????enqueuePendingPassiveHookEffectUnmount(current,?effect);
??????//?...
??????return;
????}
??}
}

對于 ClassComponent,會執(zhí)行它的 componentWillUnmount 方法:

function?commitUnmount(
??finishedRoot:?FiberRoot,
??current:?Fiber,
??renderPriorityLevel:?ReactPriorityLevel,
):?void?{
??switch?(current.tag)?{
????case?ClassComponent:?{
??????const?instance?=?current.stateNode;
??????if?(typeof?instance.componentWillUnmount?===?'function')?{
????????safelyCallComponentWillUnmount(current,?instance);
??????}
??????return;
????}
??}
}

layout階段

layout階段 會執(zhí)行 commitLayoutEffects 方法,里面核心是執(zhí)行commitLifeCycles方法。在這個方法中會根據(jù) fiber 節(jié)點的類型執(zhí)行不同的處理。

function?commitLifeCycles(
??finishedRoot:?FiberRoot,
??current:?Fiber?|?null,
??finishedWork:?Fiber,
??committedLanes:?Lanes,
):?void?{
??switch?(finishedWork.tag)?{
????case?FunctionComponent:?{
??????commitHookEffectListMount(HookLayout?|?HookHasEffect,?finishedWork);
??????schedulePassiveEffects(finishedWork);
??????return;
????}
??}
}

對于 FunctionComponent,會把 HookLayout 這個 tag 類型傳給 commitHookEffectListMount 方法,也就是說這里會執(zhí)行 useLayoutEffect 的回調(diào)函數(shù)。

接著會執(zhí)行 schedulePassiveEffects 方法:

function?schedulePassiveEffects(finishedWork:?Fiber)?{
??const?updateQueue:?FunctionComponentUpdateQueue?|?null?=?(finishedWork.updateQueue:?any);
??const?lastEffect?=?updateQueue?!==?null???updateQueue.lastEffect?:?null;
??if?(lastEffect?!==?null)?{
????const?firstEffect?=?lastEffect.next;
????let?effect?=?firstEffect;
????do?{
??????const?{next,?tag}?=?effect;
??????if?(
????????(tag?&?HookPassive)?!==?NoHookEffect?&&
????????(tag?&?HookHasEffect)?!==?NoHookEffect
??????)?{
????????enqueuePendingPassiveHookEffectUnmount(finishedWork,?effect);
????????enqueuePendingPassiveHookEffectMount(finishedWork,?effect);
??????}
??????effect?=?next;
????}?while?(effect?!==?firstEffect);
??}
}

在這里會分別注冊 useEffect 銷毀函數(shù)和回調(diào)函數(shù),其實也就是把 effect 分別推進(jìn) pendingPassiveHookEffectsUnmountpendingPassiveHookEffectsMount 這兩個數(shù)組中,用于「后續(xù)」取出來執(zhí)行。

對于 ClassComponent,如果 current 為空,也就是這個節(jié)點是首次 render,則會執(zhí)行它的 componentDidMount 生命周期方法,否則會執(zhí)行 componentDidUpdate 方法:

function?commitLifeCycles(
??finishedRoot:?FiberRoot,
??current:?Fiber?|?null,
??finishedWork:?Fiber,
??committedLanes:?Lanes,
):?void?{
??switch?(finishedWork.tag)?{
????case?ClassComponent:?{
??????const?instance?=?finishedWork.stateNode;
??????if?(finishedWork.effectTag?&?Update)?{
????????if?(current?===?null)?{
??????????instance.componentDidMount();
????????}?else?{
??????????instance.componentDidUpdate(
????????????prevProps,
????????????prevState,
????????????instance.__reactInternalSnapshotBeforeUpdate,
??????????);
????????}
??????}
??????return;
????}
??}
}

commit 階段總結(jié)

  • 等待執(zhí)行完上一輪渲染的 useEffect
  • 重置一些全局變量(如:workInProgressRoot)
  • 更新副作用列表 effect list。根節(jié)點的副作用列表是不包括自身的,如果根節(jié)點有副作用, 則需要把根節(jié)點添加到副作用列表的末尾
  • 渲染
    • 執(zhí)行 componentDidMount、componentDidUpdate
    • 執(zhí)行 useLayoutEffect 的回調(diào)函數(shù)
    • 注冊 useEffect 的回調(diào)函數(shù)和銷毀函數(shù),等 commit 階段結(jié)束后再異步執(zhí)行
    • 執(zhí)行 DOM 增刪改
    • 執(zhí)行 useLayoutEffect 的銷毀函數(shù)
    • 執(zhí)行 getSnapshotBeforeUpdate
    • 調(diào)度 useEffect
    • beforeMutation
    • mutation
    • 切換當(dāng)前 fiber 樹(root.current = finishedWork),使得 fiberRootcurrent 指向的是當(dāng)前頁面展示的 fiber 樹。
    • layout


瀏覽 82
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報
評論
圖片
表情
推薦
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 久久人人做| 亚欧成人| 毛多水多丰满女人A片| 男人天堂无码| 怡春院国产| 狼友视频在线| 欧美成人伦理片网| 亚洲国产剧情| 亚洲狼人久久久精品| 欧美不卡在线观看| 成人免费精品视频| 免费色网站| 国产91精品看黄网站在线观看 | 精品蜜桃一区二区三区| 成人抽插视频| 在线免费观看国产视频| 久久综合无码内射国产| 激情五月婷婷色| 成人黄色av| 亚洲人体视频| 国产性爱网| 3级片网站| 国产亚洲99久久精品| 精品久久无码中文字幕| 欧一美一婬一伦一区二区三区黑人-亚 | 国产一区久久| 国产女人18毛片水18精品| 亚洲日韩欧美视频| 亚洲大片| 国产Av婬乱麻豆| 日本一区二区三区视频在线观看 | 91视频亚洲| 日韩中文字| 大香蕉网站视频| 国产精品天天狠天天看| 九九久久综合| 黄片高清| 中文字幕码精品视频网站| 日本一级婬片A片免费播放一| 国产熟女av| 自拍视频在线观看| 一区二区三区四区五区在线| 久操无码视频| 中文字幕丰满的翔田千里| 国产农村妇女精品一二区| 蜜芽成人在线| 日本人妻A片成人免费看片| 免费在线国产| 免费av中文字幕| 久久AV网站| 久久区| 欧美成人精品一区二区| 国产精品一区二区黑人巨大| 欧美操逼大片| 91丨国产丨熟女熟女| 日韩人妻丰满无码区A片| 91在线看18| 日韩有码中文字幕在线观看| 少妇搡BBBB搡BBB搡造水爽| 成人一级精品| 亚洲视频在线观看网站| 国产精品自拍偷拍| 欧美一级婬片免费视频黄| 亚洲中文字幕在线观看视频网站| 中文字幕精品三区无码| 久草手机视频在线观看| 三级片久久久| 性欧美一区二区| 精品亚洲一区二区三区四区五区| 欧美色图另类| 欧美www| 国产亚洲精品久久久久久桃色| 日韩免费在线播放| 肉片无遮挡一区二区三区免费观看视频 | 精品一区二区三区免费| 成人在线视频免费| 天天搞搞| 一区二区无码区| 欧美成人精品一区二区| 亚洲影音先锋在线| 亚洲精品一二三区| 无毛片| 人妻无码一区二区三区| 免费一二区| 丁香婷婷久久久综合精品国产 | 国产一级a毛一级a做免费图片| 色婷婷AV| 97人妻一区二区精品视频| 国产欧美精品一区二区| 德国肥妇熟妇BBwBBw| 无码精品人妻| 国产成人三级| 成人AV一AV二| 欧美色图综合网| 天天干天天操综合| 日韩在线免费看| 婷婷色在线视频| 黃色A片一級二級三級免費久久久| 99久久久无码国产精品性波多| 天堂毛片| 国产精品99久久免费黑人人妻 | 无码乱| 国产精品秘麻豆果冻传媒潘甜甜丶| 黄色一级在线| 91爱搞| 综合站欧美精品| 日本黄色视频免费| 国产高清在线观看| 国产精品国产三级国产| 国产www在线观看| 亚洲超级高清无码第一在线视频观看 | 亚洲午夜电影| 91AV在线播放| 婷婷五月天色播| 精品福利视频导航| 99无码精品| 中文字幕在线不卡| 中文一区二区| 精品视频在线观看免费| H片在线观看| 草逼视频网站| 大香蕉伊人久久| 亚洲精品乱码久久久久久久| 中文字幕成人在线| 狠狠操2019| 国产www| 成人丁香五月| 国内精品国产成人国产三级| 91在线永久| AV无码在线观看| 麻豆视频一区二区三区| 亚洲aaa| 中文字幕一区二区三区在线观看| 三级久久| 日韩人妻丝袜中文字幕| 91在线观看| 毛片A级| 就去色色五月丁香婷婷久久久| 99热在线只有精品| 性猛交╳XXX乱大交| 亚洲国产天堂| 成人尤物网站| 大香蕉性爱| 一区二区三区无码精品| 成人无码精品| 欧美在线无码| 欧美区在线观看| 色综合久久天天综合网| 看A片在线| 日本一区二区三区免费视频| 欧美亚洲成人在线观看| 伊人色女操穴综合网| 午夜AV电影| 亚洲成年人网| 欧美国产精品一二三产品在哪买| 中文字幕12页| 午夜做爱福利视频| 日本伊人大香蕉| 瑟瑟免费视频| 欧美操B视频| 91人人人人| 国产成人精品视频免费看| 人人澡人人爽人人精品| 91无码精品国产AⅤ| 国产成人久久777777黄蓉| 午夜福利成人网站| 久热在线资源福利站| 欧美国产激情| 啊啊啊啊啊在线观看| 无码一区精品久久久成人| 在线观看无码av| AV在线播放中文字幕| 九色精品| 亚洲AV综合网| 色五月视频在线| 国产乱妇无码毛片A片在线看下载 日韩电影免费在线观看中文字幕 欧美性爱中文字幕 | 欧亚毛片| 黄色av免费观看| 国产一级特黄大片| 天天干天天干| 色婷婷色五月| 99久久99久久兔费精桃| 99cao| 性爱视频免费网站| 一区二区三区免费播放| 日本色影院| 成人777| 久久久久久毛片| av黄色网址| 91av视频| 色婷婷一区二区| 久久精品偷拍视频| 大香蕉伊人电影| 亚洲精品视频在线| 日韩精品视频一区二区| 日韩无码AV一区二区三区| 日韩av在线不卡| 欧美成人在线网站| 超碰福利导航| 四色婷婷| 亚洲有码中文字幕| 午夜AV影院| 狠狠干狠狠爱| 11一12周岁女毛片| 在线观看三级网址| 在线黄色网| 国内成人AV| 天堂在线8| 亚洲综合网在线| 天天干天天干天天| 嫩草AV| 撒尿BBw搡BBwBBw| 97资源在线视频| 日本不卡一区二区三区四区| 久久久999| 亚洲一级黄色视频| 热久久伊人| 亚洲AV女人18毛片水真多| 青娱乐国产AV| 高清无码免费视频| 操逼无码视频| 激情五月天网址| 人妻制服丝袜| 一级a免一级a做免费线看内裤| 中文字幕高清无码在线播放| 亚洲v| 黑人操白人| caopeng97| 亚洲网站免费观看| 国产精品国产三级国产AⅤ| 亚洲国际中文字幕在线| 日本人妻视频| 亚洲欧美另类在线| av午夜福利| 黑人巨大精品欧美| 四川BBB嫩BBBB爽BBBB| 中文字幕在线播放av| 最新中文字幕观看| 高清一区二区| 操人妻视频| 蜜桃久久久| 91丨九色丨蝌蚪丨成人| 91精品电影| 毛片A级| 免费观看一级毛一片| 欧美+日产+中文| 久久久www成人免费毛片| 超级碰碰碰碰碰碰碰碰碰| 精产国品一区二区区别| 翔田千里一区二区| 大香蕉尹在线| 久久毛片视频| 四虎在线视频| 国产嫩苞又嫩又紧AV在线| 日批动态图| 亚洲AV毛片成人精品网站| 欧美男女操逼视频| 91一级A片在线观看| 国产精品欧美性爱| 欧美日本色| 国产在线欧美在线| 亚洲天堂无码| 中文字幕乱码亚洲中文在线| 亚洲一区二区在线| 黄色视频日本| 成人视频网站在线观看| 久久丁香| 五十路在线| 9999久久久久| 一级欧美一级日韩| 日韩国产在线| 国产无码免费视频| 狠狠色婷婷| 一区二区高清无码| 天天色综| 亚洲精品区| 97超碰免费| 欧美精品久久久| 欧美日韩国产在线观看| 五月丁香在线视频| 中文字幕亚洲在线| 欧美视频一区| 韩国深夜福利视频| 国产熟女一区二区久久| 人妻中文字幕网| 国产喷水ThePorn| 91无码成人视频| 中文字幕免费久久| 做爱的网站| 麻豆国产成人AV一区二区三区| 一本久久A精品一合区久久久| 人人操人人摸人人射| 在线视频三区| 亚洲AV久久无码| 学生妹一级大片| 欧美日韩国产成人在线观看| 无码69| 欧美日韩成人网站| 精品AAA| 国语精品自拍| 国产1区2区3区中文字幕| 日韩WWW| 亚洲无码高清一区| 丁香五月婷婷网| 亚洲久久久久| 色婷婷综合网| 免费精品黄色网页| 欧美福利电影| 国产又爽又黄网站免费观看| 最近日本中文字幕中文翻译歌词| NP玩烂了公用爽灌满视频播放| 婷婷五月欧美| 日韩a电影| 亚洲51| 亚洲女与黑人正在播放| 欧美日本一区二区三区| 我要看黄色一级片| 日本爱爱免费播放视频| 韩国中文无码| 五月天欧美性爱| 色色成人网| 91亚洲精品久久久久蜜桃| 淫香欲色| 秋霞福利影院| 97大香蕉视频| 性爱视频免费网站| 操一操| 五月天婷婷在线播放视频免费观看| 手机看片1024你懂的| 精品女人| 熟妇人妻中文AV无码| 国产激情综合| 中文字幕永久在线5| 蜜桃在线无码| 亚洲操逼电影| 婷久久| 九色在线视频| 人人干人| 另类无码| 精品在线第一页| 一区二线视频| 青娱在线视频| 天天激情站| 色综合天天综合网国产成人网| 亚洲无码一| 9991区二区三区四区| 蜜桃AV在线观看| 亚洲中文字幕在线视频观看| av无码在线播放| 精品国产va久久久久久| 超碰碰碰| av资源在线看| 免费高清无码视频| 国产精品一区网站| 日韩色情网| 国产精品夜夜爽7777777 | brazzers疯狂作爱| 国产又爽又黄视频在线看| 国产熟女一区二区| 亚洲AV免费在线观看| 一级国产片| 黄色一级片免费观看| 欧美色图另类| 日韩无码专区电影| BBw日本熟妇BBwHD| 天天射天天爽| 最新AV在线播放| 国产精品成人在线视频| 波多野结衣东京热| 丁香婷婷色五月激情综合三级三级片欧美日韩国 | 天天撸天天射| 特黄AAAAAAAA片视频| 亚洲深夜福利| 日韩中文字幕人妻| 秋霞一区二区三区无码| 欧美日韩国产91| 欧美性受XXXX爽XYX熟99| 国产系列每日更新| 国产成人三级视频| 91久久久久久久| 欧美不卡在线播放| 亚洲无码一区在线| 一级片| 国产一区二区免费在线观看| 精品蜜桃秘一区二区三区在线播放| 欧美激情在线| 国产传媒一区| 黄色成人在线| 100国产精品人妻无码| 99热亚洲| 牛牛在线精品视频| 日韩人妻无码视频| 国产在线看片| 77777免费观看电视剧推荐爱的教育 | 亚洲高清无码网站| 青青草原av| 婷婷综合av| ppypp电影频道| 黑人无码一二三四五区| 日韩在线视频中文字幕码无| 久久婷婷视频| 久久久91人妻无码精品蜜桃ID| www.大鸡巴| 国产凹凸视频在线观看| 69Av视频| 日韩人妻精品无码久久| 欧美熟妇擦BBBB擦BBBB| 安徽妇搡BBBB搡BBBB| 91狠狠爱| 亚洲中文欧美| 狠狠色一区| AV青青草| 国产欧美一区二区三区视频| 91艹逼| 影音先锋成人电影| 亚洲网站免费观看| 黄片www| 五十路老国产| 99热综合| 99精品视频16在线免费观看| 欧美中文字幕在线| 东北操逼视频| 婷婷大香蕉| 日韩第1页| 国产日韩欧美| 91人人操人人爽| 懂色成人Av| 蜜桃精品视频在线观看| 黄片高清免费观看| 五月丁香性爱| 成人影片在线观看18| a视频免费在线观看| 久久精品99国产国产精| 婷婷五月天在线播放| 成人免费爱爱视频| 北条麻妃无码在线播放| 亚洲ww国产a大作| 国产成人久久| 西西人体44www大胆无码| 美女视频一区二区三区| 五月天激情导航| 久久免费成人| 成人激情五月天| 国产成人片在线观看| 麻豆91视频| 欧美国产综合| 青青草人人| 国内精品久久久久| 91密臀| 中文字幕免费在线| 乱子伦国产精品视频一级毛| 免费三区| 一级黄色免费电影| P站免费版-永久免费的福利视频平台| 欧美黄色电影网站| 少妇厨房愉情理伦BD在线观看| 成人免费区一区二区三区| 狼友视频在线免费观看| 北条麻妃成人视频| 婷婷涩嫩草鲁丝久久午夜精品| 国产黄色视频在线免费看| 国产AAA片| 成人在线视频播放| 国产黄色视频免费看| 黄色av无码| 91爱爱网| 天天天天日天天干| 一本一道vs波多野结衣| 国产在线中文字幕| 69成人在线电影| 亚卅毛片| 国产黄A片免费网站免费| 国产熟睡乱子伦午夜视频_第1集 | 羞羞视频com.入口| 亚洲天堂国产视频| 成人激情视频| 日韩免费在线播放| 新BBWBBWBBWBBW| 嫩草Av| 欧美一级性爱视频| 中文字幕在线播放第一页| 俺去也| 亚洲小电影在线| 日韩无码人妻一区| 天天操人人妻| 五月天成人社区| 国产A片免费观看| 亚洲欧美成人在线视频| 久久黄片视频| AAA片网站| 大香蕉中文在线| 日韩无码影视| 国产美女被爽到高潮免费A片软件| 大香蕉毛片| 国产精品无码专区AV免费播放 | 欧美日韩国产精品成人| 中文字幕一级A片高清免| 亚洲免费MV| 79色色| 色五月在线视频| 18禁无码网站| 蜜桃av.38| 全国最大成人网| 国产精品秘久久久久久一两个一起| 国产黄色a片| AV在线天堂| 无码人妻中文字幕| 成人免费黄色视频网站| 日韩黄色电影在线| 蜜桃精品视频| 北条麻妃日B视频| 成人在线综合| 北条麻妃A片在线播放| 成人在线视频免费观看| 午夜A区| 成人午夜在线观看| 亚洲AV无码成人H动漫| 私人玩物』黑絲OL尤物| 国产传媒自拍| 在线成人免费视频| 免费无码一级A片大黄在线观看| 国产无码a| 白浆四溢av| 91狠狠综合久久久| 亚洲美女免费视频| 久久色片| 国产aaaaaaaaaaaaa| 国产又爽又黄免费视频免费观看| 91女人18毛片水多的意思| 草久免费视频| 欧美成人免费观看| 久久综合成人| 亚洲精品无码视频| 国产在线观看国产精品产拍| 美女黄色网| 黄页视频网站| 成人手机在线视频| 内射学生妹视频| 午夜亚洲国产一区视频网站| 牛牛AV| 国产精品黄片| 国产精品视频| 日本黄色免费网站| 欧美午夜精品| 色噜噜噜| www黄色视频| 亚洲精品成人电影| 亚洲欧美一区二区三区在线| 国产精品成人无码a无码| 精品成人A片久久久久久不卡三区| 一区二区三区久久久久| 人人天天久久| 99免费小视频| 日韩免费毛片| 日韩无码网址| 国产精品无码一区二区三区免费| 亚洲中文字幕av| 国产麻豆电影在线观看| 日本一区二区视频在线观看| 91人妻人人澡人人爽人人玩 | 国产99久久久精品| 国产欧美欧洲| AAAA毛片视频| 黄片AV| 97超碰在线免费观看| 国产一卡二卡在线观看| 欧美午夜成人一区二区三区| 久久久久久黄色| 九色PORNY丨自拍蝌蚪| 影音先锋黄色资源| 亚洲色成人网站www永久四虎| AV在线不卡中文| 国产成人片| 无码国产精品一区二区免费96| 四虎一区二区| 7777影视电视剧在线观看官网 | 中文字幕国产av| 尤物AV| 色欲色欲一区二区三区| 国产精品毛片A√一区| 亚洲AV无码A片在线观看蜜桃| 黄色一级片视频| 在线观看免费成人网站| 成人网站无码| 狼友视频免费在线观看| 国产日韩91| 国产成人97精品免费看片| 在线免费观看黄片| 熟妇高潮| 超碰1999| 亚洲中文无码在线观看| 久久久精品电影91| 国产三级黄色视频| 亚洲理论片| 91天堂网| 欧美激情综合网| 成人午夜在线视频| www.污污污| 国产AV二区| 午夜精品18视频国产| 在线中文字幕777| 蜜桃传媒一区二区| 麻豆国产91在线播放| 日韩精品网| 免费操逼网| 欧美亚洲日韩一区二区三区| 日韩成人黄片| 国产熟妇码视频app| 羞羞av| 大香蕉综合伊人| 在线天堂v| 欧美成人看片| 欧美日韩中文字幕| 制服.丝袜.亚洲.中文豆花| 欧美69视频| 91av免费观看| 中文字幕+乱码+中文乱码电影 | 成人做爰黄A片免费| 国产第七页| www.91在线视频| 中文字幕www一区| av三级片在线播放| 日国无码| 久久午夜无码鲁丝片午夜精品偷窥| 91嫩草欧美久久久九九九| 无码高清一区二区| 狼友视频在线播放| 日韩综合久久| 无码成人A片在线观看| 欧美黄色网址| 肏屄视频免费| 亚洲成人无码在线播放| 欧美国产视频| 国产美女AV| 好操吊| 国产成人网站免费观看| 国产精品理论片| 91精品人妻一区二区三区四区| 久久av网站| 国产精品对白| 国产日韩在线播放| 日韩三级一区二区| 亚洲aaa在线| 久艹久| 熟妇人妻久久中文字幕| 欧美亚洲国产精品| 小H片在线观看| 日韩免费福利视频| 久久欧洲成人精品无码区| 无码一区精品久久久成人| 色男人天堂| 在线人妻| 成人视频在线免费观看| AV无码在线观看| 一级性爱视频| 久操大香蕉| 亚洲视频区| 久草中文在线| 五月丁香在线| 亚洲AV无码乱码| 久久一级片| 大香蕉在线视频99| 国产码在线成人网站| 亚洲aaa| 久久大香蕉视频| 欧美99| 日韩综合色视频导航| 丰臀肥逼高清视频电影播放| 骚白虎一区| 中文字幕永久在线5| 国产吃奶| 丁香四月婷婷| 蜜臀99| 国产久久视频在线观看| 国产精品扒开腿| AV电影天堂网| 久久免费毛片| 国产精品宾馆| 一区黄片| 日韩一级免费在线观看| 天堂a√在线8| 国产精品theporn| 免费国产A片| 亚洲成人AV无码| 中文字幕乱码中文字幕| 高清AV在线| 看毛片网址| 无码一区三区| 97久久久| 亚洲第一福利视频| 影音先锋一区二区| 久久福利| 男同人到爽无套狂欢| 在线观看三级网址| 波多野结衣中文字幕久久| 成人国产片| 久久久精品淫秽色情| 在线观看中文字幕一区| 日韩亚洲精品中文字幕| 怡春院视频| 亚洲一区二区三区在线视频| 欧美亚洲自拍偷拍| 91麻豆精品国产91久久久吃药 | 亚洲欧洲在线视频| 国产精品视频免费观看| 亚洲欧美大香蕉视频网| 欧美精品久久久久久久久爆乳| 思思热精品在线| 操B视频在线| 精品成人网| www国产在线| 国产黄色视频免费在线观看| 亚洲成人无码精品| 久久久久9| 人成视频在线免费观看| 免费黄色成人视频| 欧美日韩免费看| 999精品视频| 欧美亚洲色色网视频| 玖玖热在线视频| 久久久久久久久免费视频| 一本一道久久综合| 成人高清无码在线| 91成人在线播放| 日本国产精品| 亚洲欧美在线观看视频| 一级操逼| 亚洲天堂AB| 俩小伙3p老熟女露脸| 日韩无码黄色电影| 一本一道久久综合| 欧美操逼在线| 日本无码在线视频| 91精品久久久久久粉嫩| 夜夜爽久久精品91| 中文字幕免费高清| 国产和日韩中文字幕| 亚洲AV片一区二区三区| 怡红院一区二区| 人人射网站| 亚洲成人无码在线播放| www.91在线| 国产av一级| www.中文字幕| 人妻少妇被猛烈进入中文字幕| 丁香花免费高清视频小说完整| 亚洲色涩| 男女操逼视频网站| 日韩中文字码无砖| 超碰手机在线| 俺也来俺也去| 国产成人97精品免费看片| AV操逼网| 国产成人网站免费观看| 色综合激情| 亚洲自拍电影| 蜜桃视频一区| 六月丁香欧美综合| 国产欧美精品一区二区三区| 97人人爽人人爽人人爽人人爽| 国产成人免费在线视频| 久久久久逼| 国产粉嫩| 午夜色色福利| 中日韩中文字幕一区二区区别| 无码三级av| 久久久久一区| 免费观看黄色片| 国产成人三级| 偷偷操av| 精品国产香蕉| 无码福利视频| 影音先锋国产精品| 亚洲精品自拍| 国产精品国内自产拍| 无码中文综合成熟精品AV电影 | 苗条一区小视频| 天天干天天在线观看| 日本一级黄色| 91在线综合| 日本精品码喷水在线看| 国产成人+综合亚洲+天堂| 爱逼爱操| 水蜜桃成人网| 毛片毛片毛片毛片毛片| 国产精品美女毛片真酒店| 日本三级网站| 人与鲁牲交| 自拍偷拍图区| 日日夜夜拍| 亚洲精品秘一区二区三线观看| 大香蕉福利视频导航| 国产亚洲视频免费观看| 免费操B| 亚洲精品一区二区三| 大香蕉777| 波多野结衣无码NET,AV| 成人黄色免费在线| 超小超嫩国产合集六部| 五月婷婷六月激情| 国产精品久久AV电影| 婷婷视频网| 大地影视中文第三页最新在线观看 | 欧美亚洲成人电影| 亚洲A级片| 综合伊人| 成人五月天黄色电影| 欧美生活片18| 人人草人人操| 亚洲AV无码乱码精品| AV女优天堂| 国产免费观看视频| 国产成人精品免高潮在线人与禽一| 日日AV| 欧美福利电影| 91九色TS另类国产人妖| 中文字幕不卡一区| 日韩黄色网| 久亚洲| 熟女在线视频| 在线观看黄色AV| 北条麻妃视频| 一级色色| 大香蕉三级| 日韩免费高清视频| 欧美午夜精品一区二区蜜桃| 黄色视频网站在线播放| 日韩大屌| 九九热在线视频| 日韩成人AV在线播放| 午夜视频在线看| 亚洲成人网在线观看| 熟女一区| 亚洲Av无码午夜国产精品色软件 | 一区二区三区无码在线观看| 全部视频午夜寂寞| 91精品人妻一区二区三区四区| 久久久久无码精品国产91福利 | 大香蕉天天操| 亚洲综合免费观看高清完整版 | 久久国产一区二区| 日韩成人网站在线观看| 成人无码区免费AV毛片| 国产人成视频免费观看| 一级黄色生活片| 青青草国产亚洲精品久久| 99热最新| 91精品国产综合久久久久久久| 日韩精品一区二区三区黄冈站长 | AA片免费网站| 国产免费一区二区三区免费视频| 韩国无码高清视频| 91亚洲高清| 九九毛片| 国产丰满大乳无码免费播放| 欧美五月激情| 水蜜桃一区二区| 亚洲狼人久久久精品| 自拍偷拍综合网| 中文字幕在线观看亚洲| 麻豆18禁| 人人摸人人操人人射| 熟妇人妻中文字幕无码老熟妇| 一本久道综合| 神马午夜精品91| 秋霞一级| 日韩中文字幕无码人妻| 九九韩剧网最新电视剧免费观看 | 高清无码不卡视频| 亚洲永久视频| 国产香蕉视屏| 熟女少妇一区二区| 久久精品9| 国产精品久久久久久久免牛肉蒲 | 99久久99久久| 欧美在线观看视频| 蜜桃免费| www.99视频| 欧美五月在线网址| 成人色色| 色天堂网| 亚洲无码A片在线| 国产精品一区二区三区在线| 亚洲色久| 黄色成人网站大全| 一级黄色片免费| 国产精品黄片| 在线播放91灌醉迷J高跟美女| 操人网站| 大香蕉亚洲成人| 中文字幕内射| 色哟哟精品| 青青操原| 成人视频一区| 亚洲综合区| aav在线| 成人激情综合网| 黄色成人视频网站在线观看| www.黄色com| 在线播放你懂的|