likes
comments
collection
share

从0实现React18系列二

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

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

仓库地址

我们这一次主要写有关调和(reconciler)和ReactDom,React将调和单独的抽出一个包,暴露出入口,通过不同的宿主环境去调用不同的api。

React-Dom包

这个包主要是提供浏览器环境的一些dom操作。主要是提供2个文件hostConfig.ts 以及root.ts。 想想我们在React18中,是通过如下方式调用的。所以我们需要提供一个方法createRoot方法,返回要给包含render函数的对象。

import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(root).render(<App />)

createRoot

主要功能是2个,第一个是创建根fiberNode节点, 第二个创建更新(初始化主要是用于渲染),开始调度。

//createRoot.ts 文件
import {
  createContainer,
  updateContainer,
} from "../../react-reconciler/src/filerReconciler";

export function createRoot(container: Container) {
  const root = createContainer(container);

  return {
    render(element: ReactElementType) {
      updateContainer(element, root);
    },
  };
}

createRoot.js主要是调用的react-reconcilercreateContainer方法和updateContainer方法。我们之后看看这2个方法主要的作用

hostConfig.ts

主要是创建各种dom,已经dom的插入操作

export const createInstance = (type: string, props: any): Instance => {
  // TODO 处理props
  const element = document.createElement(type);
  return element;
};

export const appendInitialChild = (
  parent: Instance | Container,
  child: Instance
) => {
  parent.appendChild(child);
};

export const createTextInstance = (content: string) => {
  return document.createTextNode(content);
};

export const appendChildToContainer = appendInitialChild;

React-reconciler

createContainer() 函数

从上面我们可以知道,首先调用的createContainerupdateContainer,我们把它写到filerReconciler.tscreateContainer接受传入的dom元素。

/**
 * ReactDOM.createRoot()中调用
 * 1. 创建fiberRootNode 和 hostRootFiber。并建立联系
 * @param {Container} container
 */
export function createContainer(container: Container) {
  const hostRootFiber = new FiberNode(HostRoot, {}, null);
  const fiberRootNode = new FiberRootNode(container, hostRootFiber);
  hostRootFiber.updateQueue = createUpdateQueue();
  return fiberRootNode;
}

可以看到我们在这里主要就是2个事情

  1. 调用了2个方法去创建2个不同的fiberNode,一个是hostRootFiber,一个是fiberRootNode

  2. 创建一个更新队列,并将其赋值给hostRootFiber

    /**
     * 顶部节点
     */
    export class FiberRootNode {
      container: Container; // 不同环境的不同的节点 在浏览器环境 就是 root节点
      current: FiberNode;
      finishedWork: FiberNode | null; // 递归完成后的hostRootFiber
      constructor(container: Container, hostRootFiber: FiberNode) {
        this.container = container;
        this.current = hostRootFiber;
        hostRootFiber.stateNode = this;
        this.finishedWork = null;
      }
    }
    
    export class FiberNode {
      constructor(tag: WorkTag, pendingProps: Props, key: Key) {
        this.tag = tag;
        this.pendingProps = pendingProps;
        this.key = key;
        this.stateNode = null; // dom引用
        this.type = null; // 组件本身  FunctionComponent () => {}
    
        // 树状结构
        this.return = null; // 指向父fiberNode
        this.sibling = null; // 兄弟节点
        this.child = null; // 子节点
        this.index = 0; // 兄弟节点的索引
    
        this.ref = null;
    
        // 工作单元
        this.pendingProps = pendingProps; // 等待更新的属性
        this.memoizedProps = null; // 正在工作的属性
        this.memoizedState = null;
        this.updateQueue = null;
    
        this.alternate = null; // 双缓存树指向(workInProgress 和 current切换)
        this.flags = NoFlags; // 副作用标识
        this.subtreeFlags = NoFlags; // 子树中的副作用
      }
    }
    
    从0实现React18系列二

接下来,我们看看createUpdateQueue里面的执行逻辑。执行了一个函数,返回了一个对象。所以现在hostRootFiberupdateQueue指向了这个指针

/**
 * 初始化updateQueue
 * @returns {UpdateQueue<Action>}
 */
export const createUpdateQueue = <State>() => {
  return {
    shared: {
      pending: null,
    },
  } as UpdateQueue<State>;
};

