持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
这回,咱们整个大的,实现一个自己的👉 mini-react,这一节我们实现几种常用的组件渲染。
每一个功能实现我都会作为一个单独的 commit 进行提交。本节代码在 v0.0.1 分支上。 文中难免有错误或遗漏的地方,还请帮忙指正,谢谢~ 跪求您帮我点个🌟,谢谢您了~
高能预警,本文全程没有任何尿点,概念性的东西写出来就太多了,网上很多。如果有什么不明白的可以留言~
创建 React 中的一些Tag标识
创建src/ReactWorkTags.ts 文件,这些变量表示着组件的类型,这里我们暂时实现一些比较常见的类型。
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
Fiber 的含义
fiber 在 React 是非常重要的概念,我们在解释 fiber 时需要从三个层面去解释它
- 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。
- 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
- 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。
PS:以上引用自 kasong 大佬。👉原文地址
写创建 Fiber 节点的函数
创建 src/ReactFiber.ts(fiber 节点是根据 jsx 对象进行创建的)
import { Placement } from "./ReactFiberFlags"
import { FunctionComponent, HostComponent } from "./ReactWorkTags"
import { isFn, isStr } from "./utils"
// vnode 就是当前节点的 jsx 对象
// returnFiber 就是父 fiber
export function createFiber(vnode, returnFiber) {
const fiber: any = {
// 组件类型
type: vnode.type,
key: vnode.key,
props: vnode.props,
stateNode: null,
// 第一个子 fiber
child: null,
// 下一个兄弟 fiber
sibling: null,
// 父 fiber
return: returnFiber,
// 标记(这里先默认标记为 Placement)
flags: Placement,
alternate: null,
// 要删除的节点,null 或 []
deletions: null,
// 当前 fiber 所在的层级的 index
index: null,
}
const { type } = vnode
if (isStr(type)) {
fiber.tag = HostComponent
} else if (isFn(type)) {
// 函数组件以及类组件都会被判断为 function,但暂不做处理
fiber.tag = FunctionComponent
}
return fiber
}
原生组件的渲染(将 jsx 处理成 fiber )
src/ReactFiberReconciler.ts
import { createFiber, Fiber } from "./ReactFiber";
import { isArray, isStringOrNumber, updateNode } from "./utils";
// 处理原生组件
export function updateHostComponent(workInProgress: Fiber) {
if (!workInProgress.stateNode) {
const element = document.createElement(workInProgress.type)
workInProgress.stateNode = element
// 处理 props
updateNode(element, workInProgress.props)
}
// 往下处理子节点,children 是 jsx 对象
reconcileChildren(workInProgress, workInProgress.props.children)
}
export function updateFunctionComponent(workInProgress: Fiber) { }
export function updateClassComponent(workInProgress: Fiber) { }
export function updateHostTextComponent(workInProgress: Fiber) { }
export function updateFragmentComponent(workInProgress: Fiber) { }
function reconcileChildren(workInProgress: Fiber, children) {
if (isStringOrNumber(children)) {
return
}
// 这里先将子节点都当作数组来处理
const newChildren: any[] = isArray(children) ? children : [children]
// 用于保存上个 fiber 节点
let previousNewFiber: Fiber = null
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i]
if (newChild === null) {
// 会遇到 null 的节点,直接忽略即可
continue
}
const newFiber = createFiber(newChild, workInProgress)
if (previousNewFiber === null) {
// 第一个子节点直接保存到 workInProgress 上
workInProgress.child = newFiber
} else {
// 后续都保存到上一个节点的 sibling 上
previousNewFiber.sibling = newFiber
}
// 更新
previousNewFiber = newFiber
}
}
利用浏览器空闲时间来处理 fiber
src/ReactFiberWorkLoop.ts
import { Fiber } from "./ReactFiber";
import { Placement } from "./ReactFiberFlags";
import { updateClassComponent, updateFragmentComponent, updateFunctionComponent, updateHostComponent, updateHostTextComponent } from "./ReactFiberReconciler";
import { ClassComponent, Fragment, FunctionComponent, HostComponent, HostText } from "./ReactWorkTags";
// 当前正在处理的节点
let workInProgress: Fiber | null = null;
// 根节点
let workInProgressRoot: Fiber | null = null;
// 初次渲染和更新
export function scheduleUpdateOnFiber(fiber) {
workInProgress = fiber;
workInProgressRoot = fiber
}
function performUnitOfWork() {
const { tag } = workInProgress
// todo 1. 更新当前组件
switch (tag) {
case HostComponent:
updateHostComponent(workInProgress);
break;
case FunctionComponent:
updateFunctionComponent(workInProgress);
break;
case ClassComponent:
updateClassComponent(workInProgress);
break;
case Fragment:
updateFragmentComponent(workInProgress);
break;
case HostText:
updateHostTextComponent(workInProgress);
break;
default:
break;
}
// todo 2. 更新子组件(深度优先遍历)
const child = workInProgress.child;
if (child) {
// 处理子节点
workInProgress = child;
return
}
while(workInProgress) {
if (workInProgress.sibling) {
workInProgress = workInProgress.sibling;
return
}
// 没有兄弟节点就往上归一层,尝试处理上一层的兄弟节点
workInProgress = workInProgress.return;
}
// 没有兄弟节点,也没有上一层,就结束
workInProgress = null
}
function workLoop(IdleDeadLine: IdleDeadline) {
while(workInProgress && IdleDeadLine.timeRemaining() > 0) {
// 处理成 fiber,挂载 stateNode props 等操作
performUnitOfWork()
}
}
// 利用 requestIdleCallback 来计算处理
requestIdleCallback(workLoop)
处理完 fiber 以后创建 dom 并更新到真实 dom 上
src/ReactFiberWorkLoop.ts
function workLoop(IdleDeadLine: IdleDeadline) {
while(workInProgress && IdleDeadLine.timeRemaining() > 0) {
// 处理成 fiber,挂载 stateNode props 等操作
performUnitOfWork()
}
if (!workInProgress && workInProgressRoot) {
// workInProgress === null 且 workInProgressRoot 存在说明所有的 fiber 都处理完了
// 需要更新到页面上
commitRoot()
}
}
// 提交
function commitRoot() {
commitWorker(workInProgressRoot)
// 提交完以后需要清空 workInProgressRoot 防止重复提交
workInProgressRoot = null
}
// 深度优先遍历
function commitWorker(workInProgress: Fiber) {
if (!workInProgress) return
// 1. 提交自己
const parentNode = workInProgress.return.stateNode
const { flags, stateNode } = workInProgress
if (flags & Placement && stateNode) {
parentNode.appendChild(stateNode)
}
// 2. 提交子节点
commitWorker(workInProgress.child)
// 3. 提交兄弟节点
commitWorker(workInProgress.sibling)
}
实现函数组件的渲染
src/ReactFiberReconciler.ts
export function updateFunctionComponent(workInProgress: Fiber) {
const { type, props } = workInProgress
// type 就是函数,把 props 传过去就能拿到 jsx 对象了
const children = type(props)
// 向下 reconciler
reconcileChildren(workInProgress, children)
}
函数组件是没有 stateNode 的,所以在渲染时 appendChild 会抛错,那这里需要找到真正的 dom 就需要往上找了
src/ReactFiberWorkLoop.ts
function commitWorker(workInProgress: Fiber) {
if (!workInProgress) return
// 1. 提交自己
// 需要找到实际渲染的父节点
let parentNode = getParentNode(workInProgress.return)
const { flags, stateNode } = workInProgress
if (flags & Placement && stateNode) {
parentNode.appendChild(stateNode)
}
// 2. 提交子节点
commitWorker(workInProgress.child)
// 3. 提交兄弟节点
commitWorker(workInProgress.sibling)
}
// 函数组件等 是没有stateNode,所以要往上找真正的 dom 节点
function getParentNode(fiber: Fiber) {
let stateNode = fiber.stateNode
while(!stateNode) {
fiber = fiber.return
stateNode = fiber.stateNode
}
return stateNode
}
实现类组件的渲染
因为 typeof ClassComponent 和 FunctionComponent 都是 "function",所以需要处理一下创建对应的 fiber 节点时打的 tag 来区分这两者。
ClassComponent extends 自 React.Component,所以我们的区分点就在 Component 的原型上加个标志 isReactComponent,接着在创建 fiber 时根据这个标志来区分是 FunctionComponent 还是 ClassComponent
创建 React.Component 并添加 Class 组件的标志src/Component.ts
export default functino Component(props) {
this.props = props
}
Component.prototype.isReactComponent = true
根据原型上的标志区分 fiber 的类型src/ReactFiber.ts
export function createFiber(vnode, returnFiber) {
// ......
if (isStr(type)) {
fiber.tag = HostComponent
} else if (isFn(type)) {
// 函数组件以及类组件都会被判断为 function
// 根据判断原型上是否有 isReactComponent 来判断出是 class 还是 function 的类型(isReactComponent 来自于 React.Component )
fiber.tag = (type as Function).prototype.isReactComponent ? ClassComponent : FunctionComponent
}
}
实现 ClassComponent 的 reconcilersrc/ReactFiberReconciler.ts
export function updateClassComponent(workInProgress: Fiber) {
const { type, props } = workInProgress
const instance = new type(props)
// class 的 children 来自于 render 函数
const children = instance.render()
reconcileChildren(workInProgress, children)
}
实现文本组件的渲染
文本组件虽然创建了,但是可以通过打印的 fiber 来看,它上面的属性全部都是 null 或 undefined,所以在 performUnitOfWork 中不能走到对应的类型处理,需要给文本组件添加上类型src/ReactFiber.ts
export function createFiber(vnode, returnFiber) {
// ......
if (isStr(type)) {
fiber.tag = HostComponent
} else if (isFn(type)) {
// 函数组件以及类组件都会被判断为 function
// 根据判断原型上是否有 isReactComponent 来判断出是 class 还是 function 的类型(isReactComponent 来自于 React.Component )
fiber.tag = (type as Function).prototype.isReactComponent ? ClassComponent : FunctionComponent
} else if (isUndefined(type)) {
fiber.tag = HostText
fiber.props = {
// 直接将文本放到 children 上
children: vnode
}
}
}
这样就可以进入到 updateHostTextComponent
接着我们处理一下文本节点的 fiber,这部分直接创建一个文本,并将其直接放到fiber的 stateNode 属性上即可
src/ReactFiberReconciler.ts
export function updateHostTextComponent(workInProgress: Fiber) {
const { props } = workInProgress
// 创建一个文本节点放到 stateNode 上
workInProgress.stateNode = document.createTextNode(props.children)
}
这样文本节点就可以被渲染出来了
Fragment组件渲染
Fragment 组件的 type 是 Symbol(react.fragment),这里我们直接进入到判断type的 else 分支即可。按照上面的套路,Fragment 也是加上对应的 tag,然后在对应的 reconciler 中进行处理src/ReactFiber.ts
export function createFiber(vnode, returnFiber) {
// ......
if (isStr(type)) {
fiber.tag = HostComponent
} else if (isFn(type)) {
fiber.tag = (type as Function).prototype.isReactComponent ? ClassComponent : FunctionComponent
} else if (isUndefined(type)) {
fiber.tag = HostText
fiber.props = {
children: vnode
}
} else {
// 直接打上 tag 即可
fiber.tag = Fragment
}
}
同样也是在对应的 reconciler 中处理一下,这里直接调用 reconcileChildren 传入workInProgress 和 children 即可src/ReactFiberReconciler.ts
export function updateFragmentComponent(workInProgress: Fiber) {
const { props } = workInProgress
reconcileChildren(workInProgress, props.children)
}
至此,我们实现了 React 中的 Fiber 创建,以及 ReactDOM.createRoot 和常用的几种组件渲染。 在这一节当中,我们使用的是浏览器提供的 requestIdleCallback,下一节我们自己实现一个来替代它。
👉 github 地址 在这里,本节代码在 v0.0.1分支。希望能得到你的 🌟~