Quiet
  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我

bajiu

  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我
Quiet主题
  • React

React项目结构和JSX工作原理(一)

bajiu
前端

2024-11-06 16:44:00

最近觉得得开始找工作了,虽然现在好像都不咋招人了,但还是得复习一下react基本原理和相关技术栈,再着重看看部分源码,以前看的时候没有记录需要的时候找不着,这回得作个文档留存:

拉代码: https://github.com/facebook/react

demo: https://github.com/bajiu/blog_code/tree/master/react

项目结构

目录结构

react 采用的使monorepo的项目结构,这种结构下有先找ES-lint,因为一般为了代码的统一都会在最外层做统一的lint来保证代码一致性

这次先重点关注三个包:

  • react: 包含核心API,如 useState、useEffect 等。
  • react-dom: 包含与浏览器相关的DOM操作代码。
  • scheduler: 负责调度更新的模块,使react调度的核心。

然后是剩下的一些相关包:

  • shared:包含一些公共的工具和类型定义,其他模块也会引用。
  • react-reconciler:这是 React 的核心调和器模块,管理 React 树的对比和更新。
  • jest和react-test-renderer:包含测试框架 Jest 的配置和一些自定义匹配器。允许在不依赖 DOM 的情况下渲染和测试 React 组件。
  • react-noop-renderer: 主要用于测试和调试 Fiber 架构。
  • eslint-plugin-react-hooks:这是 React Hooks 的 ESLint 插件,用于检查 Hooks 的使用规则,如依赖项数组和调用顺序。
  • fixtures:包含一些小型应用和示例代码,用于演示和测试不同的 React 功能。
  • scripts:包含构建、测试和发布 React 的脚本和配置。
  • flow:React 使用了 Flow 类型检查,该文件夹包含 Flow 配置和类型声明。

接下来的阅读顺序大概如下:

  1. 从 react/src/React.js 的 API 出发,逐步分析 createElement 和 Component 等关键实现。
  2. 在 react-dom/src/ReactDOM.js 中,研究 React 是如何将虚拟 DOM 转换为真实 DOM。
  3. 通过 scheduler 理解任务调度,并在 react-reconciler 了解 Fiber 树的调度和更新机制。

在这之前,我们先了解一下JSX。

React核心API和hooks

先介绍一下核心api,这里就直接列出来了,之后分析源码时候再挨个来

  • React.createElement(type, [props], [...children]):创建一个虚拟 DOM.

  • React.Component 和 React.PureComponent:定义一个 React 组件的基类。React.PureComponent 是 Component 的子类,自动实现 shouldComponentUpdate,通过浅比较来决定是否重新渲染。

  • React.Fragment:包装多个元素,避免创建额外的 DOM 节点。

  • ReactDOM.render(element, container):将 React 元素渲染到实际的 DOM 中(一般用于项目的根节点)。

  • ReactDOM.createPortal(child, container):将子元素渲染到指定的 DOM 节点,而不在父组件的 DOM 层级内。实现模态框等浮层。

  • React.useState(initialState):在函数组件中定义一个状态变量。

  • React.useEffect(callback, [dependencies]):useEffect(callback, [dependencies])

  • React.useMemo(callback, [dependencies]):优化性能,避免不必要的计算。useMemo 会记住一个计算的值,只有当依赖项发生变化时才会重新计算,否则会返回上次的计算结果。

  • React.useCallback(callback, [dependencies])

  • React.useRef(initialValue)

  • React.useLayoutEffect(callback, [dependencies]):与 useEffect 类似,useLayoutEffect 在 DOM 更新之后、浏览器绘制之前同步执行,这意味着它会阻塞浏览器的渲染。dan同时能保证布局更新发生后立即执行,而不会让浏览器在布局更新时渲染不一致的内容。

  • React.createContext(defaultValue):创建上下文对象,允许组件通过上下文共享状态而不必手动传递 props。

  • React.useContext(Context):在函数组件中订阅上下文变化,避免多层传递属性。

  • React.useReducer(reducer, initialArg, init?):管理那些状态变更逻辑复杂、状态依赖其他状态值的场景。

  • React.useImperativeHandle(ref, createHandle, [dependencies])

  • React.memo(Component, [areEqual]):是用于组件的优化,通过缓存组件的渲染结果来避免不必要的重新渲染。

  • React.forwardRef((props, ref) => <Component {...props} ref={ref} />):使函数组件支持 ref 属性,便于父组件访问子组件的 DOM 节点或实例。

  • React.lazy(() => import(‘./MyComponent’)):实现组件的动态加载,按需加载组件以优化性能。

  • React.Suspense:与 React.lazy 一起使用,设置异步加载组件的占位内容。

  • React.StrictMode: 在开发模式下启用严格模式(没用过)。

  • React.useId: 差点儿拉下了这个,生成唯一ID的

