React源码解析之FunctionComponent(中)
- 2019 年 12 月 2 日
- 筆記
前言
接上篇— —React源码解析之FunctionComponent(上)
一、reconcileSingleElement
作用: 当子节点不为 null,则复用子节点并删除其兄弟节点; 当子节点为 null,则创建新的 fiber 节点
源码:
//当子节点不为 null,则复用子节点并删除其兄弟节点; //当子节点为 null,则创建新的 fiber 节点 function reconcileSingleElement( returnFiber: Fiber, //旧 currentFirstChild: Fiber | null, //新 element: ReactElement, expirationTime: ExpirationTime, ): Fiber { const key = element.key; let child = currentFirstChild; //从当前已有的所有子节点中,找到可以复用的 fiber 对象,并删除它的 兄弟节点 while (child !== null) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. //key 相同的话复用节点 //ReactElement里面的key,也就是开发过程中加的 key,如<div key={'a'}></div> //所以当有多个相同 element 放在同一组时,React 建议设置 key,方便不产生更新的节点能复用 //但是我自己试验了下,发现打印出的 ReactElement 的 key('a') 和_owner 下fiber 节点的 key(null) 是不一样的, //而且设置 ReactElement 的 key 不影响 fiber 对象的 key 值一直为 null //所以这边 fiber.key 和 ReactElement.key 相等的情况,大多数应为 null===null if (child.key === key) { //如果节点类型未改变的话 if ( child.tag === Fragment ? element.type === REACT_FRAGMENT_TYPE : child.elementType === element.type || // Keep this check inline so it only runs on the false path: (__DEV__ ? isCompatibleFamilyForHotReloading(child, element) : false) ) { //复用 child,删除它的兄弟节点 //因为旧节点它有兄弟节点,新节点只有它一个 deleteRemainingChildren(returnFiber, child.sibling); //复制 fiber 节点,并重置 index 和 sibling //existing就是复用的节点 const existing = useFiber( child, element.type === REACT_FRAGMENT_TYPE ? element.props.children : element.props, expirationTime, ); //设置正确的 ref existing.ref = coerceRef(returnFiber, child, element); //父节点 existing.return = returnFiber; //删除了 dev 代码 return existing; } else { deleteRemainingChildren(returnFiber, child); break; } } else { deleteChild(returnFiber, child); } child = child.sibling; } //上面是子节点不为 null 的情况,能执行到这边说明之前是 null,现在要新建 //创建新的节点 if (element.type === REACT_FRAGMENT_TYPE) { //创建Fragment类型的 fiber 节点 const created = createFiberFromFragment( element.props.children, returnFiber.mode, expirationTime, element.key, ); created.return = returnFiber; return created; } else { //创建Element类型的 fiber 节点 const created = createFiberFromElement( element, returnFiber.mode, expirationTime, ); created.ref = coerceRef(returnFiber, currentFirstChild, element); created.return = returnFiber; return created; } }
解析:
(1) 流程图如下:

(2) 针对child.key === ReactElement.key
的情况,在开发过程中,大多数的 React 组件都是复用的,因为它们都是“列表”中的第一项,所以fiber.key(nulll)=ReactElement.key(null)
为true
(3) deleteRemainingChildren()
在本文后面有讲到
(3) 执行useFiber()
,通过复制 fiber 节点达到「复用」fiber 节点的目的,里面用到了double-buffer
的技巧,本文后面会讲到
(4) 不能根据旧节点复用成新节点的话,则通过createFiberFromElement()
创建Fragment
或Element
类型的 fiber 节点
(5) 最后,返回处理过的节点
二、useFiber
作用: 复制 fiber 节点,并重置 index 和 sibling
源码:
//复制 fiber 节点,并重置 index 和 sibling function useFiber( fiber: Fiber, pendingProps: mixed, expirationTime: ExpirationTime, ): Fiber { // We currently set sibling to null and index to 0 here because it is easy // to forget to do before returning it. E.g. for the single child case. //为防止忘记,提前将 index 置为 0,兄弟节点置为 null //通过 doubleBuffer 重用未更新的 fiber 对象 const clone = createWorkInProgress(fiber, pendingProps, expirationTime); clone.index = 0; clone.sibling = null; return clone; }
解析: 执行createWorkInProgress()
,通过 doubleBuffer 重用未更新的 fiber 对象,并将返回的 fiber 节点的 index、sibling 置为初始状态
三、createWorkInProgress
作用: 通过 doubleBuffer 重用未更新的 fiber 对象
源码:
// This is used to create an alternate fiber to do work on. //通过 doubleBuffer 重用未更新的 fiber 对象 export function createWorkInProgress( current: Fiber, pendingProps: any, expirationTime: ExpirationTime, ): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { // We use a double buffering pooling technique because we know that we'll // only ever need at most two versions of a tree. We pool the "other" unused // node that we're free to reuse. This is lazily created to avoid allocating // extra objects for things that are never updated. It also allow us to // reclaim the extra memory if needed. //因为一棵 fiber 树顶多有两个版本,所以当某一 fiber 节点不更新时,在更新 fiber 树的时候, //不会去重新创建跟之前一样的 fiber 节点,而是从另一个版本的 fiber 树上重用它 workInProgress = createFiber( current.tag, pendingProps, current.key, current.mode, ); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; //删除了 dev 代码 workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; // We already have an alternate. // Reset the effect tag. workInProgress.effectTag = NoEffect; // The effect list is no longer valid. workInProgress.nextEffect = null; workInProgress.firstEffect = null; workInProgress.lastEffect = null; if (enableProfilerTimer) { // We intentionally reset, rather than copy, actualDuration & actualStartTime. // This prevents time from endlessly accumulating in new commits. // This has the downside of resetting values for different priority renders, // But works for yielding (the common case) and should support resuming. workInProgress.actualDuration = 0; workInProgress.actualStartTime = -1; } } workInProgress.childExpirationTime = current.childExpirationTime; workInProgress.expirationTime = current.expirationTime; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; // Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; workInProgress.dependencies = currentDependencies === null ? null : { expirationTime: currentDependencies.expirationTime, firstContext: currentDependencies.firstContext, events: currentDependencies.events, }; // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; if (enableProfilerTimer) { workInProgress.selfBaseDuration = current.selfBaseDuration; workInProgress.treeBaseDuration = current.treeBaseDuration; } //删除了 dev 代码 return workInProgress; }
解析: React 会准备 fiber 树的两个版本(新版本和旧版本),当新版本的某一新节点在旧版本上有时,可以复用旧 fiber 的属性,而不是重新创建新的节点。
新旧 fiber 树相互复用的思路来源于doubleBuffer
。
四、reconcileSingleTextNode
作用: 复用或创建文本节点
源码:
//复用或创建文本节点 function reconcileSingleTextNode( returnFiber: Fiber, currentFirstChild: Fiber | null, textContent: string, expirationTime: ExpirationTime, ): Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. //判断第一个节点(优化) if (currentFirstChild !== null && currentFirstChild.tag === HostText) { // We already have an existing node so let's just update it and delete // the rest. //删掉旧节点 //returnFiber是当前正在更新的节点 //currentFirstChild是第一个子节点 deleteRemainingChildren(returnFiber, currentFirstChild.sibling); //复用 const existing = useFiber(currentFirstChild, textContent, expirationTime); //指定父节点 existing.return = returnFiber; return existing; } // The existing first child is not a text node so we need to create one // and delete the existing ones. //如果第一个节点不是文本节点的话,删除所有 deleteRemainingChildren(returnFiber, currentFirstChild); //创建新的文本节点 const created = createFiberFromText( textContent, returnFiber.mode, expirationTime, ); created.return = returnFiber; return created; }
解析: 仍有则复用,没有则执行createFiberFromText()
来新建
五、createFiberFromText
作用: 创建文本类型的 fiber
源码:
export function createFiberFromText( content: string, mode: TypeOfMode, expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(HostText, content, null, mode); fiber.expirationTime = expirationTime; return fiber; }
解析: content 就是要更新的文本
createFiber()
里面逻辑不复杂,就不解析了,放下源码:
//pendingProps就是 props.children const createFiber = function( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ): Fiber { // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors return new FiberNode(tag, pendingProps, key, mode); };
FiberNode()
的源码:
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // Instance this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null; // Fiber this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.mode = mode; // Effects this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; this.expirationTime = NoWork; this.childExpirationTime = NoWork; this.alternate = null; if (enableProfilerTimer) { // Note: The following is done to avoid a v8 performance cliff. // // Initializing the fields below to smis and later updating them with // double values will cause Fibers to end up having separate shapes. // This behavior/bug has something to do with Object.preventExtension(). // Fortunately this only impacts DEV builds. // Unfortunately it makes React unusably slow for some applications. // To work around this, initialize the fields below with doubles. // // Learn more about this here: // https://github.com/facebook/react/issues/14365 // https://bugs.chromium.org/p/v8/issues/detail?id=8538 this.actualDuration = Number.NaN; this.actualStartTime = Number.NaN; this.selfBaseDuration = Number.NaN; this.treeBaseDuration = Number.NaN; // It's okay to replace the initial doubles with smis after initialization. // This won't trigger the performance cliff mentioned above, // and it simplifies other profiler code (including DevTools). this.actualDuration = 0; this.actualStartTime = -1; this.selfBaseDuration = 0; this.treeBaseDuration = 0; } //删除了 dev 代码 }
六、deleteRemainingChildren
作用: 如果旧节点存在,但是更新的节点是 null 的话,需要删除旧节点的内容
源码:
//删除旧节点 function deleteRemainingChildren( returnFiber: Fiber, currentFirstChild: Fiber | null, ): null { //第一次渲染的情况 //是没有子节点的,所以直接删除 if (!shouldTrackSideEffects) { // Noop. return null; } // TODO: For the shouldClone case, this could be micro-optimized a bit by // assuming that after the first child we've already added everything. //从当前节点的第一个子节点开始,进行删除操作 let childToDelete = currentFirstChild; //删除目标节点的所有子节点,并循环寻找兄弟节点,删除它们的子节点 while (childToDelete !== null) { deleteChild(returnFiber, childToDelete); childToDelete = childToDelete.sibling; } return null; }
解析: 注意最后 return 的是 null
七、deleteChild
作用: 为要删除的子节点们做Deletion标记
源码:
//为要删除的子节点们做Deletion标记 function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void { if (!shouldTrackSideEffects) { // Noop. return; } // Deletions are added in reversed order so we add it to the front. // At this point, the return fiber's effect list is empty except for // deletions, so we can just append the deletion to the list. The remaining // effects aren't added until the complete phase. Once we implement // resuming, this may not be true. //fiber 链表的 effect 除了是deletions外,都是空的,可以根据这个进行删除 //标记副作用,以便在 commit 阶段进行删除 const last = returnFiber.lastEffect; //做标记 if (last !== null) { //要删除的节点(fiber) last.nextEffect = childToDelete; returnFiber.lastEffect = childToDelete; } else { returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; } childToDelete.nextEffect = null; //这里并未执行删除操作,而仅仅是给effectTag赋值了Deletion //因为这里仍是对 fiber 树的更新,未涉及到真正的 DOM 节点 //真正的删除留到 commit 阶段 childToDelete.effectTag = Deletion; }
解析: 在 fiber 树上,循环每一个子节点,并做上 Deletion 标记,以便在commit 阶段进行真删除
本文主要讲了reconcileSingleElement()
、reconcileSingleTextNode()
和deleteRemainingChildren()
的方法,下篇文章会继续讲FunctionComponent
中的数组节点的更新:
//数组节点 if (isArray(newChild)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, expirationTime, ); }
GitHub:
ReactChildFiber.js:
https://github.com/AttackXiaoJinJin/reactExplain/tree/master/react16.8.6/packages/react-reconciler/src/ReactChildFiber.js
ReactFiber.js:
https://github.com/AttackXiaoJinJin/reactExplain/tree/master/react16.8.6/packages/react-reconciler/src/ReactFiber.js