React入门教学

0 阅读30分钟

React项目创建

npx create-react-app myproject --template typescript
  • npx :跟随Node.js安装,包执行工具,专注于运行包中的命令或临时安装/执行包(无需全局安装)。Node.js 的包管理器,用于安装、管理、发布 Node.js 包(依赖管理)。
  • create-react-app :React项目搭建的脚手架工具,简称CRA。
  • myproject :自己定义的项目名称。
  • --template typescript :create-react-app命令的一个参数,指定使用TS语法创建项目。

CRA脚手架创建的项目,默认使用的Webpack工具构建项目。如果想使用vite构建工具,可以用vite搭建react项目。

npm create vite@latest my-vite-app -- --template react-ts

项目创建完成后,可执行npm start启动项目。

自定义组件

React 应用程序是由 组件 组成的。一个组件是 UI(用户界面)的一部分,它拥有自己的逻辑和外观。组件可以小到一个按钮,也可以大到整个页面。

1、JSX/TSX方式编写

新建动物卡片组件Animal.tsx文件。如果是js项目,后缀名就是jsx。

import "./Animal.css";

const Animal = ({ name, age }: { name: string; age: number }) => {
  return (
    <div className="animal-card">
      <div className="animal-icon">🐾</div>
      <div className="animal-info">
        <h3 className="animal-name">{name}</h3>
        <p className="animal-age">今年 {age} 岁了!</p>
      </div>
    </div>
  );
};
export default Animal;

在项目入口的index.tsx中使用自定义动物组件。

root.render(
  <React.StrictMode>
    <Animal name={"小狗"} age={8}></Animal>
  </React.StrictMode>,
);

2、JS/TS方式编写

借助React 相关库文件,通过js引入,便可以使用react框架。React有两种创建组件的方式,分别是函数式组件类式组件

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello React World</title>
</head>
<body>
    <!-- 1. 创建一个容器,React 会接管这个区域 -->
    <div id="root"></div>
	<div id="root2"></div>

    <!-- 2. 引入 React 相关库文件(请确保路径或 CDN 链接正确) -->
	<!-- 1. 引入 React 核心库 -->
	<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
	<!-- 2. 引入 ReactDOM 库,用于操作 DOM -->
	<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
	<!-- 3. 引入 Babel 库,用于将 JSX 代码转换为浏览器能识别的 JS -->
	<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <!-- 3. 编写 React 代码,注意 type 必须是 "text/babel" -->
    <script type="text/babel">
        // 创建一个简单的 React 组件
        //  function HelloWorld() {
        //      return <h1>Hello, React World!!</h1>;
        //  }
        //  // 将我们的 HelloWorld 组件渲染到 #root 容器中
        //  const root = ReactDOM.createRoot(document.getElementById('root'));
        //  root.render(<HelloWorld />);
		
		// 函数式组件
		const vdom=(
			<h1>我是函数式组件~</h1>
		)
		ReactDOM.render(vdom,document.getElementById('root'));
		
		// 类式组件
		class MyComponent extends React.Component{
			render(){
				return <h1>我是类式组件~</h1>
			}
		}
		ReactDOM.render(<MyComponent />,document.getElementById('root2'));
    </script>
</body>
</html>

三大属性之state

组件通常需要根据交互更改屏幕上显示的内容。输入表单应该更新输入字段,单击轮播图上的“下一个”应该更改显示的图片,单击“购买”应该将商品放入购物车。组件需要“记住”某些东西:当前输入值、当前图片、购物车。在 React 中,这种组件特有的记忆被称为 state。以下代码从原始写法推理过度到精简版本,便于理解。

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>state learn</title>
  </head>
  <body>
    <!-- 1. 创建一个容器,React 会接管这个区域 -->
    <div id="root"></div>
    <div id="root2"></div>

    <!-- 2. 引入 React 相关库文件(请确保路径或 CDN 链接正确) -->
    <!-- 1. 引入 React 核心库 -->
    <script
      crossorigin
      src="https://unpkg.com/react@18/umd/react.development.js"
    ></script>
    <!-- 2. 引入 ReactDOM 库,用于操作 DOM -->
    <script
      crossorigin
      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
    ></script>
    <!-- 3. 引入 Babel 库,用于将 JSX 代码转换为浏览器能识别的 JS -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <!-- 3. 编写 React 代码,注意 type 必须是 "text/babel" -->
    <script type="text/babel">
      //推理版
      class MyComponent extends React.Component {
        constructor(props) {
          super(props);
          this.state = { name: "老张" };
          this.changeName = this.changeName.bind(this);
        }
        render() {
          return (
            <h1 onClick={this.changeName}>我变幻莫测,我是{this.state.name}</h1>
          );
        }

        changeName() {
          let name = this.state.name == "老张" ? "小李" : "老张";
          this.setState({ name: name });
        }
      }

      ReactDOM.render(<MyComponent />, document.getElementById("root"));

      //简化版
      class MyComponent2 extends React.Component {
        state = { name: "老张" };

        render() {
          return (
            <h1 onClick={this.changeName}>我变幻莫测,我是{this.state.name}</h1>
          );
        }

        changeName = () => {
          let name = this.state.name == "老张" ? "小李" : "老张";
          this.setState({ name: name });
        };
      }
	  ReactDOM.render(<MyComponent2 />, document.getElementById("root2"));
    </script>
  </body>
</html>

推理版state使用样例中 this 指向问题说明:

核心问题: 想在后面的逻辑里修改 state.name 的值,都得围绕 this 去写。

问题分析:

  1. 如果 onClick={changeName},且 changeName 定义在 MyComponent 类外面

    • 因为 <script type="text/babel"> 是严格模式,所以 this 是 undefined
    • 即使不是严格模式,this 也是 window
    • 所以拿不到 state 对象
  2. 如果 onClick={this.changeName},且 changeName 定义在 MyComponent 类里面

    • 因为 onClick={this.changeName} 实际意义是把 changeName 函数复制给了 onClick
    • 不是在 MyComponent 类里直接调用 changeName 函数
    • 所以 this 对象也是 undefined