我们从上面createRoot执行完后,返回了一个render函数,我们接下来看看render后的执行过程,是怎么渲染到页面的。

render() 调用

createRoot执行后,创建了一个rootFiberNode, 并返回了render调用,主要是执行了updateContainer用于去渲染初始化的工作。

updateContainer接受2个参数,第一个参数是传入的ReactElement(), 第二个参数是fiberRootNode

主要是做3件事情:

  1. 创建一个更新事件
  2. 把更新事件推进队列中
  3. 调用调度,开始更新
/**
 * ReactDOM.createRoot().render 中调用更新
 * 1. 创建update, 并将其推到enqueueUpdate中
 */
export function updateContainer(
  element: ReactElementType | null,
  root: FiberRootNode
) {
  const hostRootFiber = root.current;
  const update = createUpdate<ReactElementType | null>(element);
  enqueueUpdate(
    hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
    update
  );
  // 插入更新后,进入调度
  scheduleUpdateOnFiber(hostRootFiber);
  return element;
}
创建更新createUpdate

实际上就是创建一个对象,由于初始化的时候传入的是ReactElementType(), 所以返回的是App对应的ReactElement对象

/**
 * 创建更新
 * @param {Action<State>} action
 * @returns {Update<State>}
 */
export const createUpdate = (action) => {
  return {
    action,
  };
};
将更新推进队列enqueueUpdate

接受2个参数,第一个参数是我们创建一个更新队列的引用,第二个是新增的队列

/**
 * 更新update
 * @param {UpdateQueue<Action>} updateQueue
 * @param {Update<Action>} update
 */
export const enqueueUpdate = <State>(
  updateQueue: UpdateQueue<State>,
  update: Update<State>
) => {
  updateQueue.shared.pending = update;
};

执行到这一步骤,我们得到了更新队列,其实是一个ReactElement组件 及我们调用render传入的jsx对象。

开始调用scheduleUpdateOnFiber

接受FiberNode开始执行我们的渲染工作, 一开始渲染传入的是hostFiberNode 之后其他更新传递的是对应的fiberNode

export function scheduleUpdateOnFiber(fiber: FiberNode) {
  // todo 调度功能
  let root = markUpdateFromFiberToRoot(fiber);
  renderRoot(root);
}

wookLoop

执行完上面的操作后,接下来进入的调和阶段。开始我们要明白一个关键词:

workInProgress: 表示当前正在调和的fiber节点,之后简称wip

从上面我们可以看到调用了scheduleUpdateOnFiber方法,开始从根部渲染页面。scheduleUpdateOnFiber主要是执行了2个方法:

  1. markUpdateFromFiberToRoot: 由于我们更新的节点可能不是hostfiberNode, 这个方法就是不管传入的是那个节点,返回我们的根节点rootFiberNode

    // 从当前触发更新的fiber向上遍历到根节点fiber
    function markUpdateFromFiberToRoot(fiber: FiberNode) {
      let node = fiber;
      let parent = node.return;
      while (parent !== null) {
        node = parent;
        parent = node.return;
      }
      if (node.tag === HostRoot) {
        return node.stateNode;
      }
      return null;
    }
    
  2. renderRoot: 这里是我们wookLoop的入口,也是调和完成后,将生成的fiberNode树,赋值给finishedWork,并挂在根节点上,进入commit的入口。

    function renderRoot(root: FiberRootNode) {
      // 初始化,将workInProgress 指向第一个fiberNode
      prepareFreshStack(root);
      do {
        try {
          workLoop();
          break;
        } catch (e) {
          if (__DEV__) {
            console.warn("workLoop发生错误", e);
          }
          workInProgress = null;
        }
      } while (true);
    
      const finishedWork = root.current.alternate;
      root.finishedWork = finishedWork;
    
      // wip fiberNode树  树中的flags执行对应的操作
      commitRoot(root);
    }
    
    
    
    1. prepareFreshStack函数: 用于初始化当前节点的wip, 并创建alternate 的双缓存的建立。 由于我们开始的时候传入的hostFiberNode, 经过createWorkInProgress后,创建了一个新的fiberNode 并通过alternate相互指向。并赋值给wip

      let workInProgress: FiberNode | null = null;
      
      function prepareFreshStack(root: FiberRootNode) {
        workInProgress = createWorkInProgress(root.current, {});
      }
      
      export const createWorkInProgress = (
        current: FiberNode,
        pendingProps: Props
      ): FiberNode => {
        let wip = current.alternate;
      
        if (wip === null) {
          //mount
          wip = new FiberNode(current.tag, pendingProps, current.key);
          wip.stateNode = current.stateNode;
      
          wip.alternate = current;
          current.alternate = wip;
        } else {
          //update
          wip.pendingProps = pendingProps;
          // 清掉副作用(上一次更新遗留下来的)
          wip.flags = NoFlags;
          wip.subtreeFlags = NoFlags;
        }
      
        wip.type = current.type;
        wip.updateQueue = current.updateQueue;
        wip.child = current.child;
        wip.memoizedProps = current.memoizedProps;
        wip.memoizedState = current.memoizedState;
        return wip;
      };
      

      从0实现React18系列二

