React源碼解析系列文章歡迎閱讀:
React16源碼解析(一)- 圖解Fiber架構
React16源碼解析(二)-建立更新
React16源碼解析(三)-ExpirationTime
React16源碼解析(四)-Scheduler
React16源碼解析(五)-更新流程渲染階段1
React16源碼解析(六)-更新流程渲染階段2
React16源碼解析(七)-更新流程渲染階段3
React16源碼解析(八)-更新流程提交階段
正在更新中...node
還記得咱們在performUnitOfWork中調用了beginWork,beginWork會沿着子樹一直更新,每次都會返回當前節點的child。就算有多個child也只會返回第一個。那麼沿着樹的結構到達葉子節點的時候,已經沒有child了,因此beginWork返回null。若是返回null的話,就會調用completeUnitOfWork。
再瞧一眼代碼:react
// 開始組件更新 function performUnitOfWork(workInProgress: Fiber): Fiber | null { // The current, flushed, state of this fiber is the alternate. // Ideally nothing should rely on this, but relying on it here // means that we don't need an additional field on the work in // progress. // 得到 fiber 的替身,調和這一階段都是在替身上完成的 // 而後直接看 beginWork const current = workInProgress.alternate; // ...... let next; // ..... // 開始工做 next = beginWork(current, workInProgress, nextRenderExpirationTime); workInProgress.memoizedProps = workInProgress.pendingProps; // ...... // 當前fiber樹已經到達葉子節點了 if (next === null) { // If this doesn't spawn new work, complete the current work. next = completeUnitOfWork(workInProgress); } ReactCurrentOwner.current = null; return next; }
這個completeUnitOfWork幹了什麼呢?主要有如下三點:
一、根據是否中斷調用不一樣的處理方法
二、判斷是否有兄弟節點來執行不一樣的操做
三、完成節點以後賦值effect鏈segmentfault
function completeUnitOfWork(workInProgress: Fiber): Fiber | null { // Attempt to complete the current unit of work, then move to the // next sibling. If there are no more siblings, return to the // parent fiber. while (true) { // The current, flushed, state of this fiber is the alternate. // Ideally nothing should rely on this, but relying on it here // means that we don't need an additional field on the work in // progress. const current = workInProgress.alternate; const returnFiber = workInProgress.return; const siblingFiber = workInProgress.sibling; // 沒有錯誤捕獲,正常的渲染邏輯 if ((workInProgress.effectTag & Incomplete) === NoEffect) { // This fiber completed. // 完成節點的更新 nextUnitOfWork = completeWork( current, workInProgress, nextRenderExpirationTime, ); // 重置 childExpirationTime resetChildExpirationTime(workInProgress, nextRenderExpirationTime); // 構建 effect 鏈,供 commitRoot 提交階段使用 if ( returnFiber !== null && // Do not append effects to parents if a sibling failed to complete (returnFiber.effectTag & Incomplete) === NoEffect ) { // Append all the effects of the subtree and this fiber onto the effect // list of the parent. The completion order of the children affects the // side-effect order. // 把本身身上的effect鏈粘在父節點的effect後面 if (returnFiber.firstEffect === null) { returnFiber.firstEffect = workInProgress.firstEffect; } if (workInProgress.lastEffect !== null) { if (returnFiber.lastEffect !== null) { returnFiber. .nextEffect = workInProgress.firstEffect; } returnFiber.lastEffect = workInProgress.lastEffect; } // If this fiber had side-effects, we append it AFTER the children's // side-effects. We can perform certain side-effects earlier if // needed, by doing multiple passes over the effect list. We don't want // to schedule our own side-effect on our own list because if end up // reusing children we'll schedule this effect onto itself since we're // at the end. const effectTag = workInProgress.effectTag; // Skip both NoWork and PerformedWork tags when creating the effect list. // PerformedWork effect is read by React DevTools but shouldn't be committed. // 發現本身自己也有effect , 那麼要把本身也加入父節點的effect鏈上 if (effectTag > PerformedWork) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = workInProgress; } else { returnFiber.firstEffect = workInProgress; } returnFiber.lastEffect = workInProgress; } } // 有兄弟節點返回兄弟節點,繼續走beinWork if (siblingFiber !== null) { // If there is more work to do in this returnFiber, do that next. return siblingFiber; } else if (returnFiber !== null) { // 沒有兄弟節點找父節點 // If there's no more work in this returnFiber. Complete the returnFiber. workInProgress = returnFiber; continue; } else { // We've reached the root. // 一直向上或者向右找兄弟節點,找到null到達root頂點結束,更新階段完成準備進入commitRoot提交階段 return null; } } else { // ...... return null; } } // Without this explicit null return Flow complains of invalid return type // TODO Remove the above while(true) loop // eslint-disable-next-line no-unreachable return null; }
經過下面函數咱們能夠看到,函數根據workInProgress.tag對不一樣的類型節點作不一樣的處理,對大部分 tag 不進行操做或者只是 pop context,只有 HostComponent, HostText, SuspenseComponent 有稍微複雜點的操做。接下來我主要分析HostComponent和HostText。SuspenseComponent後續再進行講解。架構
function completeWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { const newProps = workInProgress.pendingProps; switch (workInProgress.tag) { case IndeterminateComponent: break; case LazyComponent: break; case SimpleMemoComponent: case FunctionComponent: break; case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } break; } case HostRoot: { popHostContainer(workInProgress); popTopLevelLegacyContextObject(workInProgress); const fiberRoot = (workInProgress.stateNode: FiberRoot); if (fiberRoot.pendingContext) { fiberRoot.context = fiberRoot.pendingContext; fiberRoot.pendingContext = null; } if (current === null || current.child === null) { // If we hydrated, pop so that we can delete any remaining children // that weren't hydrated. popHydrationState(workInProgress); // This resets the hacky state to fix isMounted before committing. // TODO: Delete this when we delete isMounted and findDOMNode. workInProgress.effectTag &= ~Placement; } updateHostContainer(workInProgress); break; } case HostComponent: { // 這裏稍微複雜,稍後講解 break; } case HostText: { // 稍後講解 break; } case ForwardRef: break; case SuspenseComponent: { const nextState = workInProgress.memoizedState; const prevState = current !== null ? current.memoizedState : null; const nextDidTimeout = nextState !== null && nextState.didTimeout; const prevDidTimeout = prevState !== null && prevState.didTimeout; if (nextDidTimeout !== prevDidTimeout) { // If this render commits, and it switches between the normal state // and the timed-out state, schedule an effect. workInProgress.effectTag |= Update; } break; } case Fragment: break; case Mode: break; case Profiler: break; case HostPortal: popHostContainer(workInProgress); updateHostContainer(workInProgress); break; case ContextProvider: // Pop provider fiber popProvider(workInProgress); break; case ContextConsumer: break; case MemoComponent: break; case IncompleteClassComponent: { // Same as class component case. I put it down here so that the tags are // sequential to ensure this switch is compiled to a jump table. const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } break; } default: invariant( false, 'Unknown unit of work tag. This error is likely caused by a bug in ' + 'React. Please file an issue.', ); } return null; }
以前咱們已經講過,tag 爲 HostComponent 表示普通 dom 節點,如: div。app
簡單歸納:
一、createInstance: 建立dom
二、appendAllChildren: 將children的host Component添加到剛建立的dom上 組成dom樹。
三、finalizeInitialChildren: 給dom設置屬性。dom
case HostComponent: { popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); const type = workInProgress.type; if (current !== null && workInProgress.stateNode != null) { updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance, ); if (current.ref !== workInProgress.ref) { markRef(workInProgress); } } else { // 首次渲染 // ...... // 建立instance , 就是建立dom節點對象 , 這個對象包含了fiber,和 props 信息 let instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, ); // 構建dom樹,由於咱們是從下往上的,因此咱們只需把我下面第一層子節點append到本身下面就行了 appendAllChildren(instance, workInProgress, false, false); // Certain renderers require commit-time effects for initial mount. // (eg DOM renderer supports auto-focus for certain elements). // Make sure such renderers get scheduled for later work. if ( // 設置屬性,初始化事件監聽 finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { // 若是須要 auto focus // 標記effect爲UPDATE markUpdate(workInProgress); } // stateNode指向建立好的dom節點 workInProgress.stateNode = instance; // ...... } break; }
接下來咱們先將首次渲染的狀況ide
一、建立 dom 節點
二、在 dom 節點對象上記錄這次建立的 fiber 和 props 信息函數
export function createInstance( type: string, props: Props, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: Object, // 傳入的當前節點的workInProgress ): Instance { let parentNamespace: string; // ...... parentNamespace = ((hostContext: any): HostContextProd); // 建立dom節點 const domElement: Instance = createElement( type, props, rootContainerInstance, parentNamespace, ); // 給domElement[__reactInternalInstance$] = internalInstanceHandle。 // 也就是指向了對應的fiber節點 precacheFiberNode(internalInstanceHandle, domElement); // domElement[__reactEventHandlers$] = props updateFiberProps(domElement, props); return domElement; }
由於咱們是從下往上的,因此咱們只需把我下面第一層子節點append到本身下面就行了。
一、對node 的 sibling兄弟節點進行遍歷
二、若是是dom原生節點或者是文本,直接appendChild
三、若是是其餘節點可是有子節點,那麼轉而去遍歷它的子節點,直到找到dom原生節點或者是文本oop
appendAllChildren = function( parent: Instance, workInProgress: Fiber, needsVisibilityToggle: boolean, isHidden: boolean, ) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. let node = workInProgress.child; while (node !== null) { if (node.tag === HostComponent || node.tag === HostText) { // 若是是dom原生節點或者是文本,直接appendChild appendInitialChild(parent, node.stateNode); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in // the portal directly. } else if (node.child !== null) { // 若是是其餘節點可是有子節點,那麼轉而去遍歷它的子節點,直到找到dom原生節點或者是文本 node.child.return = node; node = node.child; continue; } if (node === workInProgress) { return; } while (node.sibling === null) { if (node.return === null || node.return === workInProgress) { return; } node = node.return; } // 對node 的 sibling兄弟節點進行遍歷 node.sibling.return = node.return; node = node.sibling; } };
主要是設置dom元素的一些初始值。在設置初始值的時候對應不一樣的dom元素有特殊的處理。這些處理都在setInitialProperties函數中。ui
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
// 把props對應的應該在dom節點上展示的attributes,如何在掛載到Dom,還有一些事件監聽相關。
setInitialProperties(domElement, type, props, rootContainerInstance);
// 是否須要 auto focus
return shouldAutoFocusHostComponent(type, props);
}
一、調用prepareUpdate獲得新老props比較後的結果
二、把結果放到workInProgress.updateQueue
三、標記當前節點的effect 爲 UPDATE
注:比較後造成的結果是這樣的:updatePayload: [k1,null,k2,v2,k3,v3]
updateHostComponent = function( current: Fiber, workInProgress: Fiber, type: Type, newProps: Props, rootContainerInstance: Container, ) { // If we have an alternate, that means this is an update and we need to // schedule a side-effect to do the updates. // 以前的oldProps const oldProps = current.memoizedProps; if (oldProps === newProps) { // In mutation mode, this is sufficient for a bailout because // we won't touch this node even if children changed. return; } // If we get updated because one of our children updated, we don't // have newProps so we'll have to reuse them. // TODO: Split the update API as separate for the props vs. children. // Even better would be if children weren't special cased at all tho. // 當前節點的dom對象 const instance: Instance = workInProgress.stateNode; const currentHostContext = getHostContext(); // TODO: Experiencing an error where oldProps is null. Suggests a host // component is hitting the resume path. Figure out why. Possibly // related to `hidden`. // 獲得新老props比較後的結果 const updatePayload = prepareUpdate( instance, type, oldProps, newProps, rootContainerInstance, currentHostContext, ); // TODO: Type this specific to this type of component. // 把結果放到workInProgress.updateQueue workInProgress.updateQueue = (updatePayload: any); // If the update payload indicates that there is a change or if there // is a new ref we mark this as an update. All the work is done in commitWork. if (updatePayload) { // 標記當前節點的effect 爲 UPDATE markUpdate(workInProgress); } };
prepareUpdate:
這個函數只是調用了diffProperties而且返回
export function prepareUpdate( domElement: Instance, type: string, oldProps: Props, newProps: Props, rootContainerInstance: Container, hostContext: HostContext, ): null | Array<mixed> { // ...... return diffProperties( domElement, type, oldProps, newProps, rootContainerInstance, ); }
一、根據不一樣標籤節點提取新老 props 準備比較
二、第一次遍歷老 props 把要刪除的屬性都設置爲 null
三、第二次遍歷新 props , 把新的props push 到updatePayload
四、最後生成updatePayload: [k1,null,k2,v2,k3,v3]
注:這裏不一樣的屬性會有不一樣的特殊處理,好比STYLE的話,就須要展開處理等等。
// Calculate the diff between the two objects. export function diffProperties( domElement: Element, tag: string, lastRawProps: Object, nextRawProps: Object, rootContainerElement: Element | Document, ): null | Array<mixed> { let updatePayload: null | Array<any> = null; let lastProps: Object; let nextProps: Object; // 一、根據不一樣標籤節點提取新老 props 準備比較 switch (tag) { case 'input': lastProps = ReactDOMInput.getHostProps(domElement, lastRawProps); nextProps = ReactDOMInput.getHostProps(domElement, nextRawProps); updatePayload = []; break; case 'option': lastProps = ReactDOMOption.getHostProps(domElement, lastRawProps); nextProps = ReactDOMOption.getHostProps(domElement, nextRawProps); updatePayload = []; break; case 'select': lastProps = ReactDOMSelect.getHostProps(domElement, lastRawProps); nextProps = ReactDOMSelect.getHostProps(domElement, nextRawProps); updatePayload = []; break; case 'textarea': lastProps = ReactDOMTextarea.getHostProps(domElement, lastRawProps); nextProps = ReactDOMTextarea.getHostProps(domElement, nextRawProps); updatePayload = []; break; default: lastProps = lastRawProps; nextProps = nextRawProps; if ( typeof lastProps.onClick !== 'function' && typeof nextProps.onClick === 'function' ) { // TODO: This cast may not be sound for SVG, MathML or custom elements. trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); } break; } assertValidProps(tag, nextProps); // 二、第一次遍歷老 props 把要刪除的屬性都設置爲 null let propKey; let styleName; let styleUpdates = null; for (propKey in lastProps) { if ( nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null ) { continue; } if (propKey === STYLE) { const lastStyle = lastProps[propKey]; for (styleName in lastStyle) { if (lastStyle.hasOwnProperty(styleName)) { if (!styleUpdates) { styleUpdates = {}; } styleUpdates[styleName] = ''; } } } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) { // Noop. This is handled by the clear text mechanism. } else if ( propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { // Noop } else if (propKey === AUTOFOCUS) { // Noop. It doesn't work on updates anyway. } else if (registrationNameModules.hasOwnProperty(propKey)) { // This is a special case. If any listener updates we need to ensure // that the "current" fiber pointer gets updated so we need a commit // to update this element. if (!updatePayload) { updatePayload = []; } } else { // For all other deleted properties we add it to the queue. We use // the whitelist in the commit phase instead. (updatePayload = updatePayload || []).push(propKey, null); } } // 三、第二次遍歷新 props , 把新的props push 到updatePayload for (propKey in nextProps) { const nextProp = nextProps[propKey]; const lastProp = lastProps != null ? lastProps[propKey] : undefined; if ( !nextProps.hasOwnProperty(propKey) || nextProp === lastProp || (nextProp == null && lastProp == null) ) { continue; } if (propKey === STYLE) { if (lastProp) { // Unset styles on `lastProp` but not on `nextProp`. for (styleName in lastProp) { if ( lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName)) ) { if (!styleUpdates) { styleUpdates = {}; } styleUpdates[styleName] = ''; } } // Update styles that changed since `lastProp`. for (styleName in nextProp) { if ( nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName] ) { if (!styleUpdates) { styleUpdates = {}; } styleUpdates[styleName] = nextProp[styleName]; } } } else { // Relies on `updateStylesByID` not mutating `styleUpdates`. if (!styleUpdates) { if (!updatePayload) { updatePayload = []; } updatePayload.push(propKey, styleUpdates); } styleUpdates = nextProp; } } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { const nextHtml = nextProp ? nextProp[HTML] : undefined; const lastHtml = lastProp ? lastProp[HTML] : undefined; if (nextHtml != null) { if (lastHtml !== nextHtml) { (updatePayload = updatePayload || []).push(propKey, '' + nextHtml); } } else { // TODO: It might be too late to clear this if we have children // inserted already. } } else if (propKey === CHILDREN) { if ( lastProp !== nextProp && (typeof nextProp === 'string' || typeof nextProp === 'number') ) { (updatePayload = updatePayload || []).push(propKey, '' + nextProp); } } else if ( propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { // Noop } else if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp != null) { // We eagerly listen to this even though we haven't committed yet. if (__DEV__ && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } ensureListeningTo(rootContainerElement, propKey); } if (!updatePayload && lastProp !== nextProp) { // This is a special case. If any listener updates we need to ensure // that the "current" props pointer gets updated so we need a commit // to update this element. updatePayload = []; } } else { // For any other property we always add it to the queue and then we // filter it out using the whitelist during the commit. (updatePayload = updatePayload || []).push(propKey, nextProp); } } if (styleUpdates) { (updatePayload = updatePayload || []).push(STYLE, styleUpdates); } // 四、最後生成updatePayload: [k1,null,k2,v2,k3,v3] return updatePayload; }
一、更新的話,調用updateHostText
二、首次渲染的話,調用createTextInstance
case HostText: { let newText = newProps; if (current && workInProgress.stateNode != null) { // 更新 const oldText = current.memoizedProps; // If we have an alternate, that means this is an update and we need // to schedule a side-effect to do the updates. updateHostText(current, workInProgress, oldText, newText); } else { // ...... // 首次渲染 workInProgress.stateNode = createTextInstance( newText, rootContainerInstance, currentHostContext, workInProgress, ); } break; }
這是一個巨簡單的方法,直接比較文本是否相同。
updateHostText = function( current: Fiber, workInProgress: Fiber, oldText: string, newText: string, ) { // If the text differs, mark it as an update. All the work in done in commitWork. if (oldText !== newText) { markUpdate(workInProgress); } };
這個方法也很簡單,就是建立了一個TextNode文本節點。
以及給textElement[__reactInternalInstance$] = internalInstanceHandle = 當前的fiber節點。
export function createTextInstance( text: string, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: Object, ): TextInstance { const textNode: TextInstance = createTextNode(text, rootContainerInstance); precacheFiberNode(internalInstanceHandle, textNode); return textNode; }
文章若有不妥,歡迎指正~