likes
comments
collection
share

ReactDOM.createRoot(...)之后都经历了些什么?

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

在日常的开发中,我们不禁会想,为什么别人写出来的代码这么优雅,而我写出来的代码就一行代码百行报错,并且整体代码像一堆屎山,报错的时候也难以找出报错原因,那么在接下来的文章中让我们来学习React源码来对其进一步学习吧。

什么是JSX

什么是 JSX,在 React官方文档 中是这样定义的: 它是一个 JavaScript 的语法扩展,在 React 中配合使用 JSX 可以很好地描述 UI 应该呈现出他应该有的交互形式。

它像模板语言,但他具有 JavaScript 的全部功能。实际上,JSX 仅仅是 React.createElement(component, props, ...children) 函数的语法糖。

那么,JSX 的语法是怎么样的 JavaScript 中生效的呢,让我们看下面的章节。

JSX本质

React 中,JSX 会被 Babel 编译为普通的 JavaScript 函数,在 Babel 中我们输入以下代码:

<div className="moment">hello</div>;

通过 Babel 转译最终会生成以下代码:

React.createElement(
  "div",
  {
    className: "moment",
  },
  "hello"
);

我们再看看更复杂的代码:

<div className="moment">
  <div key="1" className="test">
    1111
  </div>
  <div key="2" className="niu">
    2222
  </div>
</div>;

最终会被编译成这样:

React.createElement(
  "div",
  {
    className: "moment",
  },
  React.createElement(
    "div",
    {
      key: "1",
      className: "test",
    },
    "1111"
  ),
  React.createElement(
    "div",
    {
      key: "2",
      className: "niu",
    },
    "2222"
  )
);

好了,现在我们了解了在 React 中编写的代码 JSX 最终会被编译成上面的样子,那么接下来我们该正式进入主题了。

React应用包结构

要想阅读 React 源码,那么有必要先了解一下 React 中的包结构,在 @18.1.0 版本中,与web相关的核心包共有4个,其中主要有以下包:

react: React 基础包,只提供定义 React 组件的必要函数,在编写react应用的代码时, 大部分都是调用此包的 api,例如 useStatememouseEffect 等。

react-dom: React 渲染器之一,是 ReactWeb 平台连接的桥梁,将 react-reconciler 中的运行结果输出到 web 界面上,在编写 React 应用的代码时,大多数场景下,能用到此包的就是一个入口函数ReactDOM.render(<App/>, document.getElementById('root')), 其余使用的 api, 基本是react包提供的。

react-reconciler: 该包的主要功能有以下4个方面:

  • 输入: 暴露api函数(如: scheduleUpdateOnFiber), 供给其他包(如react包)调用;
  • 注册调度任务: 与调度中心(scheduler包)交互,注册调度任务task,等待任务回调;
  • 执行任务回调: 在内存中构造出fiber树, 同时与与渲染器(react-dom)交互, 在内存中创建出与fiber对应的DOM节点;
  • 输出: 与渲染器(react-dom)交互, 渲染DOM节点;

scheduler: 时间管理大师,核心任务就是执行回调,该回调函数由 react-reconciler 提供,通过控制回调函数的执行时机,来达到任务分片的目的,实现可中断渲染。

在接下来的代码解析当中,为了代码的简洁性,会删除一下无关紧要的代码,完整代码可以可以查看文章底部的链接获取。

createElement

在上面的内容中,JSX 会被 Babel 会被编译成一个 React.createElement 函数,该函数在 React 包中如下:

// react\packages\react\src\ReactElement.js