接下来我们来分析一下workLoop中到底是如何生成fiberNode树的。它本身函数执行很简单。就是不停的根据wip进行单个fiberNode的处理。 此时wip指向的hostRootFiber。开始执行performUnitOfWork进行递归操作,其中递:beginWork,归:completeWork。React通过DFS,首先找到对应的叶子节点。

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

function performUnitOfWork(fiber: FiberNode): void {
  const next = beginWork(fiber); // next 是fiber的子fiber 或者 是null
  // 工作完成,需要将pendingProps 复制给 已经渲染的props
  fiber.memoizedProps = fiber.pendingProps;

  if (next === null) {
    // 没有子fiber
    completeUnitOfWork(fiber);
  } else {
    workInProgress = next;
  }
}

beginWork开始

主要是向下进行遍历,创建不同的fiberNode。由于我们传入的是HostRoot,所以会走到updateHostRoot分支

/**
 * 递归中的递阶段
 * 比较 然后返回子fiberNode 或者null
 */
export const beginWork = (wip: FiberNode) => {
  switch (wip.tag) {
    case HostRoot:
      return updateHostRoot(wip);
    case HostComponent:
      return updateHostComponent(wip);
    case HostText:
      // 文本节点没有子节点,所以没有流程
      return null;
    default:
      if (__DEV__) {
        console.warn("beginWork未实现的类型");
      }
      break;
  }
  return null;
};
updateHostRoot

这个方法主要是2个部分:

  1. 根据我们之前创建的更新队列获取到最新的值
  2. 创建子fiber
/**
	processUpdateQueue: 是根据不同的类型(函数和其他)生成memoizedState
*/
function updateHostRoot(wip: FiberNode) {
  const baseState = wip.memoizedState;
  const updateQueue = wip.updateQueue as UpdateQueue<ElementType>;
  // 这里获取之前的更新队列
  const pending = updateQueue.shared.pending;
  updateQueue.shared.pending = null;
  const { memoizedState } = processUpdateQueue(baseState, pending); // 最新状态
  wip.memoizedState = memoizedState; // 其实就是传入的element

  const nextChildren = wip.memoizedState; // 就是我们传入的ReactElement 对象
  reconcileChildren(wip, nextChildren);
  return wip.child;
}
reconcileChildren

调和子节点, 根据是否生成过,分别调用不同的方法。通过上面我们知道传入的hostFiber, 此时是存在alternate属性的,所以会走到reconcilerChildFibers分支。

根据当前传入的returnFiberhostFiberNode以及currentFiber为null,newChild为ReactElementType。我们可以判断接下来会走到reconcileSingleElement的执行。其中placeSingleChild是打标记使用的,我们暂时先不研究。

/**
	wip: 当前正在执行的父fiberNode
	children: 即将要生成的子fiberNode
*/
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
  const current = wip.alternate;

  if (current !== null) {
    // update
    wip.child = reconcilerChildFibers(wip, current?.child, children);
  } else {
    // mount
    wip.child = mountChildFibers(wip, null, children);
  }
}

