从源码角度看 React Function Component 渲染过程

604 阅读14分钟

前言

本节我们通过 ReactDOM.render 渲染一个简单的函数组件来了解 React 底层的初渲染流程和运行机制。

  1. 代码示例:
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
    return 'Hello React!'
}
ReactDOM.render(<App />, window.root);

2、代码调试: 打开浏览器控制台 - Sources - 找到 react-dom 文件,搜索 render 方法并在该方法内部添加断点,刷新页面即可进行调试。

3、JSX 编译转换 在进入 render 方法之前,babel 会解析和编译 JSX 语法元素,并处理成 React 可识别的语法和节点信息,本示例在 render 方法中的 element 数据格式为:

{
    $$typeof: Symbol(react.element)
    key: null
    props: {}
    ref: null
    type: ƒ App()
}

步骤一:创建应用 Fiber 树的根节点

function render(element, container, callback) {
    return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}

在 legacRenderSubtreeIngoContainer 方法中首先会调用 legacCreateRootFromDOMContainer 创建整个应用的 Fiber 树(FiberRootNode),以及根容器对应的 Fiber 节点(HostRoot):

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
    let root = container._reactRootContainer; // 根据_reactRootContainer判断是否为初次渲染
    let fiberRoot;
    if (!root) {
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
        fiberRoot = root._internalRoot;
        
        // ...
        
        unbatchedUpdates(() => {
            updateContainer(children, fiberRoot, parentComponent, callback);
        });
    }
}

在 legacCreateRootFromDOMContainer 中涉及到的相关函数调用栈层级较深,但核心是执行 createFiberRoot 这个方法来创建并返回 Fiber 树:

 function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
    const root = new FiberRootNode(containerInfo, tag, hydrate); // 创建 Fiber 应用树
    const uninitializedFiber = createHostRootFiber(tag); // 创建根容器对应的 Fiber 节点
    root.current = uninitializedFiber;
    uninitializedFiber.stateNode = root;

    initializeUpdateQueue(uninitializedFiber); // 为 fiber 创建 updateQueue
    return root;
}

步骤二:scheduler 调度阶段

React 整体工作流程分为三个阶段:

  • schedule(调度):对产生的不同优先级任务进行排序。
  • render(协调):根据优先级任务决定要更新哪些视图。
  • commit(渲染):将需要改变的视图更新到视图中。

React 现在存在三种工作模式:

  • Legacy 模式:通过 ReactDOM.render(<App>, rootNode) 创建,目前 React 默认使用的模式,
  • Blocking 模式:通过 ReactDOM.createBlockingRoot(rootNode).render(<App />) 创建,目前正在试验中,作为迁至 Concurrent 模式的第一个步骤;
  • Concurrent 模式:通过 ReactDOM.createRoot(rootNode).render(<App />) 创建,目前正在试验中,未来稳定之后,将作为 React 的默认使用模式,这个模式开启了所有的新功能。

而 schedule 调度阶段对任务的调度主要体现在 Concurrent 模式,而我们现在使用的 Legacy 模式在该阶段下没有做太多逻辑处理。

在上一步 legacRenderSubtreeIngoContainer 中创建 Fiber 应用后,会执行 updateContainer 准备进入 React 流程中的第一个阶段:scheduler(调度):

unbatchedUpdates(() => {
    updateContainer(children, fiberRoot, parentComponent, callback);
});

export function unbatchedUpdates(fn, a) {
    const prevExecutionContext = executionContext; // 获取上次的执行上下文
    executionContext &= ~BatchedContext;
    executionContext |= LegacyUnbatchedContext;
    try {
        return fn(a); // updateContainer
    } finally {
        executionContext = prevExecutionContext; // 恢复执行栈
    }
}

可以看到上面代码会先执行 unbatchedUpdates 修改当前执行栈为:LegacyUnbatchedContext(传统的同步模式),因为是初渲染,需要尽可能以最快的速度渲染到页面上,采用同步模式。

而在 updateContainer 中主要做的一件事情就是创建一个 update(每一个更新都对应一个update,这里的更新就是 element),然后调用 scheduleUpdateOnFiber 进入调度阶段:

export function updateContainer(element, container, parentComponent, callback) {
    // ...
    const update = createUpdate(eventTime, lane, suspenseConfig); // 创建一个update
    update.payload = {element};
    enqueueUpdate(current, update); // 将该update加入到fiber的updateQueue更新队列中,一个fiber对应多个update
    scheduleUpdateOnFiber(current, lane, eventTime); // 开始进入schedule调度阶段
}

