React 源码学习之 render 阶段

1,435 阅读8分钟

xps-kLfkVa_4aXM-unsplash.jpg

图片来源: unsplash.com/photos/kLfk…

前言

当前 React 共有三种模式:

  • legacy 模式: ReactDOM.render(element, rootNode)。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。
  • blocking 模式: ReactDOM.createBlockingRoot(rootNode).render(element)。目前正在实验中。作为迁移到 concurrent 模式的第一个步骤。
  • concurrent 模式: ReactDOM.createRoot(rootNode).render(element)。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。这个模式开启了所有的新功能。

React 的 render 阶段开始于 performSyncWorkOnRoot 或者 performConcurrentWorkOnRoot 方法的调用,这主要取决于本次更新是同步更新还是异步更新

performSyncWorkOnRoot & performConcurrentWorkOnRoot

  • performSyncWorkOnRoot

WeChat758392d2e392bcc016f5800c6b2b303c.png

// performSyncWorkOnRoot 会调用该方法

// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

可以从这里看到源码

  • performConcurrentWorkOnRoot

WeChat59fd08bafbcfc1cd10e92f2b61b5b5be.png

// performConcurrentWorkOnRoot 会调用该方法

/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

可以从这里看到源码

这两种模式唯一的区别是是否调用 shouldYield。如果当前浏览器帧没有剩余时间,shouldYield 会中止循环,直到浏览器有空闲时间后再继续遍历。

workInProgress 代表当前已经创建的 workInProgress Fiber。

performUnitOfWork 方法会创建下一个 Fiber 节点并赋值给 workInProgress,并将 workInProgress 与已创建的 Fiber 节点连接起来构成 Fiber 树。

React Fiber Reconciler 是从 Stack Reconciler 重构而来,通过遍历的方式实现可中断的递归,所以 performUnitOfWork 的工作可以分为两部分:“递”和“归”

“递”阶段

首先从组件树的根节点 rootFiber 开始向下深度优先遍历。为遍历到的每个 Fiber 节点调用 beginWork 方法。

该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。

当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。

“归”阶段

在“归”阶段会调用 completeWork 处理 Fiber 节点。

当某个 Fiber 节点执行完 completeWork ,如果其存在兄弟 Fiber 节点(即fiber.sibling !== null),会进入其兄弟 Fiber 的“递”阶段。

如果不存在兄弟 Fiber,会进入父级 Fiber 的“归”阶段。

“递”和“归”阶段会交错执行直到“归”到 rootFiber。至此,render 阶段的工作就结束了。

举例

function App() {
	return (
		<div className="App">
			学习React
			<span>源码</span>
		</div>
	);
}

export default App;

ReactDOM.render(<App />, document.getElementById('root'));

对应的 Fiber树 结构

未命名绘图 (9).png

render 阶段会依次执行:

  1. rootFiber beginWork
  2. App Fiber beginWork
  3. div Fiber beginWork
  4. 学习React beginWork
  5. 学习React completeWork
  6. span Fiber beginWork
  7. span Fiber completeWork
  8. div Fiber completeWork
  9. App Fiber completeWork
  10. rootFiber completeWork

之所以没有 “源码” Fiber 节点的 beginWork/completeWork,是因为作为一种性能优化手段,针对只有单一文本子节点的Fiber,React会特殊处理。

beginWork

可以从 这里 看到 beginWork 源码

beginWork 的工作是传入当前 Fiber节点,创建子 Fiber 节点

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
 // ...省略函数体
}
  • current:当前组件对应的 Fiber 节点在上一次更新时的 Fiber 节点,即workInProgress.alternate;
  • workInProgress:当前组件对应的 Fiber 节点;
  • renderLanes:优先级相关;

从 React 双缓存机制中,除 rootFiber(应用的根节点) 以外, 组件 mount 时,由于是首次渲染,是不存在当前组件对应的 Fiber 节点在上一次更新时的 Fiber 节点,即 mount 时 current === null。

组件 update 更新时,由于之前已经 mount 过,所以 current !== null。

