原文地址:github.com/HuJiaoHJ/bl…node
本文源碼是2018年8月10日拉取的React倉庫master分支上的代碼react
React源碼分析內容很是多,本文專一在如下兩個問題:git
在開始源碼分析以前,首先先簡單介紹一下React的一些基礎概念github
React定位是一個構建用戶界面的JavaScript類庫,使用JavaScript開發UI組件,支持多種方式渲染組件,輸出用戶界面。web
React常見的三種應用類型:算法
這三種應用分別對應三種不一樣的渲染方式:數組
下面,以 React Web應用 爲例,介紹下React三個主要組成部分:瀏覽器
在開始 Reconciliation 模塊以前,先簡單介紹各個模塊:緩存
const React = {
Children: {...},
createRef,
Component,
PureComponent,
createContext,
forwardRef,
Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
unstable_Profiler: REACT_PROFILER_TYPE,
createElement,
cloneElement,
createFactory,
isValidElement,
version: ReactVersion,
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
};
複製代碼
從上面的源碼能夠看到,React基礎模塊只包括了基礎的API和組件相關的定義。如:createRef、Component等。babel
其中能夠重點關注的兩點:
在平時的開發中,咱們使用的JSX語法,因此咱們並無直接接觸到 React.creatElement 方法
你們都知道,JSX語法會被babel編譯成調用 React.creatElement 方法,以下:
而 React.creatElement 最終返回的是 React Element,數據結構以下:
{
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
}
複製代碼
能夠在頁面中把 <App/>
打印出來,以下:
組件是咱們開發使用最多的,咱們能夠簡單的看下源碼:
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
複製代碼
從Component的定義上能夠看到,咱們經常使用的 setState 方法是調用了 updater.enqueueSetState,以 react-dom 爲例,此 updater 對象會調用該組件構造函數時(這塊會在後面的生命週期函數調用中講到),賦值爲classComponentUpdater,源碼以下:
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
...
},
enqueueReplaceState(inst, payload, callback) {
...
},
enqueueForceUpdate(inst, callback) {
...
},
};
複製代碼
能夠知道,組件中調用 setState 實際上是調用的 classComponentUpdater.enqueueSetState 方法,這裏就是開始 setState 的入口
至此,就簡單的介紹了React基礎模塊,下面開始介紹渲染模塊:react-dom
const ReactDOM: Object = {
createPortal,
findDOMNode(
componentOrElement: Element | ?React$Component<any, any>,
): null | Element | Text {
...
},
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
return legacyRenderSubtreeIntoContainer(null, element, container, true, callback,);
},
render(element: React$Element<any>, container: DOMContainer, callback: ?Function,) {
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback,);
},
...
};
複製代碼
這裏咱們能夠關注下 render 方法,全部 react web應用入口都會調用 ReactDOM.render(),本文也會從 ReactDOM.render() 開始進行源碼的分析
在進行源碼分析以前,先介紹下本文的核心:Reconciliation模塊
Reconciliation模塊又叫協調模塊,而咱們題目上說的 React Fiber
則是在這個模塊中使用一種調度算法
React Fiber調度算法又叫 Fiber Reconciler,是 React 16 啓用的一種新的調度算法,是對核心調度算法(Stack Reconciler)的重構
React 16版本以前使用的 Stack Reconciler 調度算法,它經過遞歸的形式遍歷 Virtual DOM,存在難以中斷和恢復的問題,若是react更新任務運行時間過長,就會阻塞佈局、動畫等的運行,可能致使掉幀。它的調用棧以下:
容許渲染過程分段完成,而沒必要須一次性完成,中間能夠返回至主進程控制執行其餘任務,它有以下新特性:
它的調用棧以下:
關於React新老調度算法的對比,你們能夠看看:zhuanlan.zhihu.com/p/37095662
關於React Fiber概念的再詳細的介紹,你們能夠看看:www.ayqy.net/blog/dive-i…
以上,就對React的基本概念進行了介紹,接下來開始源碼分析~
React Fiber架構引入了新的數據結構:Fiber節點
Fiber節點數據結構以下:
export type Fiber = {|
// Tag identifying the type of fiber.
tag: TypeOfWork,
// Unique identifier of this child.
key: null | string,
// The function/class/module associated with this fiber.
type: any,
// The local state associated with this fiber.
stateNode: any,
// Remaining fields belong to Fiber
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
// The ref last used to attach this node.
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.
// A queue of state updates and callbacks.
updateQueue: UpdateQueue<any> | null,
// The state used to create the output
memoizedState: any,
// A linked-list of contexts that this fiber depends on
firstContextDependency: ContextDependency<mixed> | null,
mode: TypeOfMode,
// Effect
effectTag: TypeOfSideEffect,
// Singly linked list fast path to the next fiber with side-effects.
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
expirationTime: ExpirationTime,
childExpirationTime: ExpirationTime,
alternate: Fiber | null,
actualDuration?: number,
actualStartTime?: number,
selfBaseDuration?: number,
treeBaseDuration?: number,
|};
複製代碼
Fiber樹結構圖(鏈表結構)以下:
咱們看張圖:
React組件渲染分爲兩個階段:reconciler、render。從圖上能夠看到:
在上面的基礎概念介紹中有提到,react-dom模塊負責react web應用的渲染工做,那麼Reconciliation模塊(協調模塊)具體作了什麼工做呢?
Reconciliation模塊的工做能夠分爲兩部分:
一、reconciliation
簡單來講就是找到須要更新的工做,經過 Diff Fiber Tree 找出要作的更新工做,這是一個js計算過程,計算結果能夠被緩存,計算過程能夠被打斷,也能夠恢復執行
因此,上面介紹 Fiber Reconciler 調度算法時,有提到新算法具備可拆分、可中斷任務的新特性,就是由於這部分的工做是一個純js計算過程,因此是能夠被緩存、被打斷和恢復的
二、commit
提交更新並調用對應渲染模塊(react-dom)進行渲染,爲了防止頁面抖動,該過程是同步且不能被打斷
下面咱們來看看這兩個階段具體的函數調用流程
咱們以 ReactDOM.render() 方法爲入口,來看看reconciliation階段的函數調用流程:
從圖中能夠看到,我把此階段分爲三部分,分別以紅線劃分。簡單的歸納下三部分的工做:
一、第一部分從 ReactDOM.render() 方法開始,把接收的React Element轉換爲Fiber節點,併爲其設置優先級,記錄update等。這部分主要是一些數據方面的準備工做。
二、第二部分主要是三個函數:scheduleWork、requestWork、performWork,即安排工做、申請工做、正式工做三部曲。React 16 新增的異步調用的功能則在這部分實現。
三、第三部分是一個大循環,遍歷全部的Fiber節點,經過Diff算法計算全部更新工做,產出 EffectList 給到commit階段使用。這部分的核心是 beginWork 函數。
第一部分較爲簡單,這裏就不詳細介紹了,小夥伴們可自行閱讀源碼~
三部曲:scheduleWork、requestWork、performWork(安排工做、申請工做、正式工做)
在三部曲中的 requestWork函數中,會判斷當前任務是同步仍是異步(暫時React的異步調用功能還在開發中,未開放使用,本文後續內容是以同步任務爲例),而後經過不一樣的方式調用任務。同步任務直接調用performWork函數當即執行,而異步任務則會在後面的某一時刻被執行,那麼異步任務是怎麼被調度的呢?
異步任務調度有兩種方式,主要是經過該任務的優先級進行判斷,主要有兩種:
一、animation(動畫):則會調用 requestAnimationFrame API 告訴瀏覽器,在下一次重繪以前調用該任務來更新動畫
二、其餘異步任務:則會調用 requestIdleCallback API 告訴瀏覽器,在瀏覽器空閒時期依次調用任務,這就可讓開發者在主事件循環中執行後臺或低優先級的任務,並且不會對像動畫和用戶交互等關鍵的事件產生影響
以上兩個API都是原生API,想深刻了解的能夠看看:requestAnimationFrame、requestIdleCallback
而原生requestIdleCallback存在兼容性問題,因此React自己開發了 ReactScheduler模塊 來實現這個功能
後續會以同步任務爲例,因此咱們開始介紹第三部分的核心函數:beginWork
從上面的函數調用流程圖能夠看到,beginWork在大循環中被調用,返回當前節點的子節點。
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
if (!hasLegacyContextChanged() && (updateExpirationTime === NoWork || updateExpirationTime > renderExpirationTime)) {
switch (workInProgress.tag) {
case HostRoot:
...
case HostComponent:
...
case ClassComponent:
pushLegacyContextProvider(workInProgress);
break;
case HostPortal:
...
case ContextProvider:
...
case Profiler:
...
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderExpirationTime,);
}
// Before entering the begin phase, clear the expiration time.
workInProgress.expirationTime = NoWork;
switch (workInProgress.tag) {
case IndeterminateComponent:
return mountIndeterminateComponent(current, workInProgress, renderExpirationTime,);
case FunctionalComponent:
return updateFunctionalComponent(current, workInProgress, renderExpirationTime,);
case ClassComponent:
return updateClassComponent(current, workInProgress, renderExpirationTime,);
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
case PlaceholderComponent:
return updatePlaceholderComponent(current, workInProgress, renderExpirationTime,);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderExpirationTime,);
case ForwardRef:
return updateForwardRef(current, workInProgress, renderExpirationTime);
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderExpirationTime,);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderExpirationTime,);
default:
...
}
}
複製代碼
首先,先介紹一下React Fiber架構的雙緩衝技術:
從上圖能夠看到有兩顆 Fiber Tree:current、workInProgress,它們之間是經過每一個Fiber節點上的alternate屬性聯繫在一塊兒,能夠查看源碼ReactFiber.js中的 createWorkInProgress 方法,以下:
export function createWorkInProgress( current: Fiber, pendingProps: any, expirationTime: ExpirationTime, ): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode,);
...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.effectTag = NoEffect;
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
...
}
...
return workInProgress;
}
複製代碼
以上代碼爲簡化以後的,能夠發現,current與workInProgress互相持有引用。而從上圖能夠發現,全部更新都是在workInProgress上進行操做,等更新完畢以後,再把current指針指向workInProgress,從而丟棄舊的Fiber Tree
從beginWork源碼來看,主要分爲兩部分,一部分是對Context的處理,一部分是根據fiber對象的tag類型,調用對應的update方法。在這裏咱們重點關注第二部分。而在第二部分中,咱們以 ClassComponent類型 爲例,講講 updateClassComponent函數 中作了什麼呢?
主要有兩部分:生命週期函數的調用及Diff算法
流程圖以下:
current爲null,意味着當前的update是組件第一次渲染
一、調用 constructClassInstance 構造組件實例,主要是調用 constructor
構造函數,並注入classComponentUpdater(這塊就是文章一開始介紹React Component時提到的updater注入)
二、mountClassInstance 則是調用 getDerivedStateFromProps
生命週期函數(v16) 及 UNSAFE_componentWillMount
生命週期函數
current不爲null,調用 updateClassInstance 方法
一、若是新老props不一致,則會調用 UNSAFE_componentWillReceiveProps
生命週期函數
二、而後調用 shouldComponentUpdate
生命週期函數,得到shouldUpdate值,若未定義今生命週期函數,默認爲true(是否從新渲染),若是shouldUpdate爲true,則會調用 UNSAFE_componentWillUpdate
生命週期函數
最後調用 finishClassComponent 方法,那麼 finishClassComponent函數 中作了什麼呢?流程圖以下:
若是 shouldUpdate 爲false,表示不須要更新,直接返回
若是 shouldUpdate 爲true,調用實例的 render
方法,返回新子節點
若是是首次渲染,調用 mountChildFibers 建立子節點的Fiber實例
不然,調用 reconcileChildFibers 對新老子節點進行Diff
執行到了這,updateClassComponent函數主要是執行了組件的生命週期函數,下面講講須要對新老子節點進行Diff時使用的Diff算法
reconcileChildFibers函數 中,源碼以下:
function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, expirationTime: ExpirationTime, ): Fiber | null {
const isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFirstChild, newChild, expirationTime,),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(returnFiber, currentFirstChild, newChild, expirationTime,),
);
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, expirationTime,),
);
}
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, expirationTime,);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, expirationTime,);
}
if (isObject) {
throwOnInvalidObjectType(returnFiber, newChild);
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
複製代碼
reconcileChildFibers函數中主要是根據newChild類型,調用不一樣的Diff算法:
一、單個元素,調用reconcileSingleElement
二、單個Portal元素,調用reconcileSinglePortal
三、string或者number,調用reconcileSingleTextNode
四、array(React 16 新特性),調用reconcileChildrenArray
前三種狀況,在新子節點上添加 effectTag:Placement,標記爲更新操做,而這些操做的標記,將用於commit階段。下面以單個元素爲例,講講具體的Diff算法
reconcileSingleElement函數源碼以下:
function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, expirationTime: ExpirationTime, ): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// 判斷key是否相等
if (child.key === key) {
if (child.tag === Fragment ? element.type === REACT_FRAGMENT_TYPE : child.type === element.type) {
// key相等且type相等,刪除舊子節點的兄弟節點,複用舊節點並返回
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.type === REACT_FRAGMENT_TYPE ? element.props.children : element.props, expirationTime,);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
} else {
// key相等但type不相等,刪除舊子節點及兄弟節點,跳出循環
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
// key不相等,刪除此舊子節點,繼續循環
deleteChild(returnFiber, child);
}
// 繼續遍歷此舊子節點的兄弟節點
child = child.sibling;
}
// 不能複用,則直接新建Fiber實例,並返回
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(element.props.children, returnFiber.mode, expirationTime,
element.key,);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, expirationTime,);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
複製代碼
具體過程在代碼的註釋中寫的比較清楚,在這就不詳細展開。不過咱們能夠看看 deleteChild(刪除子節點)中,具體作了什麼,源碼以下:
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) {
return;
}
const last = returnFiber.lastEffect;
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
childToDelete.nextEffect = null;
childToDelete.effectTag = Deletion;
}
複製代碼
能夠看到,deleteChild 刪除子節點並非真的刪除這個對象,而是經過 firstEffect、lastEffect、nextEffect 屬性來維護一個 EffectList(鏈表結構),經過 effectTag 標記當前刪除操做,這些信息都會在 commit 階段使用到
以上,就是beginWork函數的整個過程,能夠知道遍歷完Fiber樹以後,經過Diff算法,能夠產出 EffectList,給commit階段使用
函數調用流程圖以下:
commit階段作的事情是拿到reconciliation階段產出的EffectList,即全部更新工做,提交這些更新工做並調用渲染模塊(react-dom)渲染UI。
在前面也提到,commit階段會經過 effectTag標記 識別操做類型,因此咱們先來看看 effectTag 有哪些類型:
// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /* */ 0b00000000000;
export const PerformedWork = /* */ 0b00000000001;
// You can change the rest (and add more).
export const Placement = /* */ 0b00000000010;
export const Update = /* */ 0b00000000100;
export const PlacementAndUpdate = /* */ 0b00000000110;
export const Deletion = /* */ 0b00000001000;
export const ContentReset = /* */ 0b00000010000;
export const Callback = /* */ 0b00000100000;
export const DidCapture = /* */ 0b00001000000;
export const Ref = /* */ 0b00010000000;
export const Snapshot = /* */ 0b00100000000;
// Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /* */ 0b00110100100;
// Union of all host effects
export const HostEffectMask = /* */ 0b00111111111;
export const Incomplete = /* */ 0b01000000000;
export const ShouldCapture = /* */ 0b10000000000;
複製代碼
能夠看到:
一、effectTag類型是使用二進制位表示,能夠多個疊加
二、經過位運算匹配effectTag類型
從上面的流程圖,能夠看到commit階段有比較重要的三個函數:
此函數主要是保存當前DOM的一個快照,執行 getSnapshotBeforeUpdate
生命週期函數
提交全部更新並渲染,源碼以下:
function commitAllHostEffects() {
while (nextEffect !== null) {
recordEffect();
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
let primaryEffectTag = effectTag & (Placement | Update | Deletion);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
複製代碼
從源碼能夠看到,此函數主要是遍歷EffectList,根據effectTag,調用對應commit方法,進而調用react-dom提供的操做DOM的方法,渲染UI,操做DOM的方法有:
{
getPublicInstance,
supportsMutation,
supportsPersistence,
commitMount,
commitUpdate,
resetTextContent,
commitTextUpdate,
appendChild,
appendChildToContainer,
insertBefore,
insertInContainerBefore,
removeChild,
removeChildFromContainer,
replaceContainerChildren,
createContainerChildSet,
}
複製代碼
注意,在調用刪除操做的commit方法時,會執行 componentWillUnmount
生命週期函數
在這個方法中,基本完成了將更新提交併渲染UI的工做
此函數主要是根據fiber節點類型,執行相應的處理,以 ClassComponent 爲例,完成UI渲染以後,會執行後續的生命週期函數:
一、判斷是否首次渲染,是則執行 componentDidMount
生命週期函數
二、不然,執行 componentDidUpdate
生命週期函數
以上就是commit階段的全過程
至此,咱們源碼等的全過程也完成了,咱們再總結一下整個函數調用流程:
最後,咱們回到一開始的那兩個問題:
如今,是否是以爲整個過程都很清晰了呢~~~
附上,生命週期函數彙總表:
以上就是我對React16源碼的分享,但願能對有須要的小夥伴有幫助~~~
喜歡個人文章小夥伴能夠去 個人我的博客 點star ⭐️