function reconcilerChildFibers(
    returnFiber: FiberNode,
    currentFiber: FiberNode | null,
    newChild?: ReactElementType | string | number
  ) {
    // 判断当前fiber的类型
    if (typeof newChild === "object" && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(returnFiber, currentFiber, newChild)
          );
        default:
          if (__DEV__) {
            console.warn("未实现的reconcile类型", newChild);
          }
          break;
      }
    }
    // Todo 多节点的情况 ul > li * 3

    // HostText
    if (typeof newChild === "string" || typeof newChild === "number") {
      return placeSingleChild(
        reconcileSingleTextNode(returnFiber, currentFiber, newChild)
      );
    }

    if (__DEV__) {
      console.warn("未实现的reconcile类型", newChild);
    }
    return null;
  };
}
reconcileSingleElement

从名字我们可以看出是通过ReactElement 创建单一的fiberNode。通过reconcileSingleElement我们就可以得出了一个新的子节点,然后通过return指向父fiber。此时的fiberNode树如下图。

  /**
   * 根据reactElement对象创建fiber并返回
   */
  function reconcileSingleElement(
    returnFiber: FiberNode,
    _currentFiber: FiberNode | null,
    element: ReactElementType
  ) {
    const fiber = createFiberFromElement(element);
    fiber.return = returnFiber;
    return fiber;
  }

export function createFiberFromElement(element: ReactElementType): FiberNode {
  const { type, key, props } = element;
  let fiberTag: WorkTag = FunctionComponent;

  if (typeof type === "string") {
    // <div/>  type : 'div'
    fiberTag = HostComponent;
  } else if (typeof type !== "function" && __DEV__) {
    console.log("未定义的type类型", element);
  }
  const fiber = new FiberNode(fiberTag, props, key);
  fiber.type = type;
  return fiber;
}

从0实现React18系列二

调用完后,此时回到了reconcileChildren函数的这一句代码执行,指定wip的child指向。此时函数执行完毕。

// 省略无关代码
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
    wip.child = reconcilerChildFibers(wip, current?.child, children);
}

从0实现React18系列二

执行完后返回updateHostRoot函数调用reconcileChildren的地方。然后返回wip的child。

  function updateHostRoot(wip) {
      const baseState = wip.memoizedState;
      reconcileChildren(wip, nextChildren);
      return wip.child;
  }

执行完updateHostRoot函数后,返回调用它的beginWork中。beginWork也同样返回了当前wip的child节点。

export const beginWork = (wip: FiberNode) => {
  switch (wip.tag) {
    case HostRoot:
      return updateHostRoot(wip);
  }
}

执行完后,我们最后又回到了最开始调用beginWork的地方。进行接下来的操作,主要是将已经渲染过的属性赋值。然后将wip赋值给下一个刚刚生成的子节点。以便于开始下一次的递归中调用。

  function performUnitOfWork(fiber) {
      const next = beginWork(fiber); // next 是fiber的子fiber 或者 是null
      // 工作完成,需要将pendingProps 复制给 已经渲染的props
      fiber.memoizedProps = fiber.pendingProps;
      if (next === null) {
          // 没有子fiber
          completeUnitOfWork(fiber);
      }
      else {
          workInProgress = next;
      }
  }

由于workInProgress不等于null, 说明还有子节点。继续进行workLoop调用。又开始了新的一轮。直到我们到达了叶子节点。

  function workLoop() {
      while (workInProgress !== null) {
          performUnitOfWork(workInProgress);
      }
  }
例子

例如,如下例子,当遍历到hcc文本节点后,由于我们节点是没有调和流程的。所以执行到beginWork后,返回了一个null。正式结束了递归调用中的“递" 过程。此时的fiberNode树如下图所示。

const jsx = <div><span>hcc</span></div>
const root = document.querySelector('#root')
ReactDOM.createRoot(root).render(jsx)

从0实现React18系列二

completeWork开始

从上面的beginWork操作后,此时我们wip在文本节点hcc的节点位置.

completeUnitOfWork

接下来执行performUnitOfWork中的completeUnitOfWork的逻辑部分,我们看看completeUnitOfWork的逻辑部分。 我们传入的最底部的叶子节点。首先会对当前节点进行completeWork的方法调用。

function completeUnitOfWork(fiber) {
  let node = fiber;
  do {
      completeWork(node);
      const sibling = node.sibling;
      if (sibling !== null) {
          workInProgress = sibling;
          return;
      }
      node = node.return;
      workInProgress = node;
  } while (node !== null);
}
completeWork

首次我们会接受到一个最底部的子fiberNode,由于是第一次mount,所以当前的fiber下不会存在alternate属性的,所以会走到构建Dom的流程。