原因验证(纯 JS 示例):

<html>
  <body>
    <script type="text/javascript">
      class Person {
        constructor(name) {
          this.name = name;
        }
        speak() {
          console.log(this);
        }
      }
      const p1 = new Person("张三");
      p1.speak();
      // 打印 Person {name: '张三'}
      // 因为是 Person 实例 p1 调用了 speak 函数,所以 this 指向 Person 实例

      const pp = p1.speak;
      pp();
      // 打印 undefined
      // 因为是把 speak 函数赋值给了 pp,window 对象调用了 pp 函数
      // 理论上 this 指向 window,但类方法默认严格模式,所以是 undefined
    </script>
  </body>
</html>

三大属性之props

props(properties 的缩写)是 React 中组件之间传递数据的核心机制。它是从父组件流向子组件的只读数据,子组件无法修改自己接收到的 props。只有props不需要组件实例对象就可以使用,state和refs都需要实例对象。

<!doctype html>
<html>
  <body>
    <div id="root"></div>

    <!-- 1. 创建一个容器,React 会接管这个区域 -->
    <div id="root"></div>
    <div id="root2"></div>

    <!-- 2. 引入 React 相关库文件(请确保路径或 CDN 链接正确) -->
    <!-- 1. 引入 React 核心库 -->
    <script
      crossorigin
      src="https://unpkg.com/react@18/umd/react.development.js"
    ></script>
    <!-- 2. 引入 ReactDOM 库,用于操作 DOM -->
    <script
      crossorigin
      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
    ></script>
    <!-- 3. 引入 Babel 库,用于将 JSX 代码转换为浏览器能识别的 JS -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <!-- 4. 引入 PropTypes -->
    <script src="https://unpkg.com/prop-types@15.8.1/prop-types.js"></script>

    <!-- 3. 编写 React 代码,注意 type 必须是 "text/babel" -->
    <script type="text/babel">
      class MyComponent extends React.Component {
        render() {
          const { color, size, count } = this.props;
          return (
            <ul>
              <li>颜色:{color}</li>
              <li>尺寸:{size}</li>
              <li>数量:{count}</li>
            </ul>
          );
        }

        static propTypes = {
          color: PropTypes.string.isRequired,
          size: PropTypes.number,
          count: PropTypes.number,
        };
      }
      const domNode1 = document.getElementById("root");
      const root1 = ReactDOM.createRoot(domNode1);
      const things = { color: "red", size: 20, count: 5 };
      root1.render(<MyComponent {...things} />);

      const domNode2 = document.getElementById("root2");
      const root2 = ReactDOM.createRoot(domNode2);
      root2.render(<MyComponent color="green" size={1} count={30} />);
    </script>
  </body>
</html>

三大属性之Ref

Ref(引用)  是 React 提供的一种直接访问 DOM 元素或组件实例的方式,它不会触发重新渲染。 以下代码中介绍了字符串形式的、回调形式的和creactRef形式的ref。其中字符串形式的ref在高版本的react可能会被移除。creactRef使用比较广泛。

<!doctype html>
<html>
  <body>
    <div id="root"></div>

    <!-- 1. 创建一个容器,React 会接管这个区域 -->
    <div id="root"></div>
    <div id="root2"></div>

    <!-- 2. 引入 React 相关库文件(请确保路径或 CDN 链接正确) -->
    <!-- 1. 引入 React 核心库 -->
    <script
      crossorigin
      src="https://unpkg.com/react@18/umd/react.development.js"
    ></script>
    <!-- 2. 引入 ReactDOM 库,用于操作 DOM -->
    <script
      crossorigin
      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
    ></script>
    <!-- 3. 引入 Babel 库,用于将 JSX 代码转换为浏览器能识别的 JS -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <!-- 4. 引入 PropTypes -->
    <script src="https://unpkg.com/prop-types@15.8.1/prop-types.js"></script>

    <!-- 3. 编写 React 代码,注意 type 必须是 "text/babel" -->
    <script type="text/babel">
      class Demo extends React.Component {
        inFourRef = React.createRef();

        render() {
          return (
            <div
              style={{ display: "flex", flexDirection: "column", width: "50%" }}
            >
              <input
                id="inone"
                type="text"
                placeholder="点击按钮提示"
                value="原生api方式"
              ></input>
              <button onClick={this.showData}>点我</button>
              <input
                ref="intwo"
                onBlur={this.showData2}
                type="text"
                placeholder="失去焦点时提示"
                value="字符串ref"
              ></input>
              <input
                ref={(curNode) => {
                  this.inthree = curNode;
                }}
                onBlur={() => {
                  alert("提示:" + this.inthree.value);
                }}
                type="text"
                value="回调形式的ref"
              ></input>

              <input
                ref={this.inFourRef}
                onBlur={() => {
                  alert("提示:" + this.inFourRef.current.value);
                }}
                type="text"
                value="createRef的ref"
              ></input>
            </div>
          );
        }
        showData = () => {
          const inone = document.getElementById("inone");
          alert("提示:" + inone.value);
        };

        showData2 = () => {
          const { intwo } = this.refs;
          alert("提示:" + intwo.value);
        };
      }
      const domNode = document.getElementById("root");
      const root = ReactDOM.createRoot(domNode);
      root.render(<Demo />);
    </script>
  </body>
</html>

createRef只能创建ref在refs对象里只有一个对象,对象名称是current。如果多次使用createRef创建ref对象,之前创建的会被最后创建的覆盖。

React 组件生命周期详解(新旧版本对比)

React 类组件的生命周期是指组件从创建、挂载、更新到卸载的整个过程中,在不同阶段自动执行的特定方法。理解这些方法,可以在合适的时机执行数据请求、DOM 操作、状态同步等逻辑。