function enqueueUpdate(fiber, update) {
    const updateQueue = fiber.updateQueue;
    const sharedQueue = updateQueue.shared; // { pending: null }
    const pending = sharedQueue.pending;
    if (pending === null) {
        update.next = update; // 环状单向链表
    } else {
        update.next = pending.next;
        pending.next = update;
    }
    sharedQueue.pending = update;
}

在 scheduleUpdateOnFiber 方法中对初渲染的任务调度很简单(不需要进行调度),因为是初次同步渲染,并且执行上下文为 LegacyUnbatchedContext 模式,所以会进入 performSyncWorkOnRoot 开始循环让每个任务进入 Work 阶段(Work 阶段可以分为:render和commit两个阶段)。

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
    // ...
    if (lane === SyncLane) {
        if (
            (executionContext & LegacyUnbatchedContext) !== NoContext && // 初次渲染时执行unbatchedUpdates方法修改了executionContext
            (executionContext & (RenderContext | CommitContext)) === NoContext // 并且当前还没有进入render或commit阶段
        ) {
            // 从root根fiber开始进入reconciler阶段(render阶段)同步执行每一个任务
            performSyncWorkOnRoot(root);
        } 
        // ...
    }
    // ...
}

export function performSyncWorkOnRoot(root) {
    // ...
    
    // render 阶段
    renderRootSync(root, lanes);

    // commit 阶段
    commitRoot(root);
}

步骤三:render 渲染阶段

function renderRootSync(root, lanes) {
    const prevExecutionContext = executionContext;
    executionContext |= RenderContext; // 将当前执行栈改为Render阶段
    if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
        prepareFreshStack(root, lanes);
    }

    do {
        try {
            workLoopSync();
            break;
        } catch (thrownValue) {
            handleError(root, thrownValue);
        }
    } while (true);

    executionContext = prevExecutionContext; // 恢复执行栈
    // 在reconciler阶段workLoopSync中任务全部执行完毕后清空工作信息,后续进行commitRoot时是从fiberRootNode开始执行,而不是workInProgressRoot
    workInProgressRoot = null;
    workInProgressRootRenderLanes = NoLanes;
}

在 renderRootSync 中首先会调用 prepareFreshStack 方法来初始化工作信息(React 内部工作流程上使用的全局变量):

function prepareFreshStack(root, lanes) {
    root.finishedWork = null;
    root.finishedLanes = NoLanes;

    workInProgressRoot = root;
    // 基于 HostRoot Fiber 创建一个 alternate(双工协议)作为当前工作的任务
    workInProgress = createWorkInProgress(root.current, null);
    
    // ...
}

function createWorkInProgress(current, pendingProps) {
    let workInProgress = current.alternate;
    if (workInProgress === null) {
        // 基于 current 创建 Fiber Node
        workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
        workInProgress.elementType = current.elementType;
        workInProgress.type = current.type;
        workInProgress.stateNode = current.stateNode;

        workInProgress.alternate = current; // 双工协议
        current.alternate = workInProgress;
    } else {
        workInProgress.pendingProps = pendingProps;
        workInProgress.type = current.type;
        workInProgress.effectTag = NoEffect;
        workInProgress.nextEffect = null;
        workInProgress.firstEffect = null;
        workInProgress.lastEffect = null;
    }
    // ...
    return workInProgress;
}

接着在 workLoopSync 中循环调用 performUnitOfWork 处理每一个任务(一个 Fiber 节点对应一个任务):

function workLoopSync() {
    while (workInProgress !== null) {
        performUnitOfWork(workInProgress);
    }
}

function performUnitOfWork(unitOfWork) { // unit of work --> 工作单元
    const current = unitOfWork.alternate;
    // render - 递阶段
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    unitOfWork.memoizedProps = unitOfWork.pendingProps; // 更新props
    
    // render - 归阶段(当该节点没有child节点,或者child已经处理完成后,进入归阶段)
    if (next === null) {
        completeUnitOfWork(unitOfWork);
    } else {
        workInProgress = next;
    }
}

performUnitOfWork 的工作可以分为两部分:递阶段(beginWork)和 归阶段(completeUnitOfWork)

首先会调用 beginWork 将当前树上的节点和当前工作节点进行比较处理,并返回自己的子节点(如果有 child 的话)。

如果有子节点(next 值存在),则会重新进入 performUnitOfWork 让子节点执行 beginWork;如果没有则会调用 completeUnitOfWork 让当前节点进入归阶段。

beginWork 递阶段

beginWork 阶段的工作主要是传入当前 Fiber 节点,和渲染树上的节点(老节点)进行比较(Diff),并创建子 Fiber 节点。由于节点类型较多,将其分为多个 Case 交由不同的方法去处理。

