react学习笔记-2 - Shiina's Blog

react学习笔记-2

2024 年 6 月 4 日 - 18:37:20 发布
16.3K 字 55分钟
本作品采用 CC BY-NC-SA 4.0 进行许可。

react学习笔记-2

此处接着 https://react.iamkasong.com/process/reconciler.html 继续分析

更新流程

先看两个更新方法:

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && /** 需要让出线程了 */ !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

这里也解释了上一张为什么时间切片的调用没法实现的原因,他调用的是 workLoopSync,而不是 Concurrent

workInProgress 代表当前已创建的 workInProgress fiber,这是一个全局变量。

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

接下来看看 performUnitOfWork 干了啥

function performUnitOfWork(unitOfWork: Fiber): void {
  // 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 节点
  const current = unitOfWork.alternate;

  let next;
  // 为每个遍历到的节点调用 beginWork 方法
  // 该方法具体作用看下文,next 代表当前节点的子节点
  next = beginWork(/* 当前屏幕上的节点 */ current, /* 接下来要更新的节点 */ unitOfWork, entangledRenderLanes);

  // ...

  // 更新 props
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    // 没有子节点了,去检查当前节点是否存在 sibling,存在的话就让 sibling 进入 beginWork 阶段
    completeUnitOfWork(unitOfWork);
  } else {
    // 继续递归,回到 workLoop
    workInProgress = next;
  }
}

其中:

  • beginWork 用于决定该组件是否要被更新,并执行组件的 render 方法
  • completeUnitOfWork completeWork 用于创建、更新、删除 DOM 节点,以及调用组件生命周期方法等

看到这里我们可以知道,React 的中断发生时刻位于每个 Fiber 节点对比更新前的时候,而在执行 beginWork 与 completeWork 这段时间则是无法分割操作。

什么嘛,我还以为是像操作系统能从寄存器和时钟中断层面实现的更新打断(逃

更新流程例子

虽然简单但是非常重要,他与一般的dfs在实现上稍微有点区别

https://react.iamkasong.com/process/reconciler.html#%E4%BE%8B%E5%AD%90

手动实现一个可中断的 React 更新流程

个人依照理解实现的

参照着源码写了一份,个人不理解的地方就是回溯的时候是怎么防止死循环的,个人想到的就只有在 compeleteWork 那里来确认节点是否被访问过,但是这样会多出一次节点访问,影响效率

运行环境是 node

function randomUUID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    const r = Math.random() * 16 | 0
    const v = c === 'x' ? r : (r & 0x3 | 0x8)
    return v.toString(16)
  })
}

class FiberNode {
  /** @type {FiberNode} */
  sibling
  /** @type {FiberNode} */
  return
  /** @type {FiberNode} */
  child
  visited = false
  uuid
  constructor({
    parent = null,
    sibling = null,
    child = null
  } = {}) {
    this.return = parent
    this.sibling = sibling
    this.child = child
    this.uuid = randomUUID()
  }
}

/** **Becareful to use!!!** */
function createAFiberNode(layerCount = 5, eachLayerObjCount = 10) {
  const root = new FiberNode()
  root._isRoot = true
  root.child = new FiberNode()
  const dfs = (node, parent, layer = 0) => {
    node.return = parent
    if (layer === layerCount) {
      return
    }

    let currentNode = node
    for (let i = 0; i < eachLayerObjCount; i++) {
      currentNode.sibling = new FiberNode()
      currentNode.sibling.return = parent
      currentNode.child = new FiberNode()
      dfs(currentNode.child, currentNode, layer + 1)
      currentNode = currentNode.sibling
    }
  }

  dfs(root.child, root)
  return root
}

let workingInProgress = null
function beginWork(node) {
  // just dfs to child
  return node?.child || null
}

function completeWork(node) {
  // just dfs to child
  // 这里必须得确认节点是否访问过,否则会卡在 child -> return -> child 的无限死循环导致无法 bail out
  return node?.visited ? null : node?.child || null
}

function completeUnitOfWork(node) {
  let completedWork = node
  do {
    const returnFiber = completedWork.return;
    let next = completeWork(completedWork);

    if (next !== null) {
      workingInProgress = next
      return
    }

    const siblingFiber = completedWork.sibling

    if (siblingFiber !== null) {
      workingInProgress = siblingFiber
      return
    }

    completedWork = returnFiber
    workingInProgress = completedWork
  } while (completedWork !== null);


  // We've reached the root.
}