JSX详解

JSX(JavaScript XML)是 React 提供的一种语法扩展,允许我们在 JavaScript 中直接编写类似 HTML 的标签,从而更加直观地描述 UI 结构。JSX 最终会被编译成纯 JavaScript 代码,并通过 React.createElement 函数生成虚拟 DOM。

1.基本语法

jsx的基本语法类似于HTML,但实际上是在js中嵌入 XML 语法。

比如这段代码

const element = <h1>Hello, world!</h1>;

会被Babel编译成

const element = React.createElement('h1', null, 'Hello, world!');

也可以嵌入表达式:

const name = "summer889";
const element = <h1>Hello, {name}!</h1>;
// 编译后
const element = React.createElement('h1', null, `Hello, ${name}!`);

对于属性的处理与HTML类似,但是属性名需要使用驼峰命名

子元素嵌套、条件、数组、内联样式的渲染:

// 子元素嵌套
const element0 = (
  <div>
    <h1>Hello, React!</h1>
    <p>This is a paragraph.</p>
  </div>
);
// 编译结果
const element0 = React.createElement(
  'div',
  null,
  React.createElement('h1', null, 'Hello, World!'),
  React.createElement('p', null, 'run p tag')
);
// 条件渲染
const isLoggedIn = true;
const element1 = (
  <div>
    {isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please sign in.</h1>}
  </div>
);
// 编译结果
const element1 = React.createElement(
  'div',
  null,
  isLoggedIn
    ? React.createElement('h1', null, 'Welcome back!')
    : React.createElement('h1', null, 'Please sign in.')
);
// 数组渲染
const items = ['Apple', 'Banana', 'Cherry'];
const element2 = (
  <ul>
    {items.map((item, index) => (
      <li key={index}>{item}</li>
    ))}
  </ul>
);
// 内联样式渲染
const divStyle = { color: 'blue', fontSize: '20px' };
const element3 = <div style={divStyle}>Styled Text</div>;
// 编译结果
const element3 = React.createElement('div', { style: { color: 'blue', fontSize: '20px' } }, 'Styled Text');

使用 JSX 可以减少 React.createElement 的调用次数,使代码更接近 HTML 结构。

工作原理

要把大象装冰箱,总共分三步:

1.JSX 编译:React 使用 Babel 将 JSX 编译为 React.createElement 调用。
2.生成虚拟 DOM:React.createElement 会返回一个 JavaScript 对象,描述这个元素的类型、属性和子元素。这个对象就是虚拟 DOM(VNode)。
3.渲染到真实 DOM:React 会根据虚拟 DOM 的变化,通过调和(reconciliation)算法将更改反映到实际的 DOM 中。

Babel编译JSX主要也分为三个部分:

  • 类型识别:判断 JSX 标签的类型,例如 HTML 标签(h1、div)或 React 组件。
  • 属性解析:将 JSX 属性转换为对象形式传递给 React.createElement。
  • 子元素解析:嵌套的 JSX 标签会递归转换为 React.createElement 的嵌套调用。