function createElement(type, config, children) {
  // 属性名称,用于后面的 for 循环
  let propName;
  // 存储 React Element 中的普通元素属性,但是不包含 key ref self source
  const props = {};
  // 对 dom 存入的key 值
  let key = null;
  // 通过 ref 获取到的 dom 实例
  let ref = null;
  let self = null;
  let source = null;

  // 如果 config 不为空
  if (config != null) {
    if (hasValidRef(config)) {
      // 将 config.ref 属性提取到 ref 变量中
      ref = config.ref;
    }
    // 会将 key 转换为字符串
    if (hasValidKey(config)) {
      key = "" + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 通过遍历 config 中的属性并添加到 props
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  /**
   * 处理子元素,将第三个及之后的参数挂载到 props.children 属性中
   * 如果子元素是多个 props.children 是数组对象
   * 如果子元素是一个 props.children 是对象
   */
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  /**
   * 如果当前处理的是组件,看组件身上是否有 defaultProps 属性
   * 这个属性存储的是 props 对象中的默认值
   */
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
  return ReactElement(
    type, // HTML 标签
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  );
}

ReactElement 函数中的主要作用是返回一个对象,并标记为 React Element,具体代码如下:

const ReactElement = function (type, key, ref, self, source, owner, props) {
  const element = {
    // 标记这是个 React Element
    $$typeof: REACT_ELEMENT_TYPE,

    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  return element;
};

在我们的项目中有这样的组件,其代码如下:

const App = () => {
  return (
    <div className="moment">
      <div key="1" className="test">
        1111
      </div>
      <div key="2" className="niu">
        2222
      </div>
    </div>
  );
};

console.log(App());

然后再通过查看控制台,有以下输出:

ReactDOM.createRoot(...)之后都经历了些什么?

在上面的图片中可以看出,ReactElement 返回的东西正是我们前面中讲到的,而 _source 是 babel-preset-react注入的调试信息,可以提供更有用的错误信息,能具体到代码的所在文件及行数。

到这里,也许你对 JSX 会有一个清除的概念了。

createRoot

在我们使用 create-react-app 创建的项目,整个项目的入口有这样的一段代码:

const root = ReactDOM.createRoot(document.getElementById("root"));

函数 createRoot 的定义存放与 react 包中的 react-test\src\react\packages\react-dom\src\client\ReactDOMRoot.js,在该方法中,初始化了一系列变量并调用 createContainer 方法:

createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );

在上面的函数调用中,传入的参数如下图所示:

ReactDOM.createRoot(...)之后都经历了些什么?

在上面传入的第二个参数 ConcurrentRoot 意为当前 React 应用中的模式为 ConcurrentRoot

createContainer 方法中是直接返回 createFiberRoot 函数的调用:

// react\packages\react-reconciler\src\ReactFiberReconciler.old.js

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  identifierPrefix: string,
  onRecoverableError: (error: mixed) => void,
  transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
  const hydrate = false;
  const initialChildren = null;
  return createFiberRoot(
    containerInfo,
    tag,
    hydrate,
    initialChildren,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
}

其实在上面这个方法也没干啥事,也只是把参数传进来又去调用 createFiberRoot 函数,真的是层层套娃,其实阅读源码难也就难在这里,好了,接下来我们看看 createFiberRoot 又干了些啥事,具体代码实现如下图所示:

// react\packages\react-reconciler\src\ReactFiberRoot.old.js

export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  initialChildren: ReactNodeList,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  identifierPrefix: string,
  onRecoverableError: null | ((error: mixed) => void),
  transitionCallbacks: null | TransitionTracingCallbacks,
): FiberRoot {
  /** 创建 FiberRoot */
  const root: FiberRoot = (new FiberRootNode(
    containerInfo,
    tag,
    hydrate,
    identifierPrefix,
    onRecoverableError,
  ): any);

  console.log(root);

   /** 设置服务端渲染回调 */
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  /** 设置过渡回调 */
  if (enableTransitionTracing) {
    root.transitionCallbacks = transitionCallbacks;
  }

  /** 创建 HostRootFiber */
  const uninitializedFiber = createHostRootFiber(
    tag,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );

  /** 将 HostRootFiber 挂载到 FiberRoot 的 current 属性上 */
  root.current = uninitializedFiber;

  /** 将 HostRootFiber 的 stateNode 设置为 FiberRoot */
  uninitializedFiber.stateNode = root;

  /** 设置 HostRootFiber 的 memoizedState */
  if (enableCache) {
    const initialCache = createCache();
    console.log(initialCache);
    retainCache(initialCache);

    root.pooledCache = initialCache;
    retainCache(initialCache);
    const initialState: RootState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: initialCache,
      transitions: null,
      pendingSuspenseBoundaries: null,
    };
    uninitializedFiber.memoizedState = initialState;
  } else {
    const initialState: RootState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: (null: any), // not enabled yet
      transitions: null,
      pendingSuspenseBoundaries: null,
    };
    uninitializedFiber.memoizedState = initialState;
  }

  // 初始化updateQueue,对于RootFiber,queue.share.pending上面存储着element
  initializeUpdateQueue(uninitializedFiber);

  return root;
}

在上面的代码中,通过 new FiberRootNode 返回一个 fiberRoot 实例,在这个构造函数中定义了很多很好玩的东西,具体可以自己去看,这里就不详细解答了。

在这个时候,我们打印一下 new FiberRootNode 返回的实例是一个什么样的值,详情请看下图:

ReactDOM.createRoot(...)之后都经历了些什么?

现在我们只需关心这两个玩意,containerInfo 是容器的意思,也是整个项目的入口,为真实 DOM,那另外一个是什么呢,我们等下再看,代码我们继续往下看:

  // 创建 HostRootFiber
const uninitializedFiber = createHostRootFiber(
  tag,
  isStrictMode,
  concurrentUpdatesByDefaultOverride,
);

这里又调用了 createHostRootFiber 函数,该函数的主要作用有以下几个方面:

  • 设置 React Fiber 的工作模式,分别有:Concurrent模式、严格模式和 createRootStrictEffectsByDefault 模式;
  • 创建 Fiber;

紧接着,我们再看下面的代码:


// 将 HostRootFiber 挂载到 FiberRoot 的 current 属性上
root.current = uninitializedFiber;

// 将 HostRootFiber 的 stateNode 设置为 FiberRoot
uninitializedFiber.stateNode = root;

在上面的这个代码就像一个相互引用,我们再次通过上面的打印查看,发现有这样的输出:

ReactDOM.createRoot(...)之后都经历了些什么?

它们的关系图请看下图所示:

ReactDOM.createRoot(...)之后都经历了些什么?

在控制台中还有这样一个信息:

ReactDOM.createRoot(...)之后都经历了些什么?

child 指向的是子元素,而 reture 指向的父元素,两者相互指向,而 FiberNode 为当前的 Fiber 树的根节点,它没有父节点,它的 return 值为空也就讲得通了。

那么问题来了,fiberRootrootiber 的区别又是什么呢?

在我们开发的项目中,你可以理解为每一个组件都有一个由 rootFiber 构成的 fiber 树,但整个应用的根节点只有一个,那么就是 fiberRoot,fiberRoot可以指向不同的 rootFiber 以渲染不同的页面,具体请看下面的图片:

ReactDOM.createRoot(...)之后都经历了些什么?

通过上面的图片我们可以得知,fiberRoot 它是整个应用的根节点, 绑定在container._reactRootContainer, 也就是绑定在真实DOM节点的 _reactRootContainer 属性上。

createFiberRoot 函数的最后调用了 initializeUpdateQueue 用于初始化 rootFiber.updateQueue:

initializeUpdateQueue(uninitializedFiber);

要想了解该函数的作用,那么我们先来了解一下该函数是怎么定义的:

// react\packages\react-reconciler\src\ReactFiberClassUpdateQueue.old.js

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
      interleaved: null,
      lanes: NoLanes,
    },
    effects: null,
  };
  fiber.updateQueue = queue;
}

该函数的主要作用是用于初始化 UpdateQueue,这里主要有什么作用我们在后面的文章会讲到。

到这里,这个函数也执行完毕了,并且最后返回整个 fiberRoot,但是这里还会有一个知识点还没有讲到,就是 双缓存

代码又回到 createRoot 函数这里,在这里有调用了 markContainerAsRoot 函数:

// react\packages\react-dom\src\client\ReactDOMRoot.js

// 将 fiberNode 挂载到 container 对象上,也就是跟目录
markContainerAsRoot(root.current, container);

我们继续来了解一下该函数的内部构成:

export function markContainerAsRoot(hostRoot: Fiber, node: Container): void {
  node['__reactContainer$' + randomKey] = hostRoot;
}

这个函数的主要作用是给 containerInfo 也就是 div#root 生成一个随机的 key,最后通过 container.nodeType 来判断节点的类型:

  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;

因为此时的 container.nodeTypediv#root ,所以它的值为 1,而 COMMENT_NODE 的值为 8,所以最终返回的是 container 的本身,通过控制台打印,它完完整整的生成了一个 DOM 树:

ReactDOM.createRoot(...)之后都经历了些什么?

而这正和我们在 react 项目中编写的代码基本一致:

const App = () => {
  function foo() {
    console.log(1111111111);
  }
  return (
    <div className="moment">
      <div key="1" className="test" onClick={foo}>
        1111
      </div>
      <div key="2" test="111" className="niu">
        2222
      </div>
    </div>
  );
};

但是发现少了一样东西,我们定义的 onclick 事件去哪了,先别慌,往下看代码最后又继续调用 listenToAllSupportedEvents 函数,该函数的主要功能是会给 div#root 节点注册浏览器支持的所有原生事件,比如onclick等,而我们定义的事件会被关联到真实的 DOM__reactProps${key} 上,如下图所示:

ReactDOM.createRoot(...)之后都经历了些什么?

该函数最后返回 fiberRoot。到这里整个 fiberRoot 的创建也就结束了。

双缓存Fiber树

React 中最低同时会存在两颗 fiber 树,当前屏幕上显示内容对应的 fiber 树称为 current fiber 树,正在构建的 fiber 树称为 workInProgress Fiber 树,通过控制台可以看到如下输出

ReactDOM.createRoot(...)之后都经历了些什么?

React 应用的根节点通过使 current 指针在不同 Fiber 树的 rootFiber 间切换来完成 current Fiber 树指向的切换。

即当 workInProgress Fiber树 构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向 workInProgress Fiber 树,此时 workInProgress Fiber 树就变为 current Fiber 树。

每次状态更新都会产生新的 workInProgress Fiber 树,通过 currentworkInProgress 的替换,完成 DOM 更新。

而最终的 fiber 树如下图所示:

ReactDOM.createRoot(...)之后都经历了些什么?

好了到这里本篇文章也就讲解完了,如需学习后续内容敬请关注。

参考文章

总结

没有总结,请各位大佬自行总结。

欲知后事如何,请看下回分解.

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