React-从源码开始(一)

作为一名前端小白,会想起第一次debug React组件时的手足无措,在惊叹这个框架的奇妙之余也更加觊觎其中的道理。

万物伊始——react准备工作

每个react的web应用想要展示到页面上的第一步都需要执行渲染函数ReactDOM.createRoot(document.getElementById('root')).render(<App />)
,而它都做了些什么呢?

首先我们看到的是ReactDOM调用的createRoot方法,它在react内部调用createRoot方法返回一个React容器对象

export function createRoot(
  container: Container,
  options?: RootOptions,
): RootType {
  return new ReactDOMRoot(container, options);
}
复制代码

参数中的container,也就是我们通过document.getElementById('root')获取的dom对象,options这个参数,它主要用在服务端渲染流程中,所以现在为空。

再看,这个函数中返回的是ReactDOMRoot构造函数生成的对象,而ReactDOMRoot又是干什么呢?

function ReactDOMRoot(container: Container, options: void | RootOptions) {
  this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
复制代码

它为我们创建的React容器对象的_internalRoot属性通过createRootImpl函数的返回值赋值,他又是做什么的呢。

function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  ...
  // 创建FiberRoot对象
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  // 标记一下FiberRoot对象
  markContainerAsRoot(root.current, container);
  // 获取整个应用的挂在的那个元素
  const rootContainerElement =
    container.nodeType === COMMENT_NODE ? container.parentNode : container;
  // 注册事件代理
  listenToAllSupportedEvents(rootContainerElement);
  ...
  return root;
}
复制代码

首先我们看到这个函数的参数由之前的两个变成了三个,中间的tag参数是用来标志react的工作模式。自从React16之后, react引入了Fiber,工作模式也由之前的一种变成两种,随之改变的是在react应用挂载时执行的渲染函数也由一种变成了三种。

ReactDOM.render(<App />document.getElementById('root'))
ReactDOM.createBlockingRoot(document.getElementById('root')).render(<App />)
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
复制代码

第一种还是原始默认,其工作方式也是同步方式,而剩下两种则是使用新版的并行工作方式。
而它们之间就是通过刚才提到的tag参数进行区分的,而createBlockingRootcreateRoot之间借用官方的解释:

对于较旧的代码库,concurrent 模式可能步子迈的太大。这就是我们在实验版本中提供“ blocking 模式”的原因。你可以通过使用 createBlockingRoot 代替 createRoot 尝试一下。

export type RootTag = 0 | 1 | 2;

export const LegacyRoot = 0;
export const BlockingRoot = 1;
export const ConcurrentRoot = 2;
复制代码

进入这个函数,最主要的就是做了两件事

  1. 生成FiberRoot对象。
  2. 注册事件代理。

我们先看第一步,通过调用createContainer函数生成root并最终返回这个root,这也就是_internalRoot的值。

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
  // 创建并返回FiberRoot
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
复制代码

createContainer中又调用了createFiberRoot

export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
  // 实例化一个FiberRoot对象
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  ...
  // 创建一个当前组件树的根节点HostRootFiber
  const uninitializedFiber = createHostRootFiber(tag);
  // 将这个HostRootFiber对象作为FiberRoot对象current属性的引用
  root.current = uninitializedFiber;
  // 这个FiberRoot对象的stateNode属性指向容器对象
  uninitializedFiber.stateNode = root;
  // 初始化更新队列
  initializeUpdateQueue(uninitializedFiber);
  return root;
}
复制代码

首先,调用FiberRootNode函数实例化root对象,这个函数会返回一个RootNode对象,赋值给root并最终也返回它。

而之后调用createHostRootFiber创建HostRootFiber对象,赋值给uninitializedFiber
这里的createHostRootFiber函数最终返回的是一个Fiber对象。而传给createHostRootFiber的参数是当前工作模式的标志。

此时将uninitializedFiber赋值到root.current属性上,作为当前的组件树的根节点。

接下来初始化更新队列,调用initializeUpdateQueue函数

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
    },
    effects: null,
  };
  // 为fiber节点也就是当前组件树的根节点的updateQueue属性赋初始值
  fiber.updateQueue = queue;
}
复制代码

最后返回root,返回并结束我们在createRootImpl中的第一步生成FiberRoot对象。

接下来执行listenToAllSupportedEvents,参数为依然是通过document.getElementById('root')获取的dom对象

const listeningMarker =
  '_reactListening' +
  Math.random()
    .toString(36)
    .slice(2);

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  // 当(rootContainerElement: any)[listeningMarker]不为true时向下执行
  if ((rootContainerElement: any)[listeningMarker]) {
    // 优化:在相同的portal容器或者根节点只迭代一次
    // 一旦移除这个标记,我们也许还可以删除一些用于懒加载的簿记图。
    return;
  }
  // 设置(rootContainerElement: any)[listeningMarker]为true,确保不再重复执行。
  (rootContainerElement: any)[listeningMarker] = true;
  // 为所有的原生事件添加监听
  allNativeEvents.forEach(domEventName => {
    // 判断当前原生事件是否在不需要代理监听的事件列表里,如果不在则执行
    if (!nonDelegatedEvents.has(domEventName)) {
      listenToNativeEvent(
        domEventName,
        false,
        ((rootContainerElement: any): Element),
        null,
      );
    }
    listenToNativeEvent(
      domEventName,
      true,
      ((rootContainerElement: any): Element),
      null,
    );
  });
}

export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  rootContainerElement: EventTarget,
  targetElement: Element | null,
  eventSystemFlags?: EventSystemFlags = 0,
): void {
  let target = rootContainerElement;
  // selectionchange需要附加到文档上,否则它将无法捕获仅直接在文档上触发的传入事件。
  if (
    domEventName === 'selectionchange' &&
    (rootContainerElement: any).nodeType !== DOCUMENT_NODE
  ) {
    target = (rootContainerElement: any).ownerDocument;
  }
  // If the event can be delegated (or is capture phase), we can
  // register it to the root container. Otherwise, we should
  // register the event to the target element and mark it as
  // a non-delegated event.
  if (
    targetElement !== null &&
    !isCapturePhaseListener &&
    nonDelegatedEvents.has(domEventName)
  ) {
    // 对于所有未委派的事件,除了滚动之外,还将其事件侦听器附加到其事件触发的各个元
    // 素上。 这意味着我们可以跳过此步骤,因为以前已经添加了事件侦听器。 但是,我们对滚
    // 动事件进行了特殊处理,因为现实是任何元素都可以滚动。
    if (domEventName !== 'scroll') {
      return;
    }
    eventSystemFlags |= IS_NON_DELEGATED;
    target = targetElement;
  }
  // 创建或者获取事件代理集合
  const listenerSet = getEventListenerSet(target);
  // 创建代理事件名称并标记这个事件是捕获还是冒泡
  const listenerSetKey = getListenerSetKey(
    domEventName,
    isCapturePhaseListener,
  );
  // 如果是新增事件或者更新事件
  if (!listenerSet.has(listenerSetKey)) {
    if (isCapturePhaseListener) {
      eventSystemFlags |= IS_CAPTURE_PHASE;
    }
    // 添加绑定事件
    addTrappedEventListener(
      target,
      domEventName,
      eventSystemFlags,
      isCapturePhaseListener,
    );
    // 将新增或更新事件添加到事件集合中
    listenerSet.add(listenerSetKey);
  }
}
复制代码

至此,我们完成了万里长征的第一步,react在渲染更新前的准备工作

  1. 初始化根节点
  2. 初始化事件代理

万事开头难,然后中间难,最后会更难。每天挪一小步,光就会出现。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享