使用Babel插件 @babel/preset-react

Babel 使用 @babel/preset-react 这个预设(preset)来识别和转换 JSX。,先安装:

yarn add @babel/core @babel/cli @babel/preset-react -D

配置 .babelrc

{
  "presets": ["@babel/preset-react"]
}

JSX 到 React.createElement 的转换

const element = (
  <div className="container">
    <h1>Hello, World!</h1>
    <p>This is summmer889.</p>
  </div>
);

添加编译: "jsx": "babel --presets @babel/preset-react src/jsx/example1.jsx --out-dir dist" 然后我们就可以在dist/example1.js中看到编译后的代码:

const element = /*#__PURE__*/React.createElement("div", {
  className: "container"
}, /*#__PURE__*/React.createElement("h1", null, "Hello, World!"), /*#__PURE__*/React.createElement("p", null, "JSX \u8F6C\u6362"));
console.log(element);

我们可以看到汉字被转成了unicode编码,react里面应该有在做CharCodeAt方法。然后加载进界面就能看到了。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSX with Babel</title>
    <!-- 引入 React 和 ReactDOM -->
    <script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
</head>
<body>
<div id="root"></div> <!-- React 渲染容器 -->
<script src="../../dist/example1.js"></script> <!-- 编译后的 JavaScript -->
</body>
</html>

然后就能水灵灵看到界面了:
JSX编译界面

并且在日志里可以看到如下信息:
JSX编译日志

JSX编译选项

Babel 在 @babel/preset-react 中提供了以下几个选项,可以用来控制 JSX 的编译行为:

  • pragma:指定用于创建元素的函数,默认为 React.createElement。
  • pragmaFrag:指定用于创建 Fragment 的函数,默认为 React.Fragment。
  • throwIfNamespace:当使用 XML 命名空间(例如 SVG)时,是否抛出错误。默认为 true。
  • runtime:决定运行时编译方式,有两个可选值:
    • "classic":传统的编译方式,将 JSX 转换为 React.createElement。
    • "automatic":自动引入运行时,避免手动导入 React。

React 17 引入了自动化运行时,即可以使用 runtime: "automatic" 的新编译模式。在这个模式下,不再需要手动导入 React。

手动调用babel进行JSX转换

当然,我们也可以在项目中通过手动的方式进行对JSX的转换

const babel = require('@babel/core');

const jsxCode = `<h1>Hello, world!</h1>`;
const result = babel.transform(jsxCode, {
  presets: ['@babel/preset-react']
});

console.log(result.code);

通过 Babel 插件扩展 JSX

Babel 插件可以帮助我们进一步扩展 JSX 的功能。例如,可以添加自定义属性、样式前缀或自动优化等。

在每个 JSX 元素上自动添加 data-summer 属性,首先我们要创建一个自定义 Babel 插件:

module.exports = function ({ types: t }) {
  return {
    visitor: {
      JSXOpeningElement(path) {
        path.node.attributes.push(
          t.jsxAttribute(t.jsxIdentifier('data-summer'), t.stringLiteral('auto-id'))
        );
      }
    }
  };
};
  • Babel 提供的 t(即 Babel 的 AST 工具)来生成 JSX 属性。
  • 在 JSXOpeningElement 访问器中,给每个 JSX 元素添加 data-summer="auto-id" 属性。
  • 当 Babel 处理 JSX 时,每个 JSX 元素都会自动添加 data-summer 属性。

在.babelrc文件中添加插件的配置

{
  "presets": ["@babel/preset-react"],
  "plugins": [
    "./src/babelplugin"
  ]
}

然后继续刚才的操作,就有了

JSX编译Babel插件

要是有前来学习的小伙伴儿迷茫的,具体babel相关的看webpack那部分去~

上一篇

自定义hooks之拖拽功能

下一篇

RxJs常用操作符

©2024 By bajiu.