likes
comments
collection
share

React设计原理-render阶段

作者站长头像
站长
· 阅读数 14

Reconciler工作的阶段在React内部被称为render阶段. 类组件的render函数、函数组件本身都在该阶段调用. 根据调度器调度结果不同, render阶段可能开始于performSyncWorkOnRoot同步更新流程performConcurrentWorkOnRoot并发更新流程.

// (同步更新流程) 执行该方法
function workLoopSync(){
    // workInProgress(wip) 
    // 代表生成fiber tree工作已经进行到的wip fiberNode
    while(workInProgress !== null){
        // 创建下一个fiberNode并赋值给wip, 将wip与已创建的fiberNode连接构成fiberTree
        performUnitOfWork(workInProgress)
    }
}

// (并发更新流程) 执行该方法
function workLoopConcurrent(){
    // 是否可中断
    while(workInProgress !== null && !shouldYield()){
        performUnitOfWork(workInProgress)
    }
}

流程概览

Fiber Reconciler通过遍历的方式实现可中断的递归, 因此performUnitOfWork的工作可以分为“递”和“归”.

”阶段从HostRootFiber开始向下以DFS的方式遍历执行beginWork方法, 该方法根据传入的fiberNode创建下一级fiberNode. “”阶段调用completeWork方法处理fiberNode. 当某个fiberNode执行完此方法后, 会进入其兄弟fiberNode(如果存在)的“递”阶段, 或者进入父fiberNode的“归”阶段.

“递”阶段和“归”阶段会交错执行直至HostRootFiber的“归”阶段.

function performUnitOfWork(fiberNode){
    // ...执行beginWork工作
    if(fiberNode.child){
        performUnitOfWork(fiberNode.child)
    }
    // ...执行completeWork工作
    if(fiberNode.sibling){
        performUnitOfWork(fiberNode.sibling)
    }
}

beginWork

大致流程:

function beginWork(current, workInProgress,  renderLanes){
    if(current !== null){
        // 首先根据current fiberNode是否存在判断当前属于`mount`还是`update`流程
        // 如果是更新流程, 则 wip fiberNode 存在对应的 current fiberNode
        // 如果本次更新不影响 fiberNode.child 则可以复用对应的 current fiberNode
        // update时判断是否可复用
        // ...
    } else {
        // ...
    }
    
    // 如果无法复用 current fiberNode 则mount和update流程大体一致
    // 根据tag进入不同类型元素的处理分支
    switch(workInProgress.tag){
        case IndeterminateComponent:
            // FC mount时进入...
        case LazyComponent:
            // ...
        case FunctionComponent:
            // FC update时进入...
        case ClassComponent:
            // ...
        case HostRoot:
            // ...
        case HostComponent:
            // 原生element类型 eq. span div ...
        case HostText:
            // 文本元素类型 ...
    }
}

// Reconciler模块的核心部分
// 如果常见类型(FunctionComponent、ClassComponent、HostComponent)没有命中优化策略, 最终进入此方法
function reconcileChildren(current, workInProgress, nextChildren, renderLanes){
    // mount
    if(current === null){
        workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes)
    // update
    } else {
        workInProgress.child = reconcileChildFibers(workInProgress, current.children, nextChildren, renderLanes)
    }
}

// 区别只是传参不同
var reconcileChildFibers = ChildReconcile(true)
var mountChildFibers = ChildReconcile(false)
function ChildReconcile(shouldTrackSideEffects){
    // ... shouldTrackSideEffects表示 是否追踪副作用, 即是否标记flags 
}

执行mountChildFibers时由于shouldTrackSideEffects = false, 即使该fiberNode执行了placeChild(前者内部方法, 标记“要插入UI”的fiberNode)也不会被标记Placement, 因此在Renderer中也不会对“该fiberNode对应的DOM元素”执行“插入页面”的操作. reconcileChildFibers中会标记的flags主要与元素位置有关(标记ChildDeletion代表删除、标记Placement代表插入或移动).

React中的位运算

flags的本质是二进制数的位运算.

// 未处于React上下文
const NoContext = 0b0000;
// 处于Batched上下文   batchedUpdates是一种性能优化手段
const BatchedContext = 0b0001;
// 处于Render阶段
const RenderContext = 0b0010;
// 处于Commit阶段
const CommitContext = 0b0100;


// 1. 当执行流程进入render阶段, 使用按位或标记进入对应上下文
let executionContext = NoContext
executionContext |= RenderContext

// 2. 此时可以结合按位与和NoContext来判断 是否处于某一上下文
(executionContext & RenderContext) !== NoContext // true
(executionContext & CommitContext) !== NoContext // false

// 3. 离开RenderContext上下文后, 结合按位与、按位非移除标记
executionContext &= ~RenderContext
(executionContext & RenderContext) !== NoContext // false

completeWork

根据wip.tag区分对待, completeWork流程大致包括:

  1. 创建或者标记元素更新
  2. flags冒泡

