react16.8
react18
为什么react18.2不需要手动引入?import React from 'react';
react的jsx会由babel进行编译,下面对<p id="title">hello</p>进行编译。
react16.8
/*#__PURE__*/React.createElement("p", {
id: "title"
}, "hello");
react18.2
import { jsx as _jsx } from "react/jsx-runtime";
/*#__PURE__*/_jsx("p", {
id: "title",
children: "hello"
});
可以看到,react16.8中,编译后的结果是React.createElement,并没有自动引入React,而react18.2中,编译后的结果是自动引入并使用了了_jsx方法,所以是不需要再手动引入React的。
react中的jsx函数
function ReactElement(type, key, ref, props) {
//虚拟dom结构
return {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref,
props,
}
}
export function jsxDEV(type, config, maybeKey) {
let propName //属性名
const props = {} //属性对象
let key = null //每个虚拟dom可以有一个可选的key属性,用来区分一个父节点的不同子节点
let ref = null //ref属性,可以获取真实dom节点
if (typeof maybeKey !== 'undefined') {
key = maybeKey
}
if (hasValidRef(config)) {
ref = config.ref
}
//将config中的属性复制到props中
//排除原形链上的属性、排除RESERVED_PROPS中的属性
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName]
}
}
return ReactElement(type, key, ref, props)
}
可以看到,其实就是返回了ReactElement对象,这个对象的属性包$$typeof,type,key,ref,props,也就是vdom(虚拟dom)。
react中的fiber
/**
*
* @param {*} tag fiber类型,函数组件 类组件 原生组件 根元素
* @param {*} pendingProps 新属性,等待处理或生效的属性
* @param {*} key 唯一标识
*/
export function FiberNode(tag, pendingProps, key) {
this.tag = tag
this.key = key
this.type = null //fiber类型,来自于虚拟dom节点的type span div p
this.stateNode = null //此fiber对应的真实dom节点
this.return = null //父fiber
this.child = null //指向第一个子fiber
this.sibling = null //指向弟弟
this.pendingProps = pendingProps //等待生效的属性
this.memoizedProps = null //已经生效的属性
//每个fiber还会有自己的状态,每一种fiber状态存的类型是不一样的
//类组件对应的fiber,存的是类的实例的状态,HostRoot存的就是要渲染的元素
this.memoizedState = null //组件的状态
this.updateQueue = null //更新队列
//副作用的标识,表示要针对此fiber节点进行何种操作
this.flags = NoFlags
//子节点对应的副作用使用标识
this.subtreeFlags = NoFlags
//替身、轮替
this.alternate = null
this.index = 0
this.deletions = null
this.lanes = NoLanes
}
初次挂载流程
import { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
const Element = () => {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('useEffect1')
return () => {
console.log('destroy useEffect1')
}
})
return (
<p
key={1}
onClick={() => {
setCount(count + 1)
}}
style={{ fontSize: count === 0 ? '14px' : '20px' }}
>
{count}
</p>
)
}
const root = createRoot(document.getElementById('root'))
root.render(<Element></Element>)
上面的代码经过babel编译后,得到下面的代码
import { useEffect, useState } from "/node_modules/.vite/deps/react.js?v=59e60701";
import { createRoot } from "/src/react-dom/client.js";
const Element = () => {
_s();
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect1");
return () => {
console.log("destroy useEffect1");
};
});
return /* @__PURE__ */ jsxDEV(
"p",
{
onClick: () => {
setCount(count + 1);
},
style: { fontSize: count === 0 ? "14px" : "20px" },
children: count
},
1,
false,
{
fileName: "/Users/liuxuanlong/liuxl/code/personal/mini-react/src/main.jsx",
lineNumber: 15,
columnNumber: 5
},
this
);
};
_s(Element, "/xL7qdScToREtqzbt5GZ1kHtYjQ=");
_c = Element;
const root = createRoot(document.getElementById("root"));
root.render(/* @__PURE__ */ jsxDEV(Element, {}, void 0, false, {
fileName: "/Users/liuxuanlong/liuxl/code/personal/mini-react/src/main.jsx",
lineNumber: 27,
columnNumber: 13
}, this));
初次挂载的代码调用流程如下:
createRoot(document.getElementById('root'))
export function createRoot(container) {
// div#root
const root = createContainer(container)
// 事件监听
listenToAllSupportedEvents(container)
return new ReactDOMRoot(root)
}
createRoot干了两件事:
-
创建fiberRoot 和 rootFiber,建立联系,初始化rootFiber的updateQueue。 构建出的结构如下:
rootFiber的updateQueue指的是这个fiber上的更新队列。
-
处理事件监听的逻辑(此处先不做说明)
root.render(<Element></Element>)
render函数定义在ReactDOMRoot原型上。
//render
ReactDOMRoot.prototype.render = function (children) {
const root = this._internalRoot
updateContainer(children, root)
}
updateContainer
export function updateContainer(element, container) {
//获取当前的根fiber
const current = container.current
//请求一个更新车道
const lane = requestUpdateLane(current)
//创建更新
const update = createUpdate(lane)
//要更新的虚拟dom
update.payload = { element }
//把此更新对象添加到current这个根fiber的updateQueue队列中,返回根节点
const root = enqueueUpdate(current, update, lane)
//在fiber上调度更新
scheduleUpdateOnFiber(root, current, lane)
}
- updateContainer中构建了rootFiber的更新队列,
updateQueue.shared.pending指向最后1个更新(lastUpdate),lastUpdate.next指向第一个更新(在这里是它本身)。
2. 调用scheduleUpdateOnFiber
scheduleUpdateOnFiber
export function scheduleUpdateOnFiber(root, fiber, lane) {
markRootUpdated(root, lane)
ensureRootIsScheduled(root)
}
调用ensureRootIsScheduled
ensureRootIsScheduled
...
if (newCallbackPriority === SyncLane) {
// 先把performSyncWorkOnRoot添加到同步队列中
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))
// 再把flushSyncCallbacks放入微任务
queueMicrotask(flushSyncCallbacks)
newCallbackNode = null
} else {
//如果不是同步,需要调度一个新的任务
let schedulerPriorityLevel
...
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
)
}
scheduleSyncCallback & scheduleCallback
根据调度算法选择 同步(scheduleSyncCallback) 的方式执行还是 异步(scheduleCallback) 的方式执行。
同步执行performSyncWorkOnRoot.bind(null, root)
异步执行performConcurrentWorkOnRoot.bind(null, root)
performConcurrentWorkOnRoot & performConcurrentWorkOnRoot
renderRootConcurrent & renderRootSync
两种法都是按照调度算法来执行renderRootConcurrent 或者 renderRootSync。
两个函数内部都处理两种逻辑。 首先构建workInProgress根节点。构建完成的结构如下图
然后进行 workLoop,进行beginWork、complateWork递归的构建fiber树的过程。 在构建过程中会标记副作用
commitRoot
commitRoot中,根据标记的副作用,生成真实的dom,挂载到div #root中,最终完成dom的挂载。
总结
真实dom生成流程
jsx -> vdom -> fiber节点 -> 真实dom
整体的函数调用流程如下
整个源码中有两大循环:
- 任务调度循环,控制所有任务的调度。(更新优先级、批处理、并发渲染等基于此来进行)
- fiber构造循环,控制fiber树的构建,整个过程是一个树的深度优先遍历。(diff算法、 hooks等基于此来进行)
这里引用此文章中的话来解释
结合上文的宏观概览图(展示核心包之间的调用关系), 可以将 react 运行的主干逻辑进行概括:
输入: 将每一次更新(如: 新增, 删除, 修改节点之后)视为一次
更新需求(目的是要更新DOM节点).注册调度任务:
react-reconciler收到更新需求之后, 并不会立即构造fiber树, 而是去调度中心scheduler注册一个新任务task, 即把更新需求转换成一个task.执行调度任务(输出): 调度中心
scheduler通过任务调度循环来执行task(task的执行过程又回到了react-reconciler包中).
fiber构造循环是task的实现环节之一, 循环完成之后会构造出最新的 fiber 树.commitRoot是task的实现环节之二, 把最新的 fiber 树最终渲染到页面上,task完成.主干逻辑就是
输入到输出这一条链路, 为了更好的性能(如批量更新,可中断渲染等功能),react在输入到输出的链路上做了很多优化策略, 比如本文讲述的任务调度循环和fiber构造循环相互配合就可以实现可中断渲染.
后续的源码解析,请关注本专栏的后续文章
本文参考和引用文章列表:(如有侵权,请告知删除)
github.com/7kms/react-…