export function beginWork(current, workInProgress, renderLanes) {
    if (current !== null) { // update 阶段
        // beginWork 首先判断是否可以复用节点,如果可以复用,执行 bailoutOnAlreadyFinishedWork 方法
    }
    
    switch (workInProgress.tag) {
        case HostRoot: // 根节点
            return updateHostRoot(current, workInProgress, renderLanes);
        case HostComponent: // 原生节点
            return updateHostComponent(current, workInProgress, renderLanes);
        case HostText: // 文本节点
            return updateHostText(current, workInProgress);
        case IndeterminateComponent: { // 函数组件在mount时tag都是这个,在执行完beginWork后会将tag改为FunctionComponent
            // 为什么函数组件在mount时会在这个函数中处理,而不是在FunctionComponent中处理?
            return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
        }
        case FunctionComponent: { // 函数在update阶段会执行这里的逻辑s
            // ...
        }
    }
}

beginWork - HostRoot 节点的处理

在初渲染时,第一个进入 begin 阶段的是 HostRoot 根节点,在 HostRoot 的更新队列 updateQueue 上拿到子节点(传入给 ReactDOM.render 方法的第一个参数),调用 reconCilechildren 创建子节点,并将子节点返回。

function updateHostRoot(current, workInProgress, renderLanes) {
    const nextProps = workInProgress.pendingProps;
    const prevState = workInProgress.memoizedState;
    const prevChildren = prevState !== null ? prevState.element : null;
    
    // clone current.updateQueue 给 workInProgress
    cloneUpdateQueue(current, workInProgress);
    // 从workInProgress.updateQueue中拿到update.payload(子节点),赋值给 workInProgress.memoizedState
    processUpdateQueue(workInProgress, nextProps, null, renderLanes);
    const nextState = workInProgress.memoizedState;
    const nextChildren = nextState.element;
    
    // 如果两个字节点相同,复用该节点,不需要创建新的fiber
    if (nextChildren === prevChildren) {
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
    
    // 否则开始创建子 fiber
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    return workInProgress.child;
}

beginWork - reconcileChildren

reconcileChildren 会根据 current 是否有值(挂载/更新)来做不同处理(区别在于:第二参数 current.child 节点是否有值),最终会为当前 fiber 创建 child 节点。

mountChildFibers 和 reconcileChildFibers 指向同一个方法,内部通过 shouldTrackSideEffects 变量来区分 mount 和 update。

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
    if (current === null) { // 尚未渲染的新组件,初次渲染
        workInProgress.child = 
            mountChildFibers(workInProgress, null, nextChildren, renderLanes);
    } else { // 更新渲染(HostRoot在初渲染会进入这里,其他节点只能在更新渲染时才会进入到这里)
        workInProgress.child = 
            reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
    }
}

在 reconcileChildFibers 中会根据 nextChildren 的节点类型,交由不同的方法做处理,如:Fragment、单节点、多节点、纯文本节点等。

function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
    // 如果 child fiber 类型是 fragment,则跳过它,处理它的子节点
    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) { // 如果是一个对象,表示只有一个child元素,通过Single独生子女方式处理(不考虑sibling)
        switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE:
                return placeSingleChild(
                    reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes),
                );
            // ...
        }
    }
    if (typeof newChild === 'string' || typeof newChild === 'number') {
        return placeSingleChild(
            reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes),
        );
    }
    if (isArray(newChild)) {
        return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
    }

    // 其他情况都视为空,将父节点中所有的子节点都删除掉,让其成为一个空的元素
    return deleteRemainingChildren(returnFiber, currentFirstChild);
}

如果 child 子节点是单个元素节点的处理(非文本节点),会进入 reconcileSingleElement 进行处理(单节点 Diff)。通过老的 child 是否有值决定进行 Diff 还是新建节点。

function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
    const key = element.key;
    let child = currentFirstChild;
    
    while (child !== null) {
        // 通过key来判断,如果值不一样,删除节点
        if (child.key === key) {
            // key相同,接下来比较type是否相同
            switch (child.tag) {
                // ...
                default: {
                    // type相同则表示可以复用
                    if (child.elementType === element.type) {
                        // type相同表示找到了相同的节点,因为这里是单节点处理,所以要标记删除视图上其他的兄弟节点
                        deleteRemainingChildren(returnFiber, child.sibling);
                        const existing = useFiber(child, element.props);
                        existing.ref = coerceRef(returnFiber, child, element);
                        existing.return = returnFiber;
                        return existing;
                    }
                    // type不同,则跳出循环
                    break;
                }
            }
            // 代码执行到这里代表:key相同但是type不同
            // 将该fiber及其兄弟fiber标记为删除,为什么要处理sibling呢?
            // 因为视图上的子节点可能存在多个,而本次更新只有一个单节点,需要将多余的删除掉
            deleteRemainingChildren(returnFiber, child);
            break;
        } else {
            // 将该fiber标记为删除
            deleteChild(returnFiber, child);
        }
        // 当前新节点只有一个,考虑到视图上的同级老节点可能是个多节点,
        // 这里依次比较每个老sibling节点,看是否可以复用,所以也需要将sibling进行处理
        child = child.sibling;
    }

    // 如果新增子节点,或者是经过 Diff 后发现老节点不可复用,在这里创建新节点
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
}

