亲宝软件园·资讯

展开

React18之update流程从零实现详解

sunnyhuang519626 人气:0

引言

本系列是讲述从0开始实现一个react18的基本版本。由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

仓库地址

具体章节代码3个commit

本章我们主要讲解通过useState状态改变,引起的单节点update更新阶段的流程。

对比Mount阶段

对比我们之前讲解的mount阶段,update阶段也会经历大致的流程, 只是处理逻辑会有不同:

之前的章节我们主要讲了reconciler(调和) 阶段中mount阶段:

这一节的update阶段如下:

begionWork阶段:

completeWork阶段:

commitWork阶段:

useState阶段:

下面我们分别一一地实现单节点的update更新流程

beginWork流程

对于单一节点的向下调和流程,主要在childFibers文件中,分2种,一种是文本节点的处理reconcileSingleTextNode, 一种是标签节点的处理reconcileSingleElement

复用fiberNode

update阶段的话,主要有一点是要思考如何复用之前mount阶段已经创建的fiberNode

我们先以reconcileSingleElement为例子讲解。

当新的ReactElement的type 和 key都和之前的对应的fiberNode都一样的时候,才能够进行复用。我们先看看reconcileSingleElement是复用的逻辑。

function reconcileSingleElement(
  returnFiber: FiberNode,
  currentFiber: FiberNode | null,
  element: ReactElementType
) {
  const key = element.key;
  // update的情况 <单节点的处理 div -> p>
  if (currentFiber !== null) {
    // key相同
    if (currentFiber.key === key) {
      // 是react元素
      if (element.$$typeof === REACT_ELEMENT_TYPE) {
        // type相同
        if (currentFiber.type === element.type) {
          const existing = useFiber(currentFiber, element.props);
          existing.return = returnFiber;
          return existing;
        }
      }
    }
  }
}

useFiber

复用的逻辑本质就是调用了useFiber, 本质上,它是通过双缓存书指针alternate,它接受已经渲染对应的fiberNode以及新的Props 巧妙的运用我们之前创建wip的逻辑,可以很好的复用fiberNode

/**
 * 双缓存树原理:基于当前的fiberNode创建一个新的fiberNode, 而不用去调用new FiberNode
 * @param {FiberNode} fiber 正在展示的fiberNode
 * @param {Props} pendingProps 新的Props
 * @returns {FiberNode}
 */
function useFiber(fiber: FiberNode, pendingProps: Props): FiberNode {
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

对于reconcileSingleTextNode

删除旧的和新建fiberNode

当不能够复用fiberNode的时候,我们除了要像mount的时候新建fiberNode(已经有的逻辑),还需要删除旧的fiberNode

我们先以reconcileSingleElement为例子讲解。

beginWork阶段,我们只需要标记删除flags。以下2种情况我们需要额外的标记旧fiberNode删除

function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode) {
  if (!shouldTrackEffects) {
    return;
  }
  const deletions = returnFiber.deletions;
  if (deletions === null) {
    // 当前父fiber还没有需要删除的子fiber
    returnFiber.deletions = [childToDelete];
    returnFiber.flags |= ChildDeletion;
  } else {
    deletions.push(childToDelete);
  }
}

我们将需要删除的节点,通过数组形式赋值到父节点deletions中,并标记ChildDeletion有节点需要删除。

对于reconcileSingleTextNode, 当渲染视图中是HostText就可以直接复用。整体代码如下:

function reconcileSingleTextNode(
  returnFiber: FiberNode,
  currentFiber: FiberNode | null,
  content: string | number
): FiberNode {
  // update
  if (currentFiber !== null) {
    // 类型没有变,可以复用
    if (currentFiber.tag === HostText) {
      const existing = useFiber(currentFiber, { content });
      existing.return = returnFiber;
      return existing;
    }
    // 删掉之前的 (之前的div, 现在是hostText)
    deleteChild(returnFiber, currentFiber);
  }
  const fiber = new FiberNode(HostText, { content }, null);
  fiber.return = returnFiber;
  return fiber;
}

completeWork流程

当在beginWork做好相应的删除和移动标记后,在completeWork主要是做更新的标记。

对于单一的节点来说,更新标记分为2种,

这里我们只对HostText中的类型进行讲解。

case HostText:
  if (current !== null && wip.stateNode) {
    //update
    const oldText = current.memoizedProps.content;
    const newText = newProps.content;
    if (oldText !== newText) {
      // 标记更新
      markUpdate(wip);
    }
  } else {
    // 1. 构建DOM
    const instance = createTextInstance(newProps.content);
    // 2. 将DOM插入到DOM树中
    wip.stateNode = instance;
  }
  bubbleProperties(wip);
  return null;

