React 18引入了一些新的特性和API,其中包括了createRoot
和render
函数的改变。
js
ReactDOM.createRoot(root).render(<App/>)
曾听一个人说过,他的理解:「起手式 写完就不用动了
」🤪
让我们深入探究一下这些变化,以便更好地理解为什么现在可以通过以下方式直接渲染出一个真正的DOM页面
在介绍之前我们先来拓展一下React,ReactDOM,ReactReconciler之间的关系
React,ReactDOM,ReactReconciler之间的关系
我们先了解一下这三者之间的关系
想一下如果没有React的情况下我们是怎么去让dom进行渲染的
document.appendChild
document.write
document.removeChild
这些原生操作?
亦或者使用Jquery
?
其实本质都是
而我们在React
中却没有直接调用宿主API,我们一般都是调用React
自己实现的方法触发的DOM更新,如setState
,或者useState
的返回值等
所有前端架构的共性都是状态驱动:
描述UI的方法就是说JSX语法或者其他模版语法
协调UI模块说的就是框架内部实现的协调更新的模块如setState
等
调用宿主API其实就是调用真实的DOM API等
在React
框架中,导出的React
就是处理描述UI的方法这一步骤的,我们看不到的React的内部实现协调用就在协调模块,ReactDOM
对应的就是调用对应宿主的API
React
把<App/>
这种形式的标签元素转为ReactElement
对象,其实就是在我们编写jsx或者tsx的时候,编译器会把 jsx 的写法变为 js对象
编译前:
jsx
<div a={'1'}>hello <span>react</span></div>
编译后:
js
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
等。具体实现跟宿主有关
js
export function appendChild(
parentInstance: Instance,
child: Instance | TextInstance,
): void {
parentInstance.appendChild(child);
}
ReactReconciler
ReactReconciler
就是用来触发协调DOM什么时候更新的模块,我们在React
中通过babel
等编译拿到ReactElement
对象,再通过ReactReconciler
转为可实现协程概念的FiberNode
,所有的协调相关以及调度更新相关都会在这里。
ReactElement
与 FiberNode
之间的关系
ReactElement
用于描述UI的结构,它是静态的,可以帮助开发者更容易理解和维护组件树的结构。FiberNode
则用于管理动态执行的逻辑,它负责实际的渲染和更新操作。
简单点说,打个比方:
比如建造一个房子
ReactElement
就像是房子的蓝图或设计图。它描述了房子的结构,包括房间的布局、颜色、窗户的大小等。但蓝图本身不能建造房子,只是一个计划。FiberNode
就像是房子的建筑队伍。他们根据蓝图(ReactElement)来实际建造房子,他们负责处理材料、工人的调度、施工进度等。他们能够在建造过程中灵活地应对变化,例如如果突然下雨,他们可以停工并在天晴时继续工作。
好了接下来我们来开始介绍ReactDOM.createRoot(root).render(<App/>)
都做了什么
首先我们来想象一下 createRoot
函数做了什么
createRoot
函数接收了一个 container
函数,这个函数就是我们React
的根挂载dom
在我们没有看过任何React源码的时候,最起码我们应该想象到
js
function createRoot(container) {
return {
render(reactElement) {
// 根据 root 和 reactElement 进行逻辑处理
}
};
}
其实我们在在看React源码时也会发现,总体框架大差不差
js
function createRoot(container) {
...
const root = createContainer(container);
...
return new ReactDOMRoot(root);
}
不难想象 我们是直接 return
了一个 render
, React
中把这个render
放在了 ReactDOMRoot
中。
render
的 参数 <App/>
传入的App组件将会被babel等解析
js
function App(){
return <div a={'1'}>hello <span>react</span></div>
}
<App/>
编译后:
js
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
方法值
js
function jsxDEV(type, config, maybeKey, source, self){
...
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
故 render
实际接收到的应该是一个 ReactElement
对象:
render
中做了什么
render 中就是根据ReactElement
对象,创建FiberNode
,然后再根据FiberNode
调用ReactDOM
方法,创建真实DOM然后挂载到createRoot
接收参数 container
的过程。其实就是ReactReconciler
去做的一个过程。
总结: "起手式"其实就是把jsx
转为ReactElement
对象,再把ReactElement
对象转为可描述工作单元的FiberNode
,再通过FiberNode
创建真实DOM的过程。