基于此原因,beginWork 的工作可以分为两部分:

  • 组件 mount 时:除 rootFiber 以外,current === null。会根据 fiber.tag 不同,创建不同类型的子 Fiber 节点;
  • update 时:如果 current 存在,在满足一定条件时可以复用 current 节点,这样就能克隆 current.child 作为 workInProgress.child,而不需要新建 workInProgress.child。
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    // ...省略代码

    // 复用 current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    case SuspenseComponent:
     // ...省略
    // ...省略其他类型
  }
}

update 时

满足如下情况时 didReceiveUpdate === false(复用前一次更新的子 Fiber,不需要新建子Fiber)

  1. oldProps === newProps && workInProgress.type === current.type,即 props 与 fiber.type 不变;
  2. !includesSomeLane(renderLanes, updateLanes),即当前 Fiber 节点优先级不够;
if (current !== null) {
    // 省略代码...

    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
     
      switch (workInProgress.tag) {
       // 省略代码...
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
      
        didReceiveUpdate = true;
      } else {
      
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

mount 时

不满足优化路径时,我们就进入第二部分,新建子 Fiber。根据 fiber.tag 不同,进入不同类型Fiber 的创建逻辑。

可以从 这里 看到 tag 对应的组件类型

// mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    case SuspenseComponent:
     // ...省略
    // ...省略其他类型
  }

reconcileChildren

reconcileChildrenReconciler 模块的核心部分。

  1. 对于 mount 时的组件,它会创建新的子 Fiber 节点;

  2. 对于 update 时的组件,它会将当前组件与该组件在上次更新时对应的 Fiber 节点比较(也就是俗称的 Diff 算法),将比较的结果生成新 Fiber 节点

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
 
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
  
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

可以从 这里 看到 reconcileChildren 源码

通过 current === null ? 来区分组件 mount 阶段与 update 阶段。最终它会生成新的子 Fiber 节点并赋值给 workInProgress.child,作为本次 beginWork 的 返回值

reconcileChildFibersmountChildFibers 函数的区别是传入的布尔值不一样。

export const reconcileChildFibers = ChildReconciler(true);

export const mountChildFibers = ChildReconciler(false);

可以从 这里 看到源码

ChildReconciler 方法主要判断是否追踪副作用。

function ChildReconciler(shouldTrackSideEffects) {

 function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
    if (!shouldTrackSideEffects) {
      // Noop.
      return;
    }
    const deletions = returnFiber.deletions;
    if (deletions === null) {
      returnFiber.deletions = [childToDelete];
      returnFiber.flags |= ChildDeletion;// 标记 effectTag
    } else {
      deletions.push(childToDelete);
    }
  }

  ...省略代码
  
}

可以从 这里 看 ChildReconciler 到源码

effectTag

render 阶段的工作是在内存中进行,当工作结束后会通知 Renderer 需要执行的 DOM操作。要执行DOM操作的具体类型就保存在 fiber.effectTag 中。

你可以从 这里 看到 effectTag 对应的DOM操作

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;
....

Renderer 阶段将 Fiber 节点对应的 DOM 节点插入页面中,需要满足两个条件:

  1. fiber.stateNode 存在,即 Fiber 节点中保存了对应的 DOM 节点;

  2. Fiber 节点存在 Placement effectTag;

fiber.stateNode 会在 completeWork 中创建的,在 mount 时只有 rootFiber(存在current) 会赋值 Placement effectTag,在 commit 阶段只会执行一次插入操作。

beginWork 流程图

未命名绘图 (11).png

completeWork

可以从 这里 看到 completeWork 源码

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  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: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

我们重点关注页面渲染所必须的 HostComponent(原生 DOM 组件对应的 Fiber 节点)。

HostComponent

case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      
      if (current !== null && workInProgress.stateNode != null) {
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );

        // update 的情况
       // ...省略
        
      } else {
         // mount 的情况
        // ...省略
      }
      return null;
    }

workInProgress.stateNode 表示 Fiber 节点对应的 DOM 节点。

组件 update 时

当 update 时,Fiber 节点已经存在对应 DOM 节点,所以不需要生成 DOM 节点。需要做的主要是处理props