reconcileSingleElement 执行完成后返回 创建/复用 的 child 节点,并作为参数进入 placeSingleChild 方法,如果是 update 阶段,会标记 fiber 操作节点类型为 Placement(添加),对于本例来说,HostRoot 会为它的 newFiber(App 函数组件)增加 Placement 标识:

function placeSingleChild(newFiber) {
    // shouldTrackSideEffects 存在表示update阶段
    if (shouldTrackSideEffects && newFiber.alternate === null) {
        newFiber.effectTag = Placement;
    }
    return newFiber;
}

如果 child 子节点是文本节点,如我们这里的例子:函数组件返回一个普通的文本字符串:Hello React!,所以在 reconcileChildFibers 中会使用 reconcileSingleTextNode 方法处理:

function reconcileSingleTextNode(returnFiber, currentFirstChild, textContent, lanes) {
    // 如果说子节点对应的current fiber节点存在,并且也是文本节点,进行复用
    if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
        deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
        const existing = useFiber(currentFirstChild, textContent);
        existing.return = returnFiber;
        return existing;
    }
    // 在 update 阶段会删除父节点中现有的内容,mount 阶段该方法不会进行删除处理
    deleteRemainingChildren(returnFiber, currentFirstChild);
    const created = createFiberFromText(textContent, returnFiber.mode, lanes); // 创建文本节点
    created.return = returnFiber; // 建立关系
    return created;
}

内部实现比较简单:如果可以复用节点,则返回复用后的节点,否则创建一个新的文本 fiber 节点。

beginWork - 函数组件处理

对于函数组件,在创建对应的 fiber 阶段时,tag 类型赋值为 IndeterminateComponent(不确定组件),所以在 beginWork 方法中会命中 mountIndeterminateComponent 方法逻辑。

mountIndeterminateComponent 会对类组件和函数组件最不同处理,这里我们略过类组件,重点关注函数组件的处理方式。

function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
    let value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes); // 返回函数执行后的结果
    // 对于本例,函数组件是HostRoot的child,在placeSingleChild中将 effectTag 设为 Update (2)
    // 经过下面运算符合并后(函数组件完成工作后会标记 effectTag 或等于 PerformedWork 1),得到的 effectTag 为 3
    workInProgress.effectTag |= PerformedWork; // PerformedWork 值是 1

    // 省略 class 组件,只看函数组件的处理逻辑
    workInProgress.tag = FunctionComponent;
    reconcileChildren(null, workInProgress, value, renderLanes);
    return workInProgress.child;
}

可以看到 renderWithHooks 方法接收函数组件作为参数,并将返回值 value 作为 nextChild 传入 reconcileChildren,这里的 value 就是函数组件执行后 return 的结果,下面我们看看 renderWithHooks 内部是如何执行函数组件的。

function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
    currentlyRenderingFiber = workInProgress; // 保存当前要渲染的函数组件对应的fiber,后面执行函数内部的hooksAPI时会用到

    // 决定hooks使用mount方式还是update方式
    ReactCurrentDispatcher.current =
            current === null || current.memoizedState === null
                ? HooksDispatcherOnMount
                : HooksDispatcherOnUpdate;

    let children = Component(props, secondArg);

    ReactCurrentDispatcher.current = ContextOnlyDispatcher;

    currentlyRenderingFiber = null;
    // 重置函数组件中执行工作的hooks,腾出位置,方便给下一个函数组件使用。
    currentHook = null;
    workInProgressHook = null;

    return children;
}

首先根据 mount 或 update 来拿到不同的 Hooks 执行方法,即 ReactCurrentDispatcher,Hooks 的逻辑都是由这个对象提供而来;

接着执行 Component 来调用函数组件,函数执行完成,也会将内部的 Hooks 依次执行,最后恢复 ReactCurrentDispatcher 为初始值。

beginWork - HostText 文本节点的处理

