欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 手游 > 【从0实现React18】 (六) 完成commit提交流程并初步实现react-dom包,完成首屏渲染测试

【从0实现React18】 (六) 完成commit提交流程并初步实现react-dom包,完成首屏渲染测试

2024/10/24 14:16:51 来源:https://blog.csdn.net/m0_74989367/article/details/139970740  浏览:    关键词:【从0实现React18】 (六) 完成commit提交流程并初步实现react-dom包,完成首屏渲染测试

前面,我们提到 React 更新流程有四个阶段:

  • 触发更新(Update Trigger)
  • 调度阶段(Schedule Phase)
  • 协调阶段(Reconciliation Phase)
  • 提交阶段(Commit Phase)

之前我们已经实现了协调阶段(Reconciliation Phase)的 beginWorkcompleteWork 函数,接下来我们会实现提交阶段

提交阶段的主要任务是将更新同步到实际的 DOM 中,执行 DOM 操作,例如创建、更新或删除 DOM 元素,反映组件树的最新状态

Commit 阶段的三个子阶段

  • Before Mutation (布局阶段):

主要用于执行 DOM 操作之前的准备工作,包括类似 getSnapshotBeforeUpdate 生命周期函数的处理。在这个阶段会保存当前的布局信息,以便在后续的 DOM 操作中能够进行比较和优化。

  • Mutation ( DOM 操作阶段):

执行实际 DOM 操作的阶段,包括创建、更新或删除 DOM 元素等。使用深度优先遍历的方式,逐个处理 Fiber 树中的节点,根据协调阶段生成的更新计划,执行相应的 DOM 操作。

  • Layout (布局阶段):

用于处理布局相关的任务,进行一些布局的优化,比如批量更新布局信息,减少浏览器的重排(reflow)次数,提高性能。其目标是最小化浏览器对 DOM 的重新计算布局,从而提高渲染性能。

  1. 实现 commitWork

要执行的任务:

  • fiber树的切换
  • 执行Placement对应操作

需要注意的问题,考虑如下JSX,如果span含有flag,该如何找到他:

<App><div><span>只因</span></div>
</App>

首先,在 react-reconciler/src/workLoop.tsrenderRoot 函数中,执行 commitRoot 函数。

  • commitRoot 是开始提交阶段的入口函数,调用 commitWork 函数进行实际的 DOM 操作;
  • commitWork 函数是提交阶段的核心,它会判断根节点是否存在上述 3 个阶段需要执行的操作,并执行实际的 DOM 操作,并完成 Fiber 树的切换。

我们先只实现 Mutation 阶段的功能,目前已支持的 DOM 操作有:Placement | Update | ChildDeletion,判断根节点的 flagssubtreeFlags 中是否包含这三个操作,如果有,则调用 commitMutationEffects 函数执行实际的 DOM 操作。

需要注意的是,由于 current 是与视图中真实 UI 对应的 Fiber 树,而 workInProgress 是触发更新后正在 Reconciler 中计算的 Fiber 树,因此在 DOM 操作执行完之后,需要将 current 指向 workInProgress,完成 Fiber 树的切换。

 // packages/react-reconciler/src/workLoop.ts
/*** 整体 reconciler 的工作循环*/
let workInProgress: FiberNode | null = null/** reconciler最终执行的方法 */
function renderRoot(root: FiberRootNode) {// ...// 提交阶段的入口函数commitRoot(root)
}/** 进入commit阶段 */
function commitRoot(root: FiberRootNode) {const finishedWork = root.finishedWorkif (finishedWork === null) {return}if (__DEV__) {console.warn('commit阶段开始', finishedWork)}//重置root.finishedWork = null// 判断3个子阶段需要执行的操作//  使用 MutationMask 判断是否存在副作用const subtreeHasEffect =(finishedWork.subtreeFlags & MutationMask) !== NoFlags // subtreeconst rootHasEffects = (finishedWork.flags & MutationMask) !== NoFlags // rootif (subtreeHasEffect || rootHasEffects) {// 1.beforeMutation// 2.mutationCommitMutationEffects(finishedWork) // 有副作用则进入Mutation阶段root.current = finishedWork // finishedWork 是新生成的 workInProgress 树// 3.layout} else {root.current = finishedWork // 完成 Fiber 树的切换}
}
  1. 实现 Mutaion