if (current !== null && workInProgress.stateNode != null) {
        // 组件 update 时
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );

        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
    } 

你可以从 这里 看到 updateHostComponent 方法定义。

在 updateHostComponent 内部,被处理完的 props 会被赋值给workInProgress.updateQueue,并最终会在 commit 阶段被渲染在页面上。

 updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {
  
    var oldProps = current.memoizedProps;

    if (oldProps === newProps) {
      return;
    } 


    var instance = workInProgress.stateNode;
    var currentHostContext = getHostContext();

    var updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext); 

    workInProgress.updateQueue = updatePayload; 

    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

其中 updatePayload 为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value。

组件 mount 时

mount 时的主要逻辑包括三个:

  1. 为 Fiber 节点生成对应的 DOM 节点;
  2. 将子孙 DOM 节点插入刚生成的 DOM 节点中;
  3. 与update 逻辑中的 updateHostComponent 类似的处理 props 的过程;
const currentHostContext = getHostContext();

if(wasHydrated){
  ...
}else {
  // 为fiber创建对应DOM节点
  const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
        );
  // 将子孙DOM节点插入刚生成的DOM节点中      
  appendAllChildren(instance, workInProgress, false, false);
  
  // DOM节点赋值给fiber.stateNode
  workInProgress.stateNode = instance;
  
  // 与update逻辑中的updateHostComponent类似的处理props的过程
   if (
   finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
   )
 ) {
  markUpdate(workInProgress);
 }
}


mount 时只会在 rootFiber 存在 Placement effectTag 。那么 commit 阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?

原因在于 completeWork 中的 appendAllChildren 方法。

由于 completeWork 属于“归”阶段调用的函数,每次调用 appendAllChildren 时都会将已生成的子孙 DOM 节点插入当前生成的 DOM 节点下。那么当“归”到 rootFiber 时,我们已经有一个构建好的离屏 DOM 树。

effectList

作为 DOM 操作的依据,在 commit 阶段需要找到所有有 effectTagFiber 节点并依次执行 effectTag 对应操作。如果在 commit 阶段再遍历一次 Fiber 树寻找 effectTag !== null 的Fiber节点显然很低效。

为了解决这个问题,在 completeWork 的上层函数 completeUnitOfWork 中,每个执行完 completeWork 且存在 effectTag 的 Fiber 节点会被保存在一条被称为 effectList 的单向链表中。

effectList 中第一个 Fiber 节点保存在 fiber.firstEffect,最后一个元素保存在fiber.lastEffect

类似 appendAllChildren ,在“归”阶段,所有有 effectTag 的 Fiber 节点都会被追加在 effectList 中,最终形成一条以 rootFiber.firstEffect 为起点的单向链表。

                         nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber

在 commit 阶段只需要遍历 effectList 就能执行所有 effect 了。

你可以在 这里 看到 completeUnitOfWork 代码逻辑。

流程结尾

至此,render 阶段全部工作完成。在 performSyncWorkOnRoot 函数中 fiberRootNode 被传递给 commitRoot 方法,开启 commit 阶段工作流程。

 const finishedWork: Fiber = (root.current.alternate: any);
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  commitRoot(root);

completeWork 流程图

未命名绘图 (12).png

整个渲染流程

export default class App extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			num: 0,
		};
	}

	updateNum() {
		const { num } = this.state;
		this.setState({ num: num + 1 });
	}

	render() {
		return (
			<div className="App">
				<div onClick={this.updateNum.bind(this)}>学习 React--num:{this.state.num}</div>
				<span>源码</span>
			</div>
		);
	}
}

ReactDOM.render(<App />, document.getElementById('root'));

以上面 class 组件为例子

  1. rootFiber beginWork

  2. App Fiber beginWork

  3. className 为"App" 的 div Fiber beginWork

  4. 存在 onClick 事件的 div Fiber beginWork

  5. 学习 React--num:” Fiber beginWork

  6. 学习 React--num:” Fiber completeWork

  7. num 为 0 Fiber beginWork

  8. num 为 0 Fiber completeWork

  9. 存在 onClick 事件的 div Fiber completeWork

  10. span Fiber beginWork

  11. span Fiber completeWork

  12. className 为"App" 的 div Fiber completeWork

  13. App Fiber completeWork

  14. rootFiber completeWork