function goToUnitOfWork(node) {
  let next = beginWork(node)

  if (next !== null) {
    workingInProgress = next
  } else {
    completeUnitOfWork(node)
  }
}

let startTime = +Date.now();
function createNeedYield(duration = 1000) {
  return () => {
    const now = +Date.now();
    const timer_update = now - startTime > duration
    if (timer_update) {
      startTime = now
    }

    return timer_update
  }
}

const needYield = createNeedYield(10)

function workLoopConcurrent() {
  const taskBeginTime = +Date.now();
  while (workingInProgress !== null && !needYield()) {
    workingInProgress.visited = true
    goToUnitOfWork(workingInProgress)
  }
  console.log(`interrupt workLoop, spend ${+Date.now() - taskBeginTime}ms, current work progress is ${workingInProgress?.uuid || 'null'}`);
}

function runWorkLoop() {
  startTime = +Date.now();
  workLoopConcurrent()
}

function printTree(node) {
  if (!node) return
  console.log('node', node.tag, node.key, node.child)
  if (node.child) {
    printTree(node.child)
  }
  if (node.sibling) {
    printTree(node.sibling)
  }
}

function start() {
  workingInProgress = createAFiberNode(5, 10)
  console.log('begin workLoop');
  let timer = setInterval(() => {
    if (workingInProgress === null) {
      clearInterval(timer)
      console.log('end workLoop');
      return
    }
    runWorkLoop()
    console.log('sleep for 1s');
  }, 1000);
}

start()

beginWork 解析

直接看源码吧,此处为 1.18 的

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    // ...各种对比方法用于确认该节点相比 workInProgress 是否发生了更新
    // 即 update
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    // 上下文变化 + props 变化
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // 开发环境转为类型变化,进行强制更新
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // 假如发生了变化,则需要进行完整更新
      didReceiveUpdate = true;
    } else {
      // Neither props nor legacy context changes. Check if there's a pending
      // update or context change.
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
      if (
        !hasScheduledUpdateOrContext &&
        // If this is the second pass of an error or suspense boundary, there
        // may not be work scheduled on `current`, so we check for this flag.
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        // No pending updates or context. Bail out now.
        didReceiveUpdate = false;
        // 复用 current
        return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
      }
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        // This is a special case that only exists for legacy mode.
        didReceiveUpdate = true;
      } else {
        // An update was scheduled on this fiber, but there are no new props
        // nor legacy context. Set this to false. If an update queue or context
        // consumer produces a changed value, it will set this to true. Otherwise,
        // the component will assume the children have not changed and bail out.
        didReceiveUpdate = false;
      }
    }
  } else {
    // 此处无节点,即为 mount
    // ...新进入的节点
    didReceiveUpdate = false;
    // 水合判断 + 拷贝 fiber 判断
    if (getIsHydrating() && isForkedChild(workInProgress)) {}
  }

  // 设置更新优先级为无
  workInProgress.lanes = NoLanes;

  // 根据当前 fiber 的不同类型来走不同的 挂载/更新 流程
  switch (workInProgress.tag) {
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        disableDefaultPropsExceptForClasses ||
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent:
    // ...
  }
}

对于我们常见的组件类型,如 FunctionComponent/ClassComponent/HostComponent,最终会进入 reconcileChildren 方法。

reconcileChildren

1.18 源码:

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  // mount
  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } 
  // update,使用 diff 算法
  else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

byd 上面都解释的很清楚了,这个方法最终都会生成一个新的 fiber 节点并作为 beginWork 的返回值,然后赋值给 workInProgress,即下一个工作单元的目标对象

effectTag

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

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;