本例中,函数组件会返回一个普通的文本字符串:Hello React! 会被创建 Text Fiber,然后再次进入 beginWork 函数中,因为文本节点不存在 child,所以在对文本节点的处理上会直接返回 null。

function updateHostText(current, workInProgress) {
    // 因为它没有子节点,所以不需要进行下去,并返回null,让这个文本节点进入 completeUnitOfWork 归阶段
    return null;
}

completeUnitOfWork 归阶段

接着我们的例子看,Hello React! 文本节点没有子节点,所以会进入到归阶段,我们先来看一下这个方法的核心功能:

  • 调用 completeWork 为当前 fiber 节点创建真实 DOM 节点;
  • 将当前 fiber 节点上保存的 effect 列表添加到父节点之上;
  • 如果有兄弟节点,将 sibling 节点作为 workInProgress 进入 begin 递阶段;
  • 如果没有兄弟节点,则返回父节点,让父节点进入 complete 归阶段,之后再查找父节点的 sibling 即叔叔节点进入 beigin 递阶段。
export function completeUnitOfWork(unitOfWork) {
    // 完成当前工作单元,然后转到下一个单元 sibling,如果没有 sibling,返回父节点查找叔叔
    let completedWork = unitOfWork;
    do {
        const current = completedWork.alternate;
        const returnFiber = completedWork.return;

        // 该节点需要进行修改(effectTag === placement、delete、update)
        if ((completedWork.effectTag & Incomplete) === NoEffect) {
            let next = completeWork(current, completedWork, subtreeRenderLanes);
            if (next !== null) { // 又有新的工作任务,执行新任务
                workInProgress = next;
                return;
            }

            if (returnFiber !== null && (returnFiber.effectTag & Incomplete) === NoEffect) {
                // 1、将当前节点上存储的子节点 effect 添加到父节点上
                if (returnFiber.firstEffect === null) {
                    returnFiber.firstEffect = completedWork.firstEffect;
                }
                if (completedWork.lastEffect !== null) {
                    if (returnFiber.lastEffect !== null) {
                        returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
                    }
                    returnFiber.lastEffect = completedWork.lastEffect;
                }

                // 2、当前节点本身也具有effect,也添加到父节点上
                const effectTag = completedWork.effectTag;
                if (effectTag > PerformedWork) { // 当前fiber有DOM更新等操作
                    if (returnFiber.lastEffect !== null) {
                        returnFiber.lastEffect.nextEffect = completedWork;
                    } else {
                        returnFiber.firstEffect = completedWork;
                    }
                    returnFiber.lastEffect = completedWork;
                }
            }
        } else {
            // TODO...
        }

        const siblingFiber = completedWork.sibling;
        if (siblingFiber !== null) { // 存在sibling,让sibling进入begin递阶段
            workInProgress = siblingFiber;
            return;
        }
        completedWork = returnFiber;
        workInProgress = completedWork; // 这里可以理解为更新 workInProgress 为 null
    } while (completedWork !== null);
}

在 completeWork 中主要处理:根节点、class类组件、原生节点、文本节点这几种类型元素,函数组件等类型都不做处理:

function completeWork(current, workInProgress, renderLanes) {
    const newProps = workInProgress.pendingProps;
    switch (workInProgress.tag) {
        case IndeterminateComponent:
        case LazyComponent:
        case SimpleMemoComponent:
        case FunctionComponent:
        case ForwardRef:
        case Fragment:
        case Mode:
        case Profiler:
        case ContextConsumer:
        case MemoComponent:
            return null;
        case ClassComponent: {
            // ...
        }
        case HostRoot: {
            // ...
        }
        case HostComponent: {
            // ...
        }
        case HostText: {
            // ...
        }
    }
}

completeUnitOfWork - HostText 文本节点的处理

如果存在老节点并且比较后需要进行更新,则调用 updateHostText 标记节点 effect 为 Update;否则创建新节点。

case HostText: {
    const newText = newProps;
    if (current && workInProgress.stateNode != null) { // 更新文本操作
        const oldText = current.memoizedProps;
        updateHostText(current, workInProgress, oldText, newText); // 新旧文本不同时更新 workInProgress.effectTag = Update
    } else {
        const rootContainerInstance = getRootHostContainer();
        const currentHostContext = getHostContext();
        workInProgress.stateNode = createTextInstance(newText, rootContainerInstance, currentHostContext, workInProgress);
    }
    return null;
}

function updateHostText(current, workInProgress, oldText, newText) {
    if (oldText !== newText) markUpdate(workInProgress);
}
function markUpdate(workInProgress) {
    workInProgress.effectTag |= Update;
}

function createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) {
    const textNode = createTextNode(text, rootContainerInstance);
    textNode[internalInstanceKey] = internalInstanceHandle; // 将textFiber添加到text文本节点上
    return textNode;
}

completeUnitOfWork - 函数组件的处理

尽管说函数组件不需要创建节点,在 completeWork 直接返回出去了,但由于本例函数组件是 HostRoot 的根元素,它的 effectTag 为 3,所以在 completeUnitOfWork 方法中会将它的 effect 加入到父节点(HostRoot)的 effectList 上,即:

const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) { // effectTag > 1 说明有 effect 更新操作
    if (returnFiber.lastEffect !== null) {
        returnFiber.lastEffect.nextEffect = completedWork;
    } else {
        returnFiber.firstEffect = completedWork;
    }
    returnFiber.lastEffect = completedWork;
}
// 由于当前 HostRoot 上没有任何 effect,所以上面代码执行后相当于:
// returnFiber.firstEffect = returnFiber.lastEffect = completedWork; // 将函数组件fiber节点加入 effectList

completeUnitOfWork - HostRoot 根节点的处理

由于 HostRoot 对应的真实 DOM 就是我们的容器节点 id=root,所以不需要做 DOM 创建,这里主要做了调用栈的清空,并标记根节点的 effect 为:Snapshot(256)

case HostRoot: {
    // 将 workInProgress 从执行栈中移除
    popHostContainer(workInProgress);
    popTopLevelLegacyContextObject(workInProgress);
    const fiberRoot = workInProgress.stateNode;
    // 当hostRoot进入completeUnitOfWork阶段时,表示render阶段将要结束,更新context对象信息
    if (fiberRoot.pendingContext) {
        fiberRoot.context = fiberRoot.pendingContext;
        fiberRoot.pendingContext = null;
    }
    
    // 计划在下一次提交开始时清除此容器的效果。
    // 因为是初次渲染,所以根节点的effectTag设为Snapshot,表示根节点也要进行更新,
    // 而且在commitRoot的commitBeforeMutationEffects阶段,会进入处理Snapshot的方法,用于清空根容器下所有的子节点内容,
    // 确保容器节点没有任何内容,方便后续将所以字节点挂载到容器节点上
    workInProgress.effectTag |= Snapshot;
            
    updateHostContainer(workInProgress); // 这是一个空方法
    return null;
}

步骤四:commit 提交阶段

在上面我们知道,在 performSyncWorkOnRoot 中会同步执行 RenderRootSync 方法来处理每个 fiber,render 阶段完成后,便会进入 commit 阶段将 DOM 节点渲染到视图上。

export function performSyncWorkOnRoot(root) {
    renderRootSync(root, lanes); // render 阶段同步执行

    const finishedWork = root.current.alternate; // 刚刚执行完的 workInProcess HostRoot Fiber
    // 在commitRoot中会根据它来执行EffectList链表,遍历每一个effect并作出更新
    // (effectList顺序是从最小的子节点到最顶层需要更新的元素形成)
    root.finishedWork = finishedWork;
    root.finishedLanes = lanes;
    commitRoot(root);

    return null;
}

export function commitRoot(root) {
    const renderPriorityLevel = getCurrentPriorityLevel();
    // 这里使用ImmediateSchedulerPriority最高优先级来执行commitRoot
    runWithPriority(
        ImmediateSchedulerPriority,
        commitRootImpl.bind(null, root, renderPriorityLevel), 
    );
    return null;
}

在 commitRoot 中会根据优先级来执行 commit 提交处理,但核心逻辑在 commitRootImpl 这个方法中。

commitRootImpl 中的逻辑处理就很复杂了,它将 commit 阶段又划分为了三个阶段:

  • 在进入三个阶段之前会做一些初始处理。。。
  • Before Mutation 阶段(执行 DOM 操作前)
  • Mutation 阶段(执行 DOM 操作)
  • Layout 阶段(执行 DOM 操作后)
  • 三个阶段完成之后做一些处理。。。

commit - 进入三个子阶段之前的处理

在 commit 阶段主要是根据 effectList 来处理要进行更新操作的节点。所以在进入 Before Mutation 阶段之前,会先处理 effectList 并拿到第一个要更新的节点:firstEffect,如果根节点上也存在 effectTag,将根节点追加到 effectList 尾部(初渲染会追加根节点)。