组件 mount 阶段

我们知道 render 阶段首先会执行 beginWork 方法,该方法只会返回一个子 fiber 节点,以上面的测试 Demo 为例来看看主要流程。

首先进入 beginWork 是当前应用的根节点 rootFiber 。根据fiber 双缓存原理,此时 存在tag 为 3 的 HostRootcurrent fiber 以及 workInProgress

WeChatc91fe0877d982c22ce1384f5973c473a.png

WeChat2077472ad34d8bee0b1be7246d9c0b1a.png

第二个进入 beginWork 的是 tag 为 1 的 App ClassComponent , 它没有 current fiber , 只有 workInProgress fiber。

WeChat457e27971a428069587c51277d7cfa7c.png

第三个进入 beginWork 的是 tag 为 5 的 div HostComponent , 它也没有 current fiber , 只有 workInProgress fiber。

WeChat42b26568f2645092ca1a4218bc179dd0.png

剩下的省略.....

div HostComponent 为例,研究它 mount 阶段的主要过程。

beginWork

首先进入 beginWork ,根据 workInProgress.tag 进入 updateHostComponent;
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
 ....
 
 switch (workInProgress.tag) {
     .....
     
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
      
    .....
 }
 
 ...
}
在 updateHostComponent 方法中,var isDirectTextChild = shouldSetTextContent(type, nextProps); 是判断当前的 fiber 是否只有唯一的文本子节点,如果是的话不会为它创建一个子 fiber 节点,这是一种优化手段
function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  pushHostContext(workInProgress);

  if (current === null) {
    tryToClaimNextHydratableInstance(workInProgress);
  }

  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;

  let nextChildren = nextProps.children;
  const isDirectTextChild = shouldSetTextContent(type, nextProps);

  if (isDirectTextChild) {
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {

    workInProgress.flags |= ContentReset;
  }

  markRef(current, workInProgress);
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
在 updateHostComponent 会调用 reconcileChildren 方法会为当前 workInProgress 创建子 fiber 节点。

 reconcileChildren(current, workInProgress, nextChildren, renderLanes);
 
 return workInProgress.child;
  

WeChat55a910b3608e4f90db5f029eff079fbf.png

在 reconcileChildren 方法中,根据 current fiber 是否存在进入不同的处理逻辑。当前是 mount 阶段不存在 current fiber,所以进入 mountChildFibers 方法中。
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
   
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
 
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

WeChatd22800ac8a1d8a7372c62b00affbde52.png


export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

mountChildFibersreconcileChildFibers 都是通过 ChildReconciler 方法传入不同的布尔值得到的。

ChildReconciler 方法返回 reconcileChildFibers 方法。

function ChildReconciler(shouldTrackSideEffects) {
 function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
    if (!shouldTrackSideEffects) {
      // Noop.
      return;
    }
    const deletions = returnFiber.deletions;
    if (deletions === null) {
      returnFiber.deletions = [childToDelete];
      returnFiber.flags |= ChildDeletion;
    } else {
      deletions.push(childToDelete);
    }
  }
 ....
 
 return reconcileChildFibers;
}

shouldTrackSideEffects 表示是否追踪副作用,如果不追踪则直接返回,如果追踪则会打上标记。

reconcileChildFibers

在 reconcileChildFibers 方法中通过判断不同的 newChild 的类型进入不同的处理逻辑。当前 newChild 为数组,则进入 reconcileChildrenArray 的处理逻辑。