注意:本文只讨论类组件的生命周期,不涉及函数式组件中的 Hooks(如 useEffect)。以下“旧版本”指 React 16.3 之前,“新版本”指 React 16.3 及以后(带 UNSAFE_ 前缀的方法依然可用,但不推荐)。


一、旧版本生命周期(React < 16.3)

生命周期阶段及常用方法
阶段方法名用途调用时机
挂载constructor初始化 state、绑定事件组件实例创建时
componentWillMount在渲染前执行最后一次状态修改(已过时)render 之前
render返回 JSX 描述界面必须实现
componentDidMount发起网络请求、订阅、操作 DOM组件挂载到 DOM 后
更新componentWillReceiveProps根据 props 变化更新 state(已过时)父组件更新导致 props 变化时
shouldComponentUpdate性能优化,决定是否重新渲染接收到新 props/state 时
componentWillUpdate渲染前做准备工作(已过时)shouldComponentUpdate 返回 true 后,render
render重新计算界面props/state 变化
componentDidUpdate操作更新后的 DOM、发起新请求更新渲染到 DOM 后
卸载componentWillUnmount清理定时器、取消订阅、释放资源组件从 DOM 移除前

旧版本示例代码

import React from 'react';

class OldLifecycleDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    console.log('[旧版] constructor');
  }

  componentWillMount() {
    console.log('[旧版] componentWillMount');
  }

  componentDidMount() {
    console.log('[旧版] componentDidMount');
  }

  componentWillReceiveProps(nextProps) {
    console.log('[旧版] componentWillReceiveProps', nextProps);
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log('[旧版] shouldComponentUpdate', nextState);
    return true; // 允许更新
  }

  componentWillUpdate(nextProps, nextState) {
    console.log('[旧版] componentWillUpdate');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('[旧版] componentDidUpdate');
  }

  componentWillUnmount() {
    console.log('[旧版] componentWillUnmount');
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    console.log('[旧版] render');
    return (
      <div>
        <h3>旧版生命周期 Demo</h3>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>增加</button>
      </div>
    );
  }
}

// 父组件用于触发 props 更新
class OldParent extends React.Component {
  state = { parentCount: 0 };

  render() {
    console.log('[旧版父组件] render');
    return (
      <div>
        <button onClick={() => this.setState({ parentCount: this.state.parentCount + 1 })}>
          更新父组件 props
        </button>
        <OldLifecycleDemo value={this.state.parentCount} />
      </div>
    );
  }
}

export default OldParent;

运行日志(挂载 → 点击子组件按钮 → 点击父组件按钮 → 卸载)

[旧版父组件] render
[旧版] constructor
[旧版] componentWillMount
[旧版] render
[旧版] componentDidMount

// 点击子组件“增加”按钮(state 更新)
[旧版] shouldComponentUpdate {count: 1}
[旧版] componentWillUpdate
[旧版] render
[旧版] componentDidUpdate

// 点击父组件按钮(props 更新)
[旧版父组件] render
[旧版] componentWillReceiveProps {value: 1}
[旧版] shouldComponentUpdate {count: 1}
[旧版] componentWillUpdate
[旧版] render
[旧版] componentDidUpdate

// 卸载组件(模拟)
[旧版] componentWillUnmount

二、新版本生命周期(React ≥ 16.3)

React 16.3 之后,为了适应异步渲染(Fiber)并避免一些不安全的使用模式,废弃了 componentWillMountcomponentWillReceivePropscomponentWillUpdate,并增加了两个新方法:

阶段方法名用途调用时机
挂载constructor初始化 state、绑定事件组件实例创建时
static getDerivedStateFromProps根据 props 派生 state(无副作用)render 之前,挂载和更新时都会调用
render返回 JSX必须实现
componentDidMount副作用操作:网络请求、订阅、DOM 操作组件挂载后
更新static getDerivedStateFromProps同上,适用于 props 变化后同步 state挂载、更新时
shouldComponentUpdate性能优化,决定是否更新新 props/state 传入时
render重新渲染-
getSnapshotBeforeUpdate获取更新前的 DOM 快照(如滚动位置)render 输出提交到 DOM 前
componentDidUpdate操作更新后的 DOM、发起新请求更新渲染到 DOM 后
卸载componentWillUnmount清理工作组件移除前
  • 已废弃并加 UNSAFE_ 前缀的方法UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate(不建议使用)。
  • 新增静态方法static getDerivedStateFromProps 必须返回对象(合并到 state)或 null
  • 新增实例方法getSnapshotBeforeUpdate 必须返回一个快照值(任意类型),并作为第三个参数传递给 componentDidUpdate
新版本示例代码
import React from 'react';

class NewLifecycleDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, derivedMsg: '' };
    console.log('[新版] constructor');
  }

  static getDerivedStateFromProps(props, state) {
    console.log('[新版] getDerivedStateFromProps', props, state);
    // 根据 props 中的 value 更新派生状态(示例:拼接一条消息)
    if (props.value !== state.prevValue) {
      return {
        derivedMsg: `派生自 props.value: ${props.value}`,
        prevValue: props.value,
      };
    }
    return null;
  }

  componentDidMount() {
    console.log('[新版] componentDidMount');
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log('[新版] shouldComponentUpdate', nextState);
    return true;
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('[新版] getSnapshotBeforeUpdate', prevState);
    // 可以返回滚动位置、DOM 尺寸等
    return `快照: count=${prevState.count}`;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('[新版] componentDidUpdate', snapshot);
  }

  componentWillUnmount() {
    console.log('[新版] componentWillUnmount');
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    console.log('[新版] render');
    return (
      <div>
        <h3>新版生命周期 Demo</h3>
        <p>Count: {this.state.count}</p>
        <p>{this.state.derivedMsg}</p>
        <button onClick={this.handleClick}>增加</button>
      </div>
    );
  }
}

// 父组件用于触发 props 更新
class NewParent extends React.Component {
  state = { parentCount: 0 };