function commitRootImpl(root, renderPriorityLevel) {    
    // 取出Render阶段完成的工作树:finishedWork,上面保存了本次渲染要提交的 effectList
    const finishedWork = root.finishedWork;

    // 重置根节点上的变量信息
    root.finishedWork = null;
    root.finishedLanes = NoLanes;
    root.callbackNode = null;
    root.callbackId = NoLanes;

    // 如果根节点上存在 effect,将其添加到 effectList 末尾(如初渲染时根节点的effectTag为:Snapshot=256)
    // 接着取出 effectList 上的第一个节点作为开始更新节点
    let firstEffect;
    if (finishedWork.effectTag > PerformedWork) { // rootFiber存在更新操作
        if (finishedWork.lastEffect !== null) {
            finishedWork.lastEffect.nextEffect = finishedWork; // 初渲染时将根节点加入至 effectList 尾部,用于将视图挂载于容器节点
            firstEffect = finishedWork.firstEffect;
        } else {
            firstEffect = finishedWork;
        }
    } else {
        // 根节点没有effectTag,取出第一个effect执行commit阶段
        firstEffect = finishedWork.firstEffect;
    }
 
    // ... 三个阶段的代码处理。  
    
    // ... 三个阶段之后的处理
}

拿到 firstEffect 后,开始让每个 effect 进入 Before Mutation 阶段、Mutation 阶段、Layout 阶段处理任务。

function commitRootImpl(root, renderPriorityLevel) {
    // ... 三个阶段进入之前的处理
 
    if (firstEffect !== null) {
        // 设置当前执行栈为 commit 阶段
        executionContext |= CommitContext;

        // 阶段一:Before Mutation 阶段
        nextEffect = firstEffect; // nextEffect 是一个全部变量
        do {
            commitBeforeMutationEffects();
        } while (nextEffect !== null);
        
        // 阶段二:Mutation 阶段
        nextEffect = firstEffect;
        do {
            commitMutationEffects(root, renderPriorityLevel);
        } while (nextEffect !== null);
        root.current = finishedWork; // Mutation阶段完成后更新到 current 上(因为节点依旧渲染到视图上了)

        // 阶段三:Layout 阶段
        nextEffect = firstEffect;
        do {
            commitLayoutEffects(root, lanes);
        } while (nextEffect !== null);
        nextEffect = null; // 重置全局变
        executionContext = prevExecutionContext; // 恢复执行栈
    } else {
        // No effects.
        root.current = finishedWork;
    }
    
    // ... 三个阶段执行完成之后的处理
}

commit - Before Mutation 阶段

从上面的代码可以看到,从 firstEffect 开始,遍历 effect 依次进入 commitBeforeMutationEffects 中去处理。方法内部整体处理分为三部分:

  • 处理 DOM 节点上的 autoFocus、blur 逻辑;
  • 调用 getSnapshotBeforeUpdate 生命周期钩子函数(该函数用于代替以前的 ComponentWillXXX 钩子,它是在 commit 阶段下的 Before Mutation 阶段执行,因为是同步的,不会像那些钩子在可中断的 Render 阶段多次调用)。
  • 调度 useEffect(调用 flushPassiveEffects 来异步调度 useEffect )。
function commitBeforeMutationEffects() {
    while (nextEffect !== null) {
        const current = nextEffect.alternate;

        if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
            // ...focus blur相关 处理DOM节点渲染/删除后的 autoFocus、blur 逻辑
        }

        const effectTag = nextEffect.effectTag;

        // 调用getSnapshotBeforeUpdate生命周期钩子 class组件和初次挂载的HostRoot会进入到这里
        if ((effectTag & Snapshot) !== NoEffect) {
            commitBeforeMutationEffectOnFiber(current, nextEffect); // HostRoot做的事情是清除容器内的节点
        }
        // 调度useEffect
        if ((effectTag & Passive) !== NoEffect) {
            if (!rootDoesHavePassiveEffects) {
                rootDoesHavePassiveEffects = true;
                scheduleCallback(NormalSchedulerPriority, () => {
                    // 触发useEffect
                    flushPassiveEffects();
                    return null;
                });
            }
        }
        nextEffect = nextEffect.nextEffect;
    }
}

commit - Mutation 阶段

在 Mutation 阶段会进入 commitMutationEffects 方法中处理,这个阶段就会将更新渲染到视图上。内部会根据 effectTag 类型来为节点做不同处理:

  • Placement effect:获取父级节点,获取兄弟节点(用于insertBefore、appendChild),将 Fiber 对应的 DOM 节点插入到页面父节点上。(期间可能会不断的查找父节点或子节点,因为函数组件没有真实节点)。
  • Update effect:更新原生节点属性。
  • Deletion effect:删除节点。