function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
   
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }

 
    if (typeof newChild === 'object' && newChild !== null) {
      ....

      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
      
     .....
      
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

WeChat2ffa51bd7ee64c6eb462cfe419733e36.png

reconcileChildrenArray

在 reconcileChildrenArray 方法中会创建 newFiber 节点,但只会创建一个。

WeChatdbd8b0f90ee4cbc2f4314265f7db235c.png

通过上图中我们知道会调用 createChild 方法。

但只会创建一个 fiber 节点

WeChatf49887ef814c2ecb862fdf1696b317ee.png

createChild

createChild 方法会根据 newChild 不同的类型进入不同的处理逻辑。

WeChat5ca0b91a25c11a1b67f2383ee355659a.png

createFiberFromElement

createFiberFromElement 方法会调用 createFiberFromTypeAndProps 并且返回 fiber

export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let owner = null;
  
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props;
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    lanes,
  );
 
  return fiber;
}
createFiberFromTypeAndProps

WeChat8751da1b5115a728529968e11faf66f0.png

然后会调用 createFiber 方法创建 fiber 节点。

WeChat429ba0a21bbac9d5a7e986bd198191ec.png

createFiber
var createFiber = function (tag, pendingProps, key, mode) {

  return new FiberNode(tag, pendingProps, key, mode);
  
};

WeChatc94a81eb0ed3988e4094ce90af381a70.png

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.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;
}
“递” 阶段的调用栈

WeChat6048c919c203a9cc6938f2453d8c3152.png

completeWork

首先进入 completeWork 的是 tag6HostText “学习 React--num:” 文本 fiber 节点 ; 第二个进入 completeWork 同样是 tag 为 6 的是 “ 0 ” 文本 fiber 节点 ; 第三个进入 completeWork 的是 tag 为 5 ,elementType 为 "div",存在 onClick 事件的 HostComponent ,我们以该 fiber 节点为例。


function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {

 const newProps = workInProgress.pendingProps;
 switch (workInProgress.tag) {
   ....
   
   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 {
        if (!newProps) {
         
          bubbleProperties(workInProgress);
          return null;
        }

        const currentHostContext = getHostContext();
      
        if (wasHydrated) {
           .....
          }
        } else {
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );

          appendAllChildren(instance, workInProgress, false, false);

          workInProgress.stateNode = instance;

          if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
        }

        if (workInProgress.ref !== null) {
          markRef(workInProgress);
        }
      }
      bubbleProperties(workInProgress);
      return null;
    }
    
    ...
 
 }
}

createInstance

createInstance 方法会创建对应的 DOM 节点 。

  const instance = createInstance(type,newProps,rootContainerInstance,
           currentHostContext,
            workInProgress);

WeChat425cd8d59bb04ab80e8a639b90b633a1.png

createElement

在 createInstance 方法中会调用 createElement 方法创建 DOM 节点。

WeChat61e93513bc0ec06796cf2fbb3a93c4f8.png

function createElement(type, props, rootContainerElement, parentNamespace) {
  var isCustomComponentTag; 

  var ownerDocument = getOwnerDocumentFromRootContainer(rootContainerElement);
  var domElement;
  var namespaceURI = parentNamespace;

  if (namespaceURI === HTML_NAMESPACE) {
    namespaceURI = getIntrinsicNamespace(type);
  }

  if (namespaceURI === HTML_NAMESPACE) {
    {
      isCustomComponentTag = isCustomComponent(type, props); // Should this check be gated by parent namespace? Not sure we want to
      // allow <SVG> or <mATH>.

      if (!isCustomComponentTag && type !== type.toLowerCase()) {
        error('<%s /> is using incorrect casing. ' + 'Use PascalCase for React components, ' + 'or lowercase for HTML elements.', type);
      }
    }

    if (type === 'script') {
      // Create the script via .innerHTML so its "parser-inserted" flag is
      // set to true and it does not execute
      var div = ownerDocument.createElement('div');

      div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
      // This is guaranteed to yield a script element.

      var firstChild = div.firstChild;
      domElement = div.removeChild(firstChild);
    } else if (typeof props.is === 'string') {
      // $FlowIssue `createElement` should be updated for Web Components
      domElement = ownerDocument.createElement(type, {
        is: props.is
      });
    } else {
    
      domElement = ownerDocument.createElement(type); 

      if (type === 'select') {
        var node = domElement;

        if (props.multiple) {
          node.multiple = true;
        } else if (props.size) {
        
          node.size = props.size;
        }
      }
    }
  } else {
    domElement = ownerDocument.createElementNS(namespaceURI, type);
  }

  {
    if (namespaceURI === HTML_NAMESPACE) {
      if (!isCustomComponentTag && Object.prototype.toString.call(domElement) === '[object HTMLUnknownElement]' && !hasOwnProperty.call(warnedUnknownTags, type)) {
        warnedUnknownTags[type] = true;

        error('The tag <%s> is unrecognized in this browser. ' + 'If you meant to render a React component, start its name with ' + 'an uppercase letter.', type);
      }
    }
  }

  return domElement;
}
appendAllChildren

