likes
comments
collection
share

解密React 18的"起手式":createRoot和render的魔法 🚀

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

React 18引入了一些新的特性和API,其中包括了createRootrender函数的改变。

ReactDOM.createRoot(root).render(<App/>)

曾听一个人说过,他的理解:「起手式 写完就不用动了」🤪

让我们深入探究一下这些变化,以便更好地理解为什么现在可以通过以下方式直接渲染出一个真正的DOM页面

在介绍之前我们先来拓展一下React,ReactDOM,ReactReconciler之间的关系

React,ReactDOM,ReactReconciler之间的关系

我们先了解一下这三者之间的关系

想一下如果没有React的情况下我们是怎么去让dom进行渲染的

document.appendChild document.write document.removeChild 这些原生操作?

亦或者使用Jquery

其实本质都是

调用宿主API
显示真实DOM

而我们在React中却没有直接调用宿主API,我们一般都是调用React自己实现的方法触发的DOM更新,如setState,或者useState的返回值等

所有前端架构的共性都是状态驱动:

描述UI的方法
协调UI模块
调用宿主PI
显示真实DOM

描述UI的方法就是说JSX语法或者其他模版语法

协调UI模块说的就是框架内部实现的协调更新的模块如setState

调用宿主API其实就是调用真实的DOM API等

React框架中,导出的React就是处理描述UI的方法这一步骤的,我们看不到的React的内部实现协调用就在协调模块,ReactDOM对应的就是调用对应宿主的API

React

<App/>这种形式的标签元素转为ReactElement对象,其实就是在我们编写jsx或者tsx的时候,编译器会把 jsx 的写法变为 js对象

编译前:

<div a={'1'}>hello <span>react</span></div>

编译后:

import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
/*#__PURE__*/_jsxs("div", {
  a: '1',
  children: ["hello ", /*#__PURE__*/_jsx("span", {
    children: "react"
  })]
});

ReactDOM

调用真实宿主API的模块 appendChild 等。具体实现跟宿主有关

export function appendChild(
  parentInstance: Instance,
  child: Instance | TextInstance,
): void {
  parentInstance.appendChild(child);
}

源码地址可查看

ReactReconciler

ReactReconciler 就是用来触发协调DOM什么时候更新的模块,我们在React中通过babel等编译拿到ReactElement对象,再通过ReactReconciler 转为可实现协程概念的FiberNode,所有的协调相关以及调度更新相关都会在这里。

ReactElementFiberNode 之间的关系

ReactElement用于描述UI的结构,它是静态的,可以帮助开发者更容易理解和维护组件树的结构。FiberNode则用于管理动态执行的逻辑,它负责实际的渲染和更新操作。

简单点说,打个比方:

比如建造一个房子

  • ReactElement 就像是房子的蓝图或设计图。它描述了房子的结构,包括房间的布局、颜色、窗户的大小等。但蓝图本身不能建造房子,只是一个计划。
  • FiberNode 就像是房子的建筑队伍。他们根据蓝图(ReactElement)来实际建造房子,他们负责处理材料、工人的调度、施工进度等。他们能够在建造过程中灵活地应对变化,例如如果突然下雨,他们可以停工并在天晴时继续工作。

好了接下来我们来开始介绍ReactDOM.createRoot(root).render(<App/>)都做了什么

首先我们来想象一下 createRoot 函数做了什么

createRoot 函数接收了一个 container函数,这个函数就是我们React的根挂载dom

在我们没有看过任何React源码的时候,最起码我们应该想象到

function createRoot(container) {
  return {
   render(reactElement) {
    // 根据 root 和 reactElement 进行逻辑处理
   }
  };
}

其实我们在在看React源码时也会发现,总体框架大差不差

function createRoot(container) {
  ...
  const root = createContainer(container);
  ...
  return new ReactDOMRoot(root);
}

具体可查看源码

不难想象 我们是直接 return 了一个 renderReact中把这个render放在了 ReactDOMRoot 中。

render 的 参数 <App/>

传入的App组件将会被babel等解析

function App(){
    return <div a={'1'}>hello <span>react</span></div> 
}

<App/>

编译后:

import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
function App() {
  return /*#__PURE__*/_jsxs("div", {
    a: '1',
    children: ["hello ", /*#__PURE__*/_jsx("span", {
      children: "react"
    })]
  });
}
/*#__PURE__*/_jsx(App, {});

相当于 render 实际接收到的参数应该是一个 调用了jsx方法值

function jsxDEV(type, config, maybeKey, source, self){
    ...
    return ReactElement(
      type,
      key,
      ref,
      self,
      source,
      ReactCurrentOwner.current,
      props,
    );
}

源码位置可查看

render 实际接收到的应该是一个 ReactElement 对象:

解密React 18的"起手式":createRoot和render的魔法 🚀

render 中做了什么

render 中就是根据ReactElement对象,创建FiberNode,然后再根据FiberNode调用ReactDOM方法,创建真实DOM然后挂载到createRoot接收参数 container的过程。其实就是ReactReconciler去做的一个过程。

总结: “起手式”其实就是把jsx转为ReactElement对象,再把ReactElement对象转为可描述工作单元的FiberNode,再通过FiberNode创建真实DOM的过程。