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

news2024/10/10 6:21:05

前面,我们提到 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.finishedWork

  if (finishedWork === null) {
    return
  }

  if (__DEV__) {
    console.warn('commit阶段开始', finishedWork)
  }

  //重置
  root.finishedWork = null

  // 判断3个子阶段需要执行的操作
  //  使用 MutationMask 判断是否存在副作用
  const subtreeHasEffect =
    (finishedWork.subtreeFlags & MutationMask) !== NoFlags // subtree
  const rootHasEffects = (finishedWork.flags & MutationMask) !== NoFlags // root

  if (subtreeHasEffect || rootHasEffects) {
    // 1.beforeMutation
    
    // 2.mutation
    CommitMutationEffects(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 = finishedWork

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

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

/** 对 Fiber节点的 Mutation 操作 */
const commitMutationEffectsOnFiber = (finishedWork: FiberNode) => {
  const flags = finishedWork.flags

  if ((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)
  }

  // 找到parentDom
  const 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-dom

pnpm 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 处理props
  const 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 alias
      alias({
        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 --global

cd ..\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 包,更多的功能我们将在后面一一实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1863010.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IEEE JSTSP综述:从信号处理领域分析视触觉传感器的研究

触觉传感器是机器人系统的重要组成部分&#xff0c;虽然与视觉相比触觉具有较小的感知面积&#xff0c;但却可以提供机器人与物体交互过程中更加真实的物理信息。 视觉触觉传感是一种分辨率高、成本低的触觉感知技术&#xff0c;被广泛应用于分类、抓取、操作等领域中。近期&a…

JavaWeb——MySQL:DDL

目录 3.DQL&#xff1a;查询 3.5 分页查询 ​编辑 总结&#xff1a; 3. DQL&#xff1a;查询 查询是使用最多、最频繁的操作&#xff0c;因为前面的修改以及删除&#xff0c;一般会交给数据库专业的人员&#xff0c;对于非数据库专业人员来说&#xff0c;老板一般会放心的…

vue3使用v-html实现文本关键词变色

首先看应用场景 这有一段文本内容&#xff0c;是项目的简介&#xff0c;想要实现将文本中的关键词进行变色处理 有如下关键词 实现思路 遍历文本内容&#xff0c;找到关键词&#xff0c;并使用某种方法更改其字体样式。经过搜寻资料决定采用v-html实现&#xff0c;但是v-h…

数据库管理-第210期 HaloDB-Oracle兼容性测试02(20240622)

数据库管理210期 2024-06-22 数据库管理-第210期 HaloDB-Oracle兼容性测试02&#xff08;20240622&#xff09;1 表增加列2 约束3 自增列4 虚拟列5 表注释6 truncat表总结 数据库管理-第210期 HaloDB-Oracle兼容性测试02&#xff08;20240622&#xff09; 作者&#xff1a;胖头…

一些css记录

background-blend-mode 定义了背景层的混合模式&#xff08;图片与颜色&#xff09;。菜鸟教程地址 例如&#xff1a; filter 滤镜 background-clip 背景颜色出现位置&#xff0c;是否包含边框 border-box | padding-box | content-box 有点像盒子模型 border-image-sou…

【Linux】使用ntp同步时间

ntp介绍 NTP&#xff08;Network Time Protocol&#xff0c;网络时间协议&#xff09;是一种用于同步计算机时间的协议&#xff0c;工作在UDP的123端口上。它是一种客户端-服务器协议&#xff0c;用于同步计算机的时钟。通过连接到网络上的时间服务器&#xff0c;计算机可以获…

记因hive配置文件参数运用不当导致 sqoop MySQL导入数据到hive 失败的案例

sqoop MySQL导入数据到hive报错 ERROR tool.ImportTool: Encountered IOException running import job: java.io.IOException: Hive exited with status 64 报错解释&#xff1a; 这个错误表明Sqoop在尝试导入数据到Hive时遇到了问题&#xff0c;导致Hive进程异常退出。状态码…

【数据结构】--顺序表

&#x1f47b;个人主页: 起名字真南 &#x1f47e;个人专栏: [数据结构初阶] [C语言] 目录 1 线性表2 顺序表2.1 概念和结构2.2 顺序表的实现2.2.1 头文件的定义2.2.2 初始化2.2.3 检查空间大小2.2.4 尾插2.2.5 打印2.2.6 头插2.2.7 查找指定数据2.2.8 头删2.2.9 尾删 2倍 1 线…

浏览器扩展V3开发系列之 chrome.contextMenus 右键菜单的用法和案例

【作者主页】&#xff1a;小鱼神1024 【擅长领域】&#xff1a;JS逆向、小程序逆向、AST还原、验证码突防、Python开发、浏览器插件开发、React前端开发、NestJS后端开发等等 chrome.contextMenus 允许开发者向浏览器的右键菜单添加自定义项。 在使用 chrome.contextMenus 之前…

基于 Redis 实现秒杀资格判断,提升并发性能

在互联网电商平台上&#xff0c;秒杀活动往往会吸引大量用户同时抢购&#xff0c;如何高效地处理高并发请求&#xff0c;保证用户体验&#xff0c;是一个重要的技术挑战。本文将介绍如何基于 Redis 实现秒杀资格的判断&#xff0c;提高并发性能。 基本思路 秒杀活动的核心流程…

SpringBoot整合MongoDB JPA使用

一、整合MongoDB SpringDataMongoDB是 SpringData家族成员之一&#xff0c;MongoDB的持久层框架&#xff0c;底层封装了 mongodb-driver。mongodb-driver 是 MongoDB官方推出的 Java连接 MongoDB的驱动包&#xff0c;相当于JDBC驱动。 SpringBoot整合 MongoDB&#xff0c;引入…

MySQL数据库—MHA高可用配置及故障切换

目录 一、MHA概述 1.什么是 MHA 2.MHA 的组成 &#xff08;1&#xff09;MHA Node&#xff08;数据节点&#xff09; &#xff08;2&#xff09;MHA Manager&#xff08;管理节点&#xff09; (3)MHA 的特点 二、MHA的一主两从部署 实验设计 实验具体操作 1.配置主…

一次breach1靶机的渗透测试

1.端口扫描和信息收集 2.CMS后台信息收集 3.解密HTTPS流量 4.tomcat的后台利用 5.提权 1.端口扫描和信息收集&#xff1a; 首先进行主机发现&#xff0c;找到目标机器&#xff1a; nmap -sP 192.168.110.1/24 找到目标机器&#xff0c;进行端口扫描&#xff1a; nmap -T4 …

CS144 Lab3 TCPSender复盘

一.基础概念 1.TCPSender在TCPSocket中的地位与作用 Lab0中实现了基于内存模拟的流控制-字节流&#xff08;ByteStream&#xff09;&#xff0c;底层使用std::deque实现&#xff0c;根据最大容量Capacity进行容量控制。个人理解它相当于应用层的输入输出缓存区&#xff0c;用户…

【opencv - C++ - Ubuntu】putText 显示中文最快方法

话不多说&#xff0c;直接上代码 #include <iostream> #include <opencv2/opencv.hpp> #include <opencv2/freetype.hpp>using namespace std; using namespace cv;int main(void) {Mat image(1000, 1800, CV_8UC3, Scalar(200,162,33));Ptr<freetype::F…

如何找到合适的Python第三方库?

找合适的Python库其实很简单&#xff0c;按照以下三步法&#xff0c;你能找到90%的Python库。 1、百度谷歌搜索 明确自己的需求&#xff0c;用Python来干什么&#xff0c;力求简短明了。比如定位“数据分析”&#xff0c;然后去搜索关键词【Python数据分析第三方库】&#xf…

【嵌入式Linux】i.MX6ULL 外部中断服务函数的初始化

文章目录 1. Cortex-A7 中断系统1.1 分析1.2 具体处理流程 2. 外部中断服务函数的初始化2.1 基本流程分析2.2 具体代码分析2.2.1. 定义中断处理类型和结构体2.2.2. 初始化中断系统2.2.3. 注册中断处理函数2.2.4. 具体的中断处理逻辑2.2.5. 默认的中断处理函数 3. 完整代码 本文…

django学习入门系列之第三点《案例 小米商城二级菜单》

文章目录 样例划分区域搭建骨架logo区域完整代码 小结往期回顾 样例 划分区域 搭建骨架 <!-- 二级菜单部分 --> <div class"sub-header"><div class"container"><div class"logo">1</div><div class"sea…

[word] Word表格怎么填充序列号? #微信#微信#笔记

Word表格怎么填充序列号&#xff1f; Word表格怎么填充序列号&#xff1f;在Excel中填充序列号是很轻松的事情&#xff0c;在Word表格中填充序列号就没那么简单&#xff0c;但是还是有小技巧&#xff0c;可以实现Word表格序号填充&#xff0c;还能自动更新。 1、插入序号 先…

JAVAEE之网络原理_传输控制协议(TCP)的滑动窗口、流量控制、拥塞控制、延迟应答、捎带应答机制

前言 在前面几节&#xff0c;我们讲解了TCP协议的基本概念、报文格式。还介绍了确认应答机制、超时重传、连接管理机制&#xff0c;在本节中 我们将会继续介绍TCP协议的其他机制。 一、滑动窗口机制&#xff08;效率机制&#xff09; 在前面的章节中我们讨论了确认应答策略&…