appendAllChildren 方法是将已经创建好的 DOM 插入之前已经创建好的 DOM 树中。

 var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
  
 appendAllChildren(instance, workInProgress, false, false);
 
 workInProgress.stateNode = instance; 
           

 if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
              markUpdate(workInProgress);
            }
workInProgress.stateNode = instance;

将创建好的 DOM 节点赋值给 workInProgress fiber 的 stateNode。

finalizeInitialChildren

finalizeInitialChildren 方法为 DOM 节点设置属性。

performSyncWorkOnRoot

当所有的节点都执行完 completeWork 时,会进入 performSyncWorkOnRoot 的其它逻辑。

function performSyncWorkOnRoot(root) {
  ....
  var finishedWork = root.current.alternate;
  
  root.finishedWork = finishedWork;
  
  root.finishedLanes = lanes;
  
  commitRoot(root); // 进入 commit 阶段

  ensureRootIsScheduled(root, now());
  return null;
}

root.current.alternate 指向当前本次更新创建的 workInProgress fiber 节点。

WeChat7195ca9b514a3e001eb8f9e7d7f7bcc5.png

WeChat51f5e97475bdc60685553a4e809228d6.png

组件 update 阶段

当我们点击 div 标签改变 num 的值时,组件就进入 update 阶段。我们还是以
<div onClick={this.updateNum.bind(this)}> 学习 React--num:{this.state.num}</div> div 标签为例。

beginWork

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
    ...
   if (current !== null) {
      var oldProps = current.memoizedProps;
      var newProps = workInProgress.pendingProps;
       if (oldProps !== newProps || hasContextChanged() || ( workInProgress.type !== current.type )) {
      didReceiveUpdate = true;
      } ....
      
      
   } else {
    didReceiveUpdate = false;
  }
  workInProgress.lanes = NoLanes;
  switch (workInProgress.tag) {
    ...
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    ...
  }
  ....
}

在组件更新阶段此时已经存在 current fiber , 通过条件判断设置 didReceiveUpdate = true, 紧接着进入 updateHostComponent 逻辑。

updateHostComponent
function updateHostComponent(current, workInProgress, renderLanes) {
  .....
  
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
  
}

在 updateHostComponent 方法中会调用 reconcileChildren

reconcileChildren

reconcileChildren 方法通过 current fibernextChildren 进行对比,将对比的结果创建一个新的 fiber 节点并返回。

WeChat91ae70f8cb4deda9c63ae83ed9a1d818.png

completeWork

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    ...
    case HostComponent: {
      ....
      if (current !== null && workInProgress.stateNode != null) {
        updateHostComponent$1(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );

       .....
      }
      ....
  }
  .....
 }
updateHostComponent$1
   updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {
   
   .....
    
    var updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext); 

    workInProgress.updateQueue = updatePayload; 

    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

updateHostComponent$1 方法中最重要的是 prepareUpdate 方法,prepareUpdate 方法跟属性的改变有关。

prepareUpdate
function prepareUpdate(domElement, type, oldProps, newProps, rootContainerInstance, hostContext) {
  {
  .....
  
  return diffProperties(domElement, type, oldProps, newProps);
}
diffProperties
function diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) {
 .....
 
 return updatePayload;
}

diffProperties 方法最后返回 updatePayload, 然后赋值给workInProgress.updateQueue。

自此,render 阶段的 update 主要过程就完了。

参考资料

React技术揭秘