从上面我们可以看出,我们根据文本内容的不同,进行当前节点wip进行标记。

function markUpdate(fiber: FiberNode) {
  fiber.flags |= Update;
}

commitWork流程

通过beginWorkcompleteWork之后,我们得到了相应的标记。在commitWork阶段,我们就需要根据相应标记去处理不同的逻辑。本节主要讲解更新删除阶段的处理。

更新update

在之前的章节中,我们讲解了commitWorkmount阶段,我们现在根据update的flag进行逻辑处理。

// flags update
if ((flags & Update) !== NoFlags) {
  commitUpdate(finishedWork);
  finishedWork.flags &= ~Update;
}

commitUpdate

对于文本节点,commitUpdate主要是根据新的文本内容,更新之前的dom的文本内容。

export function commitUpdate(fiber: FiberNode) {
  switch (fiber.tag) {
    case HostText:
      const text = fiber.memoizedProps.content;
      return commitTextUpdate(fiber.stateNode, text);
  }
}
export function commitTextUpdate(textInstance: TestInstance, content: string) {
  textInstance.textContent = content;
}

删除ChildDeletion

beginWork过程中,对于存在要删除的子节点,我们会保存在当前父节点的deletions, 所以在删除阶段,我们需要根据当前节点的deletions属性进行对要删除的节点进行不同的处理。

// flags childDeletion
if ((flags & ChildDeletion) !== NoFlags) {
  const deletions = finishedWork.deletions;
  if (deletions !== null) {
    deletions.forEach((childToDelete) => {
      commitDeletion(childToDelete);
    });
  }
  finishedWork.flags &= ~ChildDeletion;
}

如果当前节点存在要删除的子节点的话,我们需要对每一个子节点进行commitDeletion的操作。

commitDeletion

commitDeletion函数的是对每一个要删除的子节点进行处理。它的主要功能有几点:

基于上面的2点分析,我们很容易就想到,commitDeletion肯定会执行DFS向下遍历,进行不同子节点的删除逻辑处理。

/**
 * rootHostNode 找到对应的DOM节点。
 * commitNestedComponent DFS遍历节点的进行卸载相关的逻辑
 * @param {FiberNode} childToDelete
 */
function commitDeletion(childToDelete: FiberNode) {
  let rootHostNode: FiberNode | null = null;
  // 递归子树
  commitNestedComponent(childToDelete, (unmountFiber) => {
    switch (unmountFiber.tag) {
      case HostComponent:
        if (rootHostNode === null) {
          rootHostNode = unmountFiber;
        }
        // TODO: 解绑ref
        return;
      case HostText:
        if (rootHostNode === null) {
          rootHostNode = unmountFiber;
        }
        return;
      case FunctionComponent:
        // TODO: useEffect unmount 解绑ref
        return;
      default:
        if (__DEV__) {
          console.warn("未处理的unmount类型", unmountFiber);
        }
        break;
    }
  });
  // 移除rootHostNode的DOM
  if (rootHostNode !== null) {
    const hostParent = getHostParent(childToDelete);
    if (hostParent !== null) {
      removeChild((rootHostNode as FiberNode).stateNode, hostParent);
    }
  }
  childToDelete.return = null;
  childToDelete.child = null;
}

commitNestedComponent

commitNestedComponent中主要是完成我们上面说的2点。

接受2个参数。1. 当前的fiberNode, 2. 递归到不同的子节点的同时,需要执行的回调函数执行不同的卸载流程。

function commitNestedComponent(
  root: FiberNode,
  onCommitUnmount: (fiber: FiberNode) => void
) {
  let node = root;
  while (true) {
    onCommitUnmount(node);
    if (node.child !== null) {
      // 向下遍历
      node.child.return = node;
      node = node.child;
      continue;
    }
    if (node === root) {
      // 终止条件
      return;
    }
    while (node.sibling === null) {
      if (node.return === null || node.return === root) {
        return;
      }
      // 向上归
      node = node.return;
    }
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

这里可能比较绕,我们下面通过几个例子总结一下,这个过程的主要流程。

总结

如果按照如下的结构,要删除外层div元素,会经历如下的流程

<div>
   <Child />
   <span>hcc</span>
   yx
</div>
function Child() {
  return <div>hello world</div>
}

下一节预告

下一节我们讲解通过useState改变状态后,如何更新节点以及函数组件hooks是如何保存数据的。

加载全部内容

相关教程
猜你喜欢
用户评论