接下来我们来实现 Mutation 阶段执行 DOM 操作的具体实现,新建 packages/react-reconciler/src/commitWork.ts 文件,定义 commitMutationEffects 函数。

let nextEffect: FiberNode | null = null
export const CommitMutationEffects = (finishedWork: FiberNode) => {nextEffect = finishedWorkwhile (nextEffect !== null) {// 向下遍历const child: FiberNode | null = nextEffect.childif ((nextEffect.subtreeFlags & MutationMask) !== NoFlags &&child !== null) {//  子节点有可能存在Mutation对应的操作nextEffect = child} else {up: while (nextEffect !== null) {// 找到第一个没有 subtreeFlags 的节点, 即最下的 flag 处commitMutationEffectsOnFiber(nextEffect)// 向上遍历 DFSconst sibling: FiberNode | null = nextEffect.siblingif (sibling !== null) {nextEffect = siblingbreak up}nextEffect = nextEffect.return}}}
}

commitMutationEffects 函数负责深度优先遍历 Fiber 树,递归地向下寻找子节点是否存在 Mutation 阶段需要执行的 flags,如果遍历到某个节点,其所有子节点都不存在 flags(即 subtreeFlags == NoFlags),则停止向下,调用 commitMutationEffectsOnFiber 处理该节点的 flags,并且开始遍历其兄弟节点和父节点,即继续向上遍历。

/** 对 Fiber节点的 Mutation 操作 */
const commitMutationEffectsOnFiber = (finishedWork: FiberNode) => {const flags = finishedWork.flagsif ((flags & Placement) !== NoFlags) {// 存在 Placement 操作commitPlacement(finishedWork)finishedWork.flags &= ~Placement //移除Placement} else if ((flags & Update) !== NoFlags) {}
}

commitMutationEffectsOnFiber 会根据每个节点的 flags 和更新计划中的信息执行相应的 DOM 操作。

const commitPlacement = (finishedWork: FiberNode) => {if (__DEV__) {console.warn('执行Placement操作', finishedWork)}// 找到parentDomconst hostParent = getHostParent(finishedWork)appendPlacementNodeIntoContainer(finishedWork, hostParent)// 找到finishedWork对应的Dom 然后 append 到 hostParent
}

Placement 为例:如果 Fiber 节点的标志中包含 Placement,表示需要在 DOM 中插入新元素,此时就需要取到该 Fiber 节点对应的 DOM,并将其插入对应的父 DOM 节点中。

至此,我们就完成了 React 更新流程中的提交阶段(Commit Phase),实现了 DOM 树更新


React-Dom

React 是一个跨平台的库,可以用于构建 Web 应用、移动应用(React Native)等。而 react-dom 就是 React 在 Web 环境中的渲染实现,用于将 React 组件渲染到实际的 DOM 上,并提供了一些与 DOM 操作相关的功能。

之前我们在 react-reconciler/src/hostConfig.ts 中模拟实现了一些生成、插入 DOM 元素的函数,现在就在 react-dom真正实现它

  1. 实现 react-dom 包

先创建 packages/react-dom 文件夹,并初始化:

cd packages
mkdir react-dom
cd react-dompnpm init

修改package.json文件:

{"name": "react-dom","version": "1.0.0","description": "","module": "index.ts","dependencies": {"shared": "workspace:*","react-reconciler": "workspace:*"},"peerDependencies": {"react": "workspace:*"},"keywords": [],"author": "","license": "ISC"
}

新建 packages/react-dom/scr/hostConfig.ts 文件,将之前的 hostConfig.ts 文件复制过来并删除:

/*** 描述数组环境方法*/export type Container = Element
export type Instance = Element/** 创建Dom实例 */
export const createInstance = (type: string, props: any): Instance => {// TODO 处理propsconst element = document.createElement(type)return element
}/** Dom 的插入 */
export const appendInitialChild = (parent: Instance | Container,child: Instance
) => {parent.append(child)
}/** 创建Text 节点 */
export const createTextInstance = (content: string) => {return document.createTextNode(content)
}/** 插入节点 */
export const appendChildToContainer = appendInitialChild

接着实现 packages/react-dom/scr/root.ts,先来实现 ReactDOM.createRoot().render() 方法,我们之前讲过,这个函数过程中会调用两个 API:

  • createContainer 函数: 用于创建一个新的容器(container),该容器包含了 React 应用的根节点以及与之相关的一些配置信息。
  • updateContainer 函数: 用于更新已经存在的容器中的内容,将新的 React 元素(element)渲染到容器中,并更新整个应用的状态。

这两个 API 在 react-reconciler 包里面已经实现了,直接调用即可。

import {createContainer,updateContainer,
} from 'react-reconciler/src/fiberReconciler'
import { Container } from './hostConfig'
import { ReactElementType } from 'shared/ReactTypes'// 实现 ReactDom.createRoot(root).render(<App/>)
export function createRoot(container: Container) {const root = createContainer(container)return {render(element: ReactElementType) {updateContainer(element, root)},}
}

现在我们已经实现了 React 首屏渲染的更新流程,即:

通过 ReactDOM.createRoot(root).render(<App />) 方法,创建 React 应用的根节点,将一个 Placement 加入到更新队列中,并触发了首屏渲染的更新流程:在对 Fiber 树进行深度优先遍历(DFS)的过程中,比较新旧节点,生成更新计划,执行 DOM 操作,最终将 <App /> 渲染到根节点上。

目前我们还只实现了首屏渲染触发更新,还有很多触发更新的方式,如类组件的 this.setState()、函数组件的 useState useEffect ,将在后面实现。

  1. 实现 ReactDom 打包流程

接着来实现 react-dom 包的打包流程,具体过程参考 第 2 节,需要注意两点:

  • 需要安装一个包来处理 hostConfig 的导入路径:pnpm i -D -w @rollup/plugin-alias
  • ReactDOM = Reconciler + hostConfig,不要将 react 包打包进 react-dom 里,否则会出现数据共享冲突;

react-dom.config.js 的具体配置如下:

import { getPackageJSON, resolvePkgPath, getBaseRollupPlugins } from './utils'
import generatePackageJson from 'rollup-plugin-generate-package-json'
import alias from '@rollup/plugin-alias'const { name, module, peerDependencies } = getPackageJSON('react-dom')
// react-dom 包的路径
const pkgPath = resolvePkgPath(name)
// react-dom 包的产物路径
const pkgDistPath = resolvePkgPath(name, true)export default [// react-dom{input: `${pkgPath}/${module}`,output: [{file: `${pkgDistPath}/index.js`,name: 'ReactDOM',format: 'umd',},{file: `${pkgDistPath}/client.js`,name: 'client',format: 'umd',},],external: [...Object.keys(peerDependencies)],plugins: [...getBaseRollupPlugins(),// webpack resolve aliasalias({entries: {hostConfig: `${pkgPath}/src/hostConfig.ts`,},}),generatePackageJson({inputFolder: pkgPath,outputFolder: pkgDistPath,baseContents: ({ name, description, version }) => ({name,description,version,peerDependencies: {react: version,},main: 'index.js',}),}),],},
]

再将 tsconfig.json 中的 hostConfig 指向 react-dom 包中的路径;

// tsconfig.json
{// ..."paths": {"hostConfig": ["./react-dom/src/hostConfig.ts"]}
}

最后,为了在执行 npm run build-dev 时能同时将 reactreact-dom 都打包,我们新建一个 dev.config.js 文件,将 react.config.jsreact-dom.config.js 统一导出。

// scripts/rollup/dev.config.js
import reactDomConfig from './react-dom.config';
import reactConfig from './react.config';export default [...reactConfig, ...reactDomConfig];

并将 package.json 中的 npm run build-dev 命令改为:"rimraf dist && rollup --config scripts/rollup/dev.config.js --bundleConfigAsCjs"

现在运行 npm run build-dev 就可以得到 reactreact-dom 的打包产物了。

  1. 打包后的测试

通过 pnpm lint --global 全局链接自己开发的react包和react-dom包:

cd .\dist\node_modules\react\
pnpm link --globalcd ..\react-dom\
pnpm link --global

打开测试项目,链接至全局包,然后启动项目:

pnpm link react --global
pnpm link react-dom --global
pnpm start

在项目src/index.js中:

import React from 'react'
import ReactDOM from 'react-dom'const jsx = (<div key={123} ref={'khs'}><span>big-react</span></div>
)const root = document.querySelector('#root')
ReactDOM.createRoot(root).render(jsx)console.log(React)
console.log(jsx)
console.log(ReactDOM)

渲染测试成功:


至此,我们就实现了基础版的 react-dom 包,更多的功能我们将在后面一一实现。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com