function commitMutationEffects(root, renderPriorityLevel) {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;
        // ... 省略其他Tag的处理情况,如:refTag

        // 利用运算符更快的判断出 effectTag 归属于哪一类,比如本例:函数组件的effectTag为3,3 & Placement(2) 后得到2,为函数组件执行插入操作
        const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
        switch (primaryEffectTag) {
            // 插入DOM
            case Placement: { // 2
                commitPlacement(nextEffect);
                // ~表示非,作用就是将effectTag置为0,此时后面的layout阶段中拿到的effectTag都变为0了,更新和删除并没有这样做
                nextEffect.effectTag &= ~Placement;
                break;
            }
            // 更新DOM
            case Update: { // 4
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            // 删除DOM
            case Deletion: { // 8
                commitDeletion(root, nextEffect, renderPriorityLevel);
                break;
            }
        }

        nextEffect = nextEffect.nextEffect;
    }
}

对于本例的函数组件来说,它的 effectTag 为 3,会被作为 Placement 插入方式去处理,并调用 commitPlacement。

在这个方法内部会先找到函数组件的父节点,然后将函数组件的子节点添加到父节点上(函数组件不是一个真实DOM节点),此时本例中的 Hello React! 就挂载到了视图容器之上,这时候页面就看到了效果。

function commitPlacement(finishedWork) {
    // 1、获取父级DOM节点。其中finishedWork为传入的Fiber节点
    const parentFiber = getHostParentFiber(finishedWork);
    let parent;
    let isContainer; // 是否为容器
    // 父级DOM节点
    const parentStateNode = parentFiber.stateNode;

    switch (parentFiber.tag) {
        case HostComponent:
            parent = parentStateNode; // 原生 DOM 节点
            isContainer = false;
            break;
        case HostRoot:
            parent = parentStateNode.containerInfo; // 容器节点
            isContainer = true;
            break;
        // ...
        default:
            break;
    }

    // 2、获取Fiber节点的DOM兄弟节点
    const before = getHostSibling(finishedWork);
    // 3、根据DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作。
    if (isContainer) {
        insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
    } else {
        insertOrAppendPlacementNode(finishedWork, before, parent);
    }
}

commit - Layout 阶段

在 Layout 阶段中会进入 commitLayoutEffects 方法,它一共做了两件事:

  • 满足条件调用 class 组件和 Hooks 生命周期函数(componentDidMount、componentDidUpdate、uselayoutEffect),此时可以在钩子函数中拿到更新后的 DOM。
  • 满足条件,赋值 ref 节点属性。
function commitLayoutEffects(root, committedLanes) {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;

        // 1、调用生命周期钩子和hook
        if (effectTag & (Update | Callback)) {
            const current = nextEffect.alternate;
            commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
        }

        // 2、赋值ref
        if (effectTag & Ref) {
            commitAttachRef(nextEffect);
        }

        nextEffect = nextEffect.nextEffect;
    }
}

commit - 三个子阶段完成之后的处理

最后如果函数组件中没有用到 Effect Hook,则主要做的工作是初始化处理,从 firstEffect 开始,遍历每个 effect 节点,并将 effect 标记从 fiber 节点上移除,以便下次更新时重新计算 effect 副作用。

还有一点就是处理生命周期回调中产生的更新,新的更新会开启新的 render-commit 流程。(如:componentDidMount 钩子函数中调用 this.setState)。

function commitRootImpl(root, renderPriorityLevel) {    
    // ... 三个阶段之前的处理
 
    // ... 三个阶段的代码处理。  
    
    // 1、将 effectList 列表初始化为 null
    const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; // 根节点挂载后还有effect
    if (rootDoesHavePassiveEffects) { // 处理 Effect Hook
        rootDoesHavePassiveEffects = false;
        rootWithPendingPassiveEffects = root; // 存在useEffect要处理,保存root
        pendingPassiveEffectsLanes = lanes;
        pendingPassiveEffectsRenderPriority = renderPriorityLevel;
    } else { // 将effectList列表初始化为null
        nextEffect = firstEffect;
        while (nextEffect !== null) {
            const nextNextEffect = nextEffect.nextEffect;
            nextEffect.nextEffect = null;
            if (nextEffect.effectTag & Deletion) {
                detachFiberAfterEffects(nextEffect);
            }
            nextEffect = nextNextEffect;
        }
    }
    
    // 2、开启新的更新流程
    ensureRootIsScheduled(root, now());
    flushSyncCallbackQueue();
}

function detachFiberAfterEffects(fiber) {
    fiber.sibling = null;
}

结尾

至此,一个简单的函数组件初渲染分析完成了。整体阅读下来代码还是很长的,文中如有需要纠正的地方,欢迎读者提出宝贵意见。