beginWorkreconcileChildFibers方法用来“标记fiberNode的插入、删除、移动”. completeWork的步骤(1)会完成“更新的标记”(该fiberNode在本次更新中的增、删、改操作均已标记完成). 至此Reconciler中“标记flags”相关工作基本完成.

flags冒泡

高效地收集散落在Wip Fiber Tree各处的“被标记的fiberNode”.

// 每个fiberNode将子孙中标记的flags向上冒泡一层
let subtreeFlags = NoFlags

// 收集子fiberNode的子孙fiberNode中标记的flags
subtreeFlags |= child.subtreeFlags
// 收集子fiberNode标记的flags
subtreeFlags |= child.flags
// 附加在当前fiberNode的subtreeFlags上
completedWork.subtreeFlags |= subtreeFlags

HostRootFiber完成completeWork, 整棵Wip Fiber Tree中所有“被标记的flags”都在HostRootFiber.subtreeFlags中定义. 在Renderer中, 通过任意一级fiberNode.subtreeFlags都可以快速确定“该fiberNode所在子树是否存在副作用需要执行”.

mount概览

  1. 根据wip.tag进入不同处理分支
  2. 通过current !== null判断是否处于mount/update流程
  3. 对于HostComponent, 通过createInstance创建fiberNode对应的dom元素
  4. 执行appendAllChildren方法将下一层dom元素插入上面创建的dom元素中
    // 与flags冒泡一样都是处理某个元素的下一级元素
    // 前者处理下一级DOM元素类型, 后者处理下一级flags
    function appendAllChildren(parent, workInProgress, /* ...args */){
        let node = workInProgress.child
        // 从当前`fiberNode`向下遍历
        while(node !== null) {
            // 1) 将第一层DOM元素类型(HostComponent、HostText)通过`appendChildren`方法插入`parent`末尾
            if(node.tag === HostComponent || node.tag === HostText) {
                appendInitialChild(parent, node.stateNode)
            } else if (node.child !== null)(
                node.child.return = node
                node = node.child
                continue;
            ) 
            // 终止情况1: 遍历到parent对应FiberNode
            if(node === workInProgress) return
            // 如果没有兄弟则向父级FiberNode遍历
            while(node.sibling === null) {
                // 终止情况2: 回到最初执行步骤(1)所在层
                if(node.return === null || node.return === workInProgress) return 
                node = node.return
            }
            
            // 2) 对兄弟FiberNode执行步骤(1)
            node.sibling.return = node.return
            node = node.sibling
        }
    }
    
  5. 执行finalizeInitialChildren完成属性的初始化, 包括stylesinnerHTML、文本类型children、不会在DOM中冒泡的事件(cancel、close、load、scroll、invalid、toggle等)及其他属性(setValueForProperty)等.
  6. 执行bubbleProperties完成flags冒泡.

由于appendAllChildren存在, 当completeWork执行到HostRootFiber时已经形成一颗完整的离屏DOM Tree. 由于HostRootFiber存在alternate(HostRootFiber.current!==null), 因此HostRootFiberbeginWork时会进入reconcileChildFibers而不是mountChildFibers.

update概览

mount流程完成了属性的初始化, update流程将完成标记属性的更新. 其主要逻辑在diffProperties中包括两次遍历:

  1. 第一次遍历, 标记删除“更新前有, 更新后没有”的属性
  2. 第一次遍历, 标记更新“update流程前后发生改变”的属性

所有变化属性的key、value会保存在fiberNode.updateQueue中, 同时该fiberNode会被标记updateworkInProgess.flags |= Update.

ReactDOM Renderer

仿照react-dom实现一个ReactDOM Renderer.

// 初始化 ReactReconciler
// ReactReconciler 由 react-reconciler 包提供
// hostConfig是宿主环境的配置项, 提供一系列API. 包括初始化环境信息、创建DOM Node、关键逻辑判断、DOM操作等
const ReactReconcilerInst = ReactReconciler(hostConfig);
export default {
    render(reactElement, domElement, callback){
        if(!document._rootContainer) {
            document._rootContainer = ReactReconcilerInst.createContainer(domElement, false);
        }
        
        return ReactReconcilerInst.updateContainer(reactElement, document._rootContainer, null, callback)
    }
}

总结

Reconciler采用DFS的顺序构建Wip Fiber Tree. 整个过程分为“递”、“归”两个阶段, 分别对应beginWorkcompleteWork方法.

beginWork根据当前fiberNode构建下一级fiberNode, 在update时标记Placement(新增、移动)、ChildDeletion(删除).completeWork在mount时会构建DOM Tree, 初始化属性, 在update时标记Update, 最终执行flags冒泡.

当最终HostRootFiber完成completeWork时, Reconciler的工作流程结束. 此时得到了:

  • 代表本次更新的 Wip Fiber Tree
  • 被标记的 flags

HostRootFiber对应的FiberRootNode会被传递给Renderer进行下一阶段工作.