/**
 * 递归中的归
 */
export const completeWork = (wip: FiberNode) => {
  const newProps = wip.pendingProps;
  const current = wip.alternate;

  switch (wip.tag) {
    case HostComponent:
      if (current !== null && wip.stateNode) {
        //update
      } else {
        // 1. 构建DOM
        const instance = createInstance(wip.type, newProps);
        // 2. 将DOM插入到DOM树中
        appendAllChildren(instance, wip);
        wip.stateNode = instance;
      }
      bubbleProperties(wip);
      return null;
    case HostText:
      if (current !== null && wip.stateNode) {
        //update
      } else {
        // 1. 构建DOM
        const instance = createTextInstance(newProps.content);
        // 2. 将DOM插入到DOM树中
        wip.stateNode = instance;
      }
      bubbleProperties(wip);
      return null;
    case HostRoot:
      bubbleProperties(wip);
      return null;
    default:
      if (__DEV__) {
        console.warn("未实现的completeWork");
      }
      break;
  }
};

// 根据逻辑判断,走到下面的逻辑判断,传入了文本
// 1. 构建DOM 
const instance = createTextInstance(newProps.content); 
// 2. 将DOM插入到DOM树中 
wip.stateNode = instance;

经过completeWork后,我们给当前的wip添加了stateNode属性,用于指向生成的Dom节点。 执行完completeWork后,继续返回到completeUnitOfWork中,查找sibling节点,目前我们demo中没有,所以会向上找到当前节点的return指向。继续执行completeWork工作,此时的结构变成了如下图:

从0实现React18系列二

由于我们wip目前是HostComponent, 所以走到了如下的completeWork的逻辑。这里根据type创建不同的Dom元素,和之前一样,绑定到对应的stateNode属性上。我们可以看到除了这2个,还执行了一个函数appendAllChildren。我们去看看这个函数的作用是什么

// 1. 构建DOM
const instance = createInstance(wip.type);
// 2. 将DOM插入到DOM树中
appendAllChildren(instance, wip);
wip.stateNode = instance;
appendAllChildren

接受2个参数,第一个是刚刚通过wip的type生成的对应的dom, 另外一个是wip本身。 它的作用就是把我们上一步产生的dom节点,插入到刚刚产生的父dom节点上,形成一个局部的小dom树。

/**
 * 在parent的节点下,插入wip
 * @param {FiberNode} parent
 * @param {FiberNode} wip
 */
function appendAllChildren(parent: Container, wip: FiberNode) {
  let node = wip.child;

  while (node !== null) {
    if (node?.tag === HostComponent || node?.tag === HostText) {
      appendInitialChild(parent, node?.stateNode);
    } else if (node.child !== null) {
      node.child.return = node;
      // 继续向下查找
      node = node.child;
      continue;
    }

    if (node === wip) {
      return;
    }
    while (node.sibling === null) {
      if (node.return === null || node.return === wip) {
        return;
      }
      // 向上找
      node = node?.return;
    }
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

我们用这个图来说明一下流程

从0实现React18系列二

  1. 当前的”归“到了div对应的fiberNode。我们获取到node是第一个子元素的span, 执行appendInitialChild方法,把对应的stateNode的dom节点插入parent中。
  2. 接下来执行由于node.sibling不为空,所以会将node 复制给第二个span。然后继续执行appendInitialChild。以此执行到第三个span节点。
  3. 第三个span节点对应的sibling为空,所以开始向上查找到node.return === wip结束函数调用。
  4. 此时三个span产生的dom,都已经插入到parent(div dom)中。
回到completeUnitOfWork

经过上述操作后,我们继续回到completeUnitOfWork的调用,继续向上归并。到上述例子的div节点。直到我们遍历到hostFiberNode, 它是没有return属性的,所以返回null,结束了completeUnitOfWork的执行。回到了最开始的workLoop。此时的workInProgress等于null, 结束循环。

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

回到renderRoot

执行完workLoop, 就回到了renderRoot的部分。此时我们已经得到了完整的fiberNode树,以及相应的dom元素。此时对应的结果如下图:

从0实现React18系列二

那么生成的fiberNode树是如何渲染的界面上的,我们下一章的commit章节介绍,如何打标签和渲染

转载自:https://juejin.cn/post/7182148488665399353
评论
请登录