那么,如果要通知Renderer将Fiber节点对应的DOM节点插入页面中,需要满足两个条件:

  • fiber.stateNode 存在,即Fiber节点中保存了对应的DOM节点
  • (fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag

我们知道,mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?

针对第一个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。

第二个问题的答案十分巧妙:假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。

为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。

completeWork 解析

completeUnitOfWork 缩略源码

function completeUnitOfWork(unitOfWork: Fiber): void {
  // 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.
  let completedWork: Fiber = unitOfWork;
  do {
    // 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 = completedWork.alternate;
    // parent 节点
    const returnFiber = completedWork.return;

    let next;
    // 处理已完成更新的 fiber 节点
    next = completeWork(current, completedWork, entangledRenderLanes);

    // 还有子节点更新,继续
    if (next !== null) {
      // Completing this fiber spawned new work. Work on that next.
      workInProgress = next;
      return;
    }

    // 检查 sibling,有就继续走 beginWork
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    // $FlowFixMe[incompatible-type] we bail out when we get a null
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);

  // We've reached the root.
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

completeWork 缩略源码

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  // Note: This intentionally doesn't check if we're hydrating because comparing
  // to the current tree provider fiber is just as fast and less error-prone.
  // Ideally we would have a special version of the work loop only
  // for hydration.
  popTreeContext(workInProgress);
  // 
  switch (workInProgress.tag) {
    case IncompleteFunctionComponent: {
      if (disableLegacyMode) {
        break;
      }
      // Fallthrough
    }
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      bubbleProperties(workInProgress);
      return null;
    case ClassComponent: {
      const Component = workInProgress.type;
      if (isLegacyContextProvider(Component)) {
        popLegacyContext(workInProgress);
      }
      bubbleProperties(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  }

  throw new Error(
    `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
      'React. Please file an issue.',
  );
}

从 HostComponent 看起

这里是 原生DOM 对应 FiberNode 的一层,其他的另外再说(

以下所有源码均来自 React18

case HostComponent: {
  popHostContext(workInProgress);
  const type = workInProgress.type;
  
  if (current !== null && workInProgress.stateNode != null) {
    // update
  } else {
    // mount
  }
  bubbleProperties(workInProgress);
  // This must come at the very end of the complete phase, because it might
  // throw to suspend, and if the resource immediately loads, the work loop
  // will resume rendering as if the work-in-progress completed. So it must
  // fully complete.
  preloadInstanceAndSuspendIfNeeded(
    workInProgress,
    workInProgress.type,
    workInProgress.pendingProps,
    renderLanes,
  );
  return null;
}

HostComponent update

似乎没什么好说的,这里解释一下 updateHostComponent 里面干了啥:

  • 浅比较 props,确认是否要更新
  • 调用外部的渲染器方法创建实例对象
  • 调用渲染器方法,处理实例:
    • onClick、onChange等回调函数的注册
    • 处理style prop
    • 处理DANGEROUSLY_SET_INNER_HTML prop
    • 处理children prop

PS:这里与网站写的不同,因为这里只关注了协调器本身,网站原文是涉及到了渲染器,以及队列更新过程,但在 18 这些部分似乎都放到了其他地方

updateHostComponent(
  current,
  workInProgress,
  type,
  newProps,
  renderLanes,
);

后续的 updateQueue 在 18 也找不到了,只在 SuspenseComponent 看到过

16 则没有把这些进行分离

HostComponent mount

mount 逻辑:

// ...渲染前判断有没有 newProps
const currentHostContext = getHostContext();

const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
  // hydrate 准备
} else {
  const rootContainerInstance = getRootHostContainer();
  // 根据节点调用渲染器方法创建实例
  const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
  // 将新的child DOM 节点插入到生成的实例中
  appendAllChildren(instance, workInProgress, false, false);
  // 赋值给 stateNode
  workInProgress.stateNode = instance;
  // 调用渲染器方法处理实例
  if (
    finalizeInitialChildren(
      instance,
      type,
      newProps,
      currentHostContext,
    )
  ) {
    markUpdate(workInProgress);
  }
}

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

原因就在于completeWork中的appendAllChildren方法。

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

即 react 自底向上回溯时,会在内存里从底层一步步创建元素,并向更高层的 DOM 元素插入进去,这样就做到了不需要更新用户视图也能创建 / 更新 DOM,当回到根元素时,我们就有了一个完整的 DOM 树。

在浏览器里头实现一下

不急,等后续看 mini-react 仓库再说吧(逃

个人信息
avatar
Shiinafan
文章
40
标签
52
归档
4
导航