  render() {
    console.log('[新版父组件] render');
    return (
      <div>
        <button onClick={() => this.setState({ parentCount: this.state.parentCount + 1 })}>
          更新父组件 props
        </button>
        <NewLifecycleDemo value={this.state.parentCount} />
      </div>
    );
  }
}

export default NewParent;

运行日志(挂载 → 点击子组件按钮 → 点击父组件按钮 → 卸载)

[新版父组件] render
[新版] constructor
[新版] getDerivedStateFromProps {value: 0} {count: 0, derivedMsg: '', prevValue: undefined}
[新版] render
[新版] componentDidMount

// 点击子组件“增加”按钮(state 更新)
[新版] shouldComponentUpdate {count: 1, derivedMsg: '', prevValue: 0}
[新版] render
[新版] getSnapshotBeforeUpdate {count: 0, derivedMsg: '', prevValue: 0}
[新版] componentDidUpdate 快照: count=0

// 点击父组件按钮(props 更新)
[新版父组件] render
[新版] getDerivedStateFromProps {value: 1} {count: 1, derivedMsg: '', prevValue: 0}
[新版] shouldComponentUpdate {count: 1, derivedMsg: '派生自 props.value: 1', prevValue: 1}
[新版] render
[新版] getSnapshotBeforeUpdate {count: 1, derivedMsg: '派生自 props.value: 1', prevValue: 1}
[新版] componentDidUpdate 快照: count=1

// 卸载组件
[新版] componentWillUnmount

三、新旧版本对比

对比维度旧版本(≤16.2)新版本(≥16.3)说明
挂载前componentWillMount移除或加 UNSAFE_旧方法容易误用(如订阅、setState 可能引起额外渲染),新版本中推荐在 constructorcomponentDidMount 中处理
props 派生 statecomponentWillReceivePropsstatic getDerivedStateFromProps新方法为静态方法,无副作用且强制返回对象或 null;每次渲染(包括初始挂载)前都会调用,更安全可控
更新前componentWillUpdategetSnapshotBeforeUpdate旧方法可能在异步渲染下产生 bug;新方法专门用于捕获 DOM 信息(如滚动位置),且返回值可直接用于 componentDidUpdate
错误处理componentDidCatch 在 16.0 引入增加 static getDerivedStateFromError(16.6)错误边界进一步完善,静态方法用于渲染 fallback UI 前更新 state
异步渲染兼容部分方法(componentWillXXX)可能被调用多次新方法在异步模式下行为确定为了支持 React 的 Fiber 架构,避免不安全的生命周期逻辑
代码可读性生命周期逻辑分散在多个方法中新方法职责更单一,强制无副作用更易于维护和测试
核心变化总结
  1. 废弃三个带 Will 的生命周期componentWillMountcomponentWillReceivePropscomponentWillUpdate → 改用 UNSAFE_ 前缀或完全移除。
  2. 新增静态方法 getDerivedStateFromProps:统一处理 props 到 state 的映射,且保证在每次渲染前都被调用。
  3. 新增实例方法 getSnapshotBeforeUpdate:专门用于在更新前读取 DOM 属性(如滚动位置),避免旧版 componentWillUpdate 中读取 DOM 的隐患。
  4. 强化 componentDidUpdate:接收第三个参数 snapshot(由 getSnapshotBeforeUpdate 返回),方便在更新后恢复状态。

使用建议:

  • 新项目应完全遵循新版生命周期,避免使用 UNSAFE_ 系列方法。
  • 旧项目迁移时,可对照下表将旧方法替换为新方法:
旧方法替换方案
componentWillMount逻辑移至 constructorcomponentDidMount
componentWillReceiveProps改用 static getDerivedStateFromProps,或使用 componentDidUpdate 配合条件判断
componentWillUpdate改用 getSnapshotBeforeUpdate + componentDidUpdate

通过上述示例和对比,你可以清晰地观察到新旧生命周期方法的调用顺序及各自的适用场景。理解这些差异,能帮助你在 React 开发中写出更稳健、可维护的组件。

DOM Diff 算法详解(以 React 为例)

一、什么是 Diff 算法?

在 React 等前端框架中,为了减少直接操作真实 DOM 带来的性能开销,采用了 虚拟 DOM(Virtual DOM) 技术。当组件的状态变化时,会先生成新的虚拟 DOM 树,然后与旧的虚拟 DOM 树进行比较,找出差异(Diff),最后只将变化的部分更新到真实 DOM 上。这个比较过程就是 Diff 算法

React 的 Diff 算法基于以下三个核心假设(启发式策略):

  1. 不同类型的元素会生成不同的树。例如,<div> 变成 <span>,则直接销毁原节点及其子树,重新构建。
  2. 可以通过 key 属性来暗示哪些子元素在不同的渲染中保持稳定
  3. 同一层级的子节点比较时,只进行同层比较,不会跨层级移动

这些假设使得算法复杂度从 O(n³) 降至 O(n)。


二、React Diff 算法的执行过程

React 的 Diff 过程由 协调(Reconciliation) 模块完成。主要分为两大部分:

  1. 树的比较(单节点比较)
  2. 列表的比较(多节点比较,依赖 key
2.1 单节点 Diff(不同类型的节点)
  • 节点类型不同:直接卸载旧节点及其所有子节点,重新挂载新节点。
  • 节点类型相同:保留 DOM 节点,仅更新变化的属性。然后递归比较子节点。
2.2 多节点 Diff(同一层级的子节点列表)

这是 key 发挥核心作用的地方。对于子节点列表,React 会遍历新旧两个列表,通过 key 来判断哪些节点可以复用、哪些需要新增或删除。

React 旧版算法(React 15 及之前):采用双重循环,逐个匹配 key,性能较依赖 key 的顺序。

React 16 及之后的优化算法(基于 Fiber 架构):采用三个指针 + Map 的方式,进一步提升了效率。核心步骤:

  1. 更新阶段:遍历新列表,在旧列表中根据 key 查找可复用的节点。
  2. 移动阶段:如果找到的旧节点位置与当前位置不符,则生成移动操作;否则标记删除或新增。
  3. 最终删除剩余旧节点,插入新增节点。
2.3 节点复用条件

两个虚拟 DOM 节点能否复用,必须同时满足:

  • 同一层级(兄弟节点之间)
  • 相同 key(显式或隐式)
  • 相同节点类型(例如都是 <div>,或都是自定义组件)

三、Key 的作用与使用说明

3.1 为什么需要 Key?

当列表渲染时(如通过 map 生成的多个同类型子元素),如果没有 key,React 只能通过节点在列表中的顺序索引来进行对比。这会导致:

  • 性能下降:插入、删除、排序等操作会使大量节点被重新创建,而非移动。
  • 状态混乱:例如一个带输入框的列表项,顺序变化后,输入框的内容可能会错位(因为复用了错误的 DOM 节点)。

key 为每个节点提供了一个稳定、唯一的标识,让 React 能够准确识别哪些节点是相同的,从而只移动或更新必要的内容。

3.2 Key 的使用规则
  1. 必须唯一:在兄弟节点中,key 必须是唯一的。全局不要求唯一。
  2. 必须稳定key 在组件的不同渲染中应该保持不变。不能是随机数或 Date.now()
  3. 不要使用数组索引作为 key(除非列表是静态的、永不会重新排序或过滤)。
3.3 代码示例:正确与错误使用 Key
// ❌ 错误:用 index 作为 key
{items.map((item, index) => <li key={index}>{item.name}</li>)}

// ✅ 正确:使用数据中唯一的 id
{items.map(item => <li key={item.id}>{item.name}</li>)}

// ✅ 如果数据没有 id,可以生成稳定的 hash(但尽量后端提供)
{items.map(item => <li key={hash(item)}>{item.name}</li>)}
3.4 当没有提供 Key 时会发生什么?

React 会默认使用索引作为 key。这会引起两个问题:

  • 性能浪费:在列表头部插入元素时,所有后续元素索引改变,导致整个列表重新创建。
  • 组件状态错误:如果列表项是受控组件(如 <input>),输入内容可能会被错误地保留在移动后的节点上。

四、Key 的注意事项(重要)

场景建议原因
列表动态增删/排序使用数据中的唯一 ID(如数据库主键、uuid)确保 React 能正确识别节点移动,而非重建
列表静态且不会重新排序可以使用 index,但最好仍使用唯一 ID虽然 index 此时可行,但一旦需求变化容易引发 bug
同层不同组件的 key无需全局唯一,只要同层内唯一即可React 只在同层比较
跨层级移动(如将子节点提升为兄弟)这种操作不建议,React 不会尝试复用跨层级节点Diff 算法基于“不同层级不会复用”的假设,会导致重建
使用 Math.random() 或时间戳绝对禁止每次渲染 key 都不同,导致所有节点被销毁重建,失去 Diff 意义
列表内包含表单元素必须使用稳定的唯一 ID否则输入焦点、文本内容会错乱
反例:用索引作为 key 导致的问题
// 初始列表:['苹果', '香蕉', '橘子']
// 渲染三个 <li><input defaultValue={item}/></li>
// 用户在第一个输入框输入“水果”
// 现在在头部插入“草莓”:列表变为 ['草莓', '苹果', '香蕉', '橘子']
// 因为没有 key(或 key=index),React 会认为原来的第一个输入框现在属于“草莓”,
// 用户输入的“水果”依然留在第一个框,而“苹果”对应的输入框变成了空。
// 正确行为应该是:原有输入框跟随各自的项移动,但实际发生了错位。

五、总结与最佳实践

  1. 始终为动态列表提供 key,且使用数据中天然存在的、稳定的唯一标识。
  2. 不要使用索引,除非你能百分之百保证列表不会变化(例如静态配置菜单)。
  3. key 值应该是字符串或数字,不需要全局唯一,只需在兄弟元素中唯一。
  4. key 不会传递到组件内部,它只是 React 内部使用的属性。如果要同时传递一个 ID 到组件,请用另一个 prop(如 id)。
  5. 理解 Diff 与重建的区别:正确的 key 可以让 React 仅移动 DOM 节点,而不是销毁重建,从而保留组件内部状态(如动画、输入焦点)。

通过合理使用 key,可以让 React 的 Diff 算法发挥最大效能,提升页面渲染性能,并避免组件状态混乱。 使用 create-react-app 确实能快速启动一个零配置的现代 React 项目。下面我将从项目创建、目录结构,到核心文件的代码注释,一步步带你了解它的方方面面。

CRA脚手架介绍

第一步:创建项目和目录结构

以下是一个标准的初始化与启动流程:

  • 确保环境:确保已安装 Node.js (版本 ≥ 14) 和 npm (通常随 Node.js 一起安装)。
  • 创建新项目:在终端中执行 npx create-react-app my-app (my-app 替换为你的项目名)。这会在当前目录下创建一个名为 my-app 的新文件夹。
  • 进入项目目录cd my-app
  • 启动开发服务器:运行 npm start。随后浏览器会自动打开 http://localhost:3000,展示默认的 React 欢迎页面。
完整的项目目录结构

项目创建完成后,其根目录的完整结构如下:

my-app/
├── .gitignore              # Git版本管理忽略的文件配置
├── package-lock.json       # 锁定项目依赖包的精确版本
├── package.json            # 项目配置文件,包含依赖、脚本等
├── public/                 # 存放公共静态资源,此目录内容会原样输出
│   ├── favicon.ico         # 浏览器标签页图标
│   ├── index.html          # 单页应用的唯一HTML文件模板
│   ├── logo192.png         # PWA(渐进式Web应用)小图标
│   ├── logo512.png         # PWA大图标
│   ├── manifest.json       # PWA应用配置文件
│   └── robots.txt          # 爬虫规则文件
├── src/                    # 核心源代码目录,Webpack会处理此目录下的文件
│   ├── App.css             # App组件的专属样式文件
│   ├── App.js              # 根组件,应用的起点
│   ├── App.test.js         # App组件的单元测试文件
│   ├── index.css           # 全局样式文件
│   ├── index.js            # 整个应用的JavaScript入口文件
│   ├── logo.svg            # React默认Logo
│   ├── reportWebVitals.js  # 性能分析工具
│   └── setupTests.js       # 单元测试全局配置文件
└── README.md               # 项目说明文档

以上就是 my-app 初始化完成后的完整文件列表。

第二步:重要文件代码注释

接下来,我们来深入分析项目中三个最核心的文件。为了方便学习和理解,我会对它们进行详细的代码注释。

1. public/index.html (HTML 模板)

这个文件是整个应用的HTML模板。React 组件最终会被渲染到此文件内的一个 <div> 元素中。

<!-- 1. 声明文档类型为 HTML5 -->
<!DOCTYPE html>

<!-- 2. html 根元素,指定页面语言为英文 -->
<html lang="en">

<!-- 3. 头部,存放页面的元数据,不在页面主体中显示 -->
<head>
  <!-- 4. 设置字符编码为 UTF-8,确保全球字符正常显示 -->
  <meta charset="utf-8" />
  <!-- 5. 移动端适配:设置视口宽度等于设备宽度,初始缩放比例为1 -->
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <!-- 6. 配置 iOS 设备上 Safari 顶部状态栏的颜色 -->
  <meta name="theme-color" content="#000000" />
  <!-- 7. 提供页面描述,常用于SEO(搜索引擎优化)和社交媒体分享 -->
  <meta name="description" content="Web site created using create-react-app" />
  <!-- 8. 设置浏览器标签页标题 -->
  <title>React App</title>
</head>

<!-- 9. 页面主体 -->
<body>
  <!-- 10. 一个用于显示“浏览器不支持JS”的备用信息(通常不显示) -->
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <!-- 11. 一个空的 div,它是React应用挂载的根节点。React渲染的所有内容都会替换这个div -->
  <div id="root"></div>
</body>
</html>

以上代码结合了public/index.html的默认结构与最佳实践进行注释。

2. src/index.js (应用入口)

这是 React 应用的JavaScript入口文件。它负责将 React 应用(<App /> 组件)挂载到 index.html 中的 #root 元素上。

// 1. 导入 React 核心库,JSX语法依赖于此
import React from 'react';
// 2. 导入 ReactDOM 库,它用于操作和管理React应用在真实DOM上的渲染
import ReactDOM from 'react-dom/client';
// 3. 导入全局样式文件,其作用域是全局的
import './index.css';
// 4. 导入根组件 App
import App from './App';
// 5. 导入性能分析工具(可选)
import reportWebVitals from './reportWebVitals';

// 6. 获取 index.html 中 id 为 "root" 的 DOM 元素作为根容器
const root = ReactDOM.createRoot(document.getElementById('root'));
// 7. 将 App 组件渲染到根容器中
root.render(
  // 8. <React.StrictMode> 是一个严格模式包裹组件,用于检测应用中潜在的问题,但不会在UI中显示
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// 9. 性能分析函数,可以传入一个回调函数来收集或上报性能数据
reportWebVitals();

以上代码参考了src/index.js的标准写法及其作为JavaScript入口的核心作用。

3. src/App.js (根组件)

这是应用的根组件,也是所有其他组件的“容器”。它的结构和功能将决定整个页面的初始布局。

// 1. 导入 React 核心库
import React from 'react';
// 2. 导入 React Logo 图片文件
import logo from './logo.svg';
// 3. 导入此组件的专属样式,其作用域默认仅限此组件
import './App.css';

// 4. App 组件,它是一个函数式组件
function App() {
  return (
    // 5. 必须返回一个单一的JSX元素(通常是一个<div>或<React.Fragment>)
    <div className="App">
      {/* 6. 在JSX中嵌入JavaScript表达式需要放在大括号 {} 中 */}
      <header className="App-header">
        {/* 7. 显示Logo图片,src属性使用了导入的图片变量 */}
        <img src={logo} className="App-logo" alt="logo" />
        {/* 8. 一段提示文本 */}
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        {/* 9. 一个指向React官方教程的链接,target="_blank" 表示在新标签页打开 */}
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

// 10. 导出 App 组件,以便在其他文件(如 index.js)中导入并使用
export default App;

以上代码对src/App.js中每个元素的JSX结构、样式应用和文件导出逻辑做了详细说明。


💎 核心文件关系总结

梳理 index.html -> index.js -> App.js 的关系,可以把项目运行过程理解成这样:

React 应用准备启动时,public/index.html 是基础模板。随后,src/index.js 作为整个应用的 “大门”,将 src/App.js 定义的根组件,动态地“注入”到 HTML 模板中预设的 <div id="root"> 位置,最终在浏览器中渲染出完整的界面。

代理设置

在 Create React App 项目里给 Axios 请求设置代理,最主要的目的就是解决开发环境中的跨域问题。这里有 两种配置方式,可以根据项目的复杂程度来选:

🧭 两种代理配置方式对比

特性方式一:package.json 简单代理方式二:setupProxy.js 灵活代理(推荐)
适用场景后端接口单一且路径一致的项目大部分项目,尤其是多人协作、接口众多或路径不统一的场景
配置位置package.jsonsrc/setupProxy.js
配置内容一个简单的字符串,如 "proxy": "http://localhost:5000"使用 http-proxy-middleware 库,可进行精细化配置
核心优势配置简单,一行代码即可功能强大,支持多目标代理、路径重写等
需要重启,修改后必须重启开发服务器,同样需要重启开发服务器
灵活性,无法处理复杂逻辑,可精确控制代理行为

方式一:简单配置(适用于单一后端)

如果你的项目只需要代理一个后端服务,在 package.json 中添加 proxy 字段是最简单的。

步骤:

  1. 在项目根目录下的 package.json 文件中,添加 "proxy" 字段,值为后端服务器的完整地址。例如:
    {
      // ... 其他配置
      "proxy": "http://localhost:8080" // 后端接口地址
    }
    
    注意proxy 的值必须以 http://https:// 开头,否则会报错。
  2. 重启你的开发服务器 npm start,使配置生效。

使用方法(在代码中): 配置完成后,在 Axios 请求时,不再需要写完整 URL,直接写接口的路径即可

  • 之前axios.get('http://localhost:8080/api/users')
  • 之后axios.get('/api/users')

开发服务器会自动将 /api/users 这个请求转发到 http://localhost:8080/api/users,从而避免了跨域问题。

方式二:进阶配置(推荐,适用于多种后端)

这种方式使用 http-proxy-middleware 中间件,能更灵活地处理复杂的代理需求。

步骤:

  1. 安装依赖:在项目根目录下,通过终端安装 http-proxy-middleware。(正常情况下脚手架已经默认安装好了中间件)

    npm install http-proxy-middleware --save
    # 或
    yarn add http-proxy-middleware
    
  2. 创建配置文件:在 src 文件夹下,新建一个名为 setupProxy.js 的文件。(注意:文件名必须精确为 setupProxy.js

  3. 写入配置:在 setupProxy.js 中,使用 createProxyMiddleware 来配置代理规则。

    const { createProxyMiddleware } = require('http-proxy-middleware');
    
    module.exports = function (app) {
      // 配置1:将以 '/api' 开头的请求代理到目标服务器
      app.use(
        '/api',
        createProxyMiddleware({
          target: 'http://localhost:8080', // 目标服务器地址
          changeOrigin: true,             // 开启后,可解决后端CORS问题,防止代理失败
          // pathRewrite: { '^/api': '' }, // 可选:路径重写,例如将 '/api/users' 重写为 '/users'
        })
      );
      
      // 可以配置多个代理规则
      // app.use(
      //   '/auth',
      //   createProxyMiddleware({
      //     target: 'https://auth-server.com',
      //     changeOrigin: true,
      //   })
      // );
    };
    
  4. 重启开发服务器:修改配置文件后,需要重启服务器 npm start

使用方法(在代码中): 这种方式通过请求路径的前缀来区分转发到哪个后端。

  • 使用配置:如果你的配置中 '/api' 是请求前缀,那么在 Axios 中请求时就以 /api 开头。

    // 这个请求会被转发到 http://localhost:8080/api/users
    axios.get('/api/users')
    
  • 理解 pathRewrite:如果你配置了 pathRewrite: { '^/api': '' },意味着在转发请求时,会把路径中的 /api 前缀去掉。

    // 请求: axios.get('/api/users')
    // 实际转发到: http://localhost:8080/users
    

    pathRewritehttp-proxy-middleware 的核心功能,下面的表格展示了它带来的灵活性:

axios.get() 请求路径pathRewrite 配置代理服务器实际请求的路径
/api/users{ '^/api': '' }http://localhost:8080/users
/api/users{ '^/api': '/v1' }http://localhost:8080/v1/users
/api/users未配置http://localhost:8080/api/users

React Router 完全指南:从入门到进阶

掌握 React 中声明式路由的核心技巧

React Router 是 React 生态中最流行的路由解决方案。它让你可以在单页应用中实现多页面的体验——URL 变化、浏览器前进后退、数据预加载,一切都很流畅。本文将从零开始,教会你如何使用 React Router,然后深入探讨嵌套路由、路由守卫、懒加载等高级特性。

一、基础篇:快速上手

1. 安装

使用 npm 或 yarn 安装 react-router-dom

npm install react-router-dom
# 或
yarn add react-router-dom
2. 第一个路由示例

我们创建一个简单的博客应用,包含首页、关于页和文章页。

App.js

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function Home() {
  return <h2>🏠 首页</h2>;
}

function About() {
  return <h2>📖 关于我们</h2>;
}

function Article() {
  return <h2>✍️ 文章页面</h2>;
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">首页</Link> |{' '}
        <Link to="/about">关于</Link> |{' '}
        <Link to="/article">文章</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/article" element={<Article />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

核心概念解析:

  • BrowserRouter:使用 HTML5 history API 保持 UI 与 URL 同步。通常放在组件树的最外层。
  • Routes:包裹所有 Route,负责匹配当前 URL 并渲染第一个匹配的组件。
  • Route:定义路径与组件的映射关系,path 为路径,element 为要渲染的组件。
  • Link:声明式导航组件,代替 <a> 标签,避免页面刷新。
3. 导航高亮

使用 NavLink 替代 Link,它可以自动添加 active 类名:

import { NavLink } from 'react-router-dom';

<NavLink to="/" className={({ isActive }) => isActive ? 'active-link' : ''}>
  首页
</NavLink>

二、进阶篇:复杂功能详解

1. 嵌套路由

嵌套路由允许你在父组件中定义子路由,非常适合带有侧边栏或子页面的布局。

Layout.jsx

import { Outlet, Link } from 'react-router-dom';

function BlogLayout() {
  return (
    <div>
      <header>博客头部</header>
      <aside>
        <ul>
          <li><Link to="react">React 教程</Link></li>
          <li><Link to="vue">Vue 教程</Link></li>
        </ul>
      </aside>
      <main>
        <Outlet /> {/* 子路由组件会渲染在这里 */}
      </main>
      <footer>博客尾部</footer>
    </div>
  );
}

路由配置:

<Routes>
  <Route path="/blog" element={<BlogLayout />}>
    <Route path="react" element={<ReactPost />} />
    <Route path="vue" element={<VuePost />} />
    {/* 默认子路由 */}
    <Route index element={<div>请选择一篇文章</div>} />
  </Route>
</Routes>
  • Outlet 是占位符,父路由组件用它来渲染匹配的子路由。
  • index 路由:当父路由路径被精确匹配时(如 /blog),渲染该组件。
2. 动态路由与参数

动态路径通常用于详情页,例如 /article/123

<Route path="/article/:id" element={<ArticleDetail />} />

在组件中获取参数:

import { useParams } from 'react-router-dom';

function ArticleDetail() {
  const { id } = useParams();
  // 或者使用 useParams() 获取所有参数

  return <div>当前文章 ID:{id}</div>;
}
3. 查询参数(Query String)

URL 形如 /search?keyword=react&page=2

import { useSearchParams } from 'react-router-dom';

function Search() {
  const [searchParams, setSearchParams] = useSearchParams();
  const keyword = searchParams.get('keyword') || '';
  const page = searchParams.get('page') || 1;

  const updateKeyword = (newKeyword) => {
    setSearchParams({ keyword: newKeyword, page: 1 });
  };

  return (
    <div>
      <input 
        value={keyword} 
        onChange={(e) => updateKeyword(e.target.value)} 
      />
      <p>当前搜索词:{keyword},第 {page} 页</p>
    </div>
  );
}
4. 重定向与导航
使用 Navigate 组件声明式重定向
<Route path="/old-about" element={<Navigate to="/about" replace />} />

replace 属性表示替换历史记录,而不是新增一条。

使用 useNavigate 进行命令式跳转
import { useNavigate } from 'react-router-dom';

function Login() {
  const navigate = useNavigate();

  const handleLogin = () => {
    // 登录逻辑...
    navigate('/dashboard', { replace: true });
    // 后退一步
    navigate(-1);
  };

  return <button onClick={handleLogin}>登录</button>;
}
5. 路由守卫(权限控制)

没有内置的路由守卫,但可以通过封装 Route 或创建自定义组件实现。

方案一:包装组件

function PrivateRoute({ children }) {
  const isLoggedIn = localStorage.getItem('token');
  return isLoggedIn ? children : <Navigate to="/login" />;
}

// 使用
<Route 
  path="/admin" 
  element={
    <PrivateRoute>
      <AdminPanel />
    </PrivateRoute>
  } 
/>

方案二:布局式守卫(更灵活)

function AuthGuard() {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return <Outlet />;
}

// 路由配置
<Route element={<AuthGuard />}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/settings" element={<Settings />} />
</Route>

登录后可获取 state.from 跳回原页面:

const location = useLocation();
const from = location.state?.from?.pathname || '/';
navigate(from, { replace: true });
6. 懒加载与代码分割

配合 React.lazySuspense 实现按需加载,减少首屏体积。

import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}
7. 滚动复位

当路由切换时,往往希望页面滚动到顶部。React Router 提供了 ScrollRestoration 组件(实验性)或手动实现:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function ScrollToTop() {
  const { pathname } = useLocation();
  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);
  return null;
}

// 在 App 中,<BrowserRouter> 内部添加 <ScrollToTop />
8. 路由配置集中管理

对于中大型应用,推荐使用 useRoutes 钩子实现配置式路由。

routes.js

import { Navigate } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Dashboard from './pages/Dashboard';
import AuthGuard from './components/AuthGuard';

const routes = [
  { path: '/', element: <Home /> },
  { path: '/about', element: <About /> },
  {
    path: '/dashboard',
    element: <AuthGuard />,
    children: [
      { index: true, element: <Dashboard /> },
      { path: 'settings', element: <Settings /> }
    ]
  },
  { path: '*', element: <Navigate to="/" /> } // 404 重定向
];

export default routes;

App.js

import { BrowserRouter, useRoutes } from 'react-router-dom';
import routes from './routes';

function AppRoutes() {
  return useRoutes(routes);
}

function App() {
  return (
    <BrowserRouter>
      <AppRoutes />
    </BrowserRouter>
  );
}
9. 与 Redux / 状态管理集成

你可以像普通 React 组件一样使用路由钩子。例如在 Redux 的 action 或 thunk 中获取当前路径:

// 在 thunk 中
export const fetchUser = () => (dispatch, getState) => {
  const { pathname } = getState().router.location; // 需要 connected-react-router
  // 或者直接在组件中传参
};

更简单的做法:将路由钩子放在组件中,通过 props 传递给业务逻辑。

10. 处理 404 页面

在所有路由规则之后添加一个没有 pathRoute(或 path="*"):

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>

三、常见问题与最佳实践

Q1: BrowserRouter 和 HashRouter 怎么选?
  • BrowserRouter:需要服务器配置(将所有请求指向 index.html),适合生产环境。
  • HashRouter:URL 中会包含 #,不需要服务器额外配置,适合静态托管或演示。
Q2: 路由组件如何接收自定义 props?
<Route path="/user" element={<User profile={userProfile} />} />
Q3: 如何获取上一个路径?

React Router v6 没有直接提供,可以借助 useLocationuseRef 自行记录。

const location = useLocation();
const prevLocationRef = useRef(location);

useEffect(() => {
  prevLocationRef.current = location;
}, [location]);

const prevLocation = prevLocationRef.current;
Q4: 多个 Routes 组件可以共存吗?

可以。多个 Routes 会独立匹配,适合模态框中的独立路由系统或微前端场景。

四、总结

本文涵盖了 React Router 从基础到进阶的全部核心内容:

知识点应用场景
BrowserRouter, Routes, Route基础路由配置
Link, NavLink声明式导航
useParams, useSearchParams获取路由参数和查询字符串
Navigate, useNavigate重定向与编程式导航
嵌套路由 + Outlet页面布局复用
路由守卫权限控制、未登录拦截
React.lazy + Suspense代码分割优化性能
useRoutes集中式路由管理

掌握这些技巧后,你已经能够构建任何复杂度的 React 单页应用。路由是应用的骨架,清晰的架构会让项目长期维护更加轻松。