React 揭秘:从新手到高手的进阶之路

 

目录

React:前端开发新宠​

React 初相识​

什么是 React​

React 的核心特性​

1.组件化开发

2.虚拟 DOM 与 Diff 算法

单向数据流

搭建 React 开发环境

环境准备​

创建 React 项目

项目结构解析

React 基础语法与核心概念

JSX 语法​

基本语法规则​

属性与表达式​

注意事项​

React 组件

函数式组件

类组件

组件通信​

状态管理(State)​

状态的定义与初始化​

状态更新​

函数式组件中的状态(Hooks)​


React:前端开发新宠​

在当今的前端开发领域,React 已然成为了一颗耀眼的明星,备受开发者们的青睐。它由 Facebook 开发并开源,自诞生以来,便以其独特的理念和强大的功能,迅速改变了前端开发的格局 ,成为构建现代用户界面的首选工具之一。无论是大型企业级应用,还是小型创业项目,React 都展现出了卓越的适应性和强大的生命力。那么,React 究竟有何魅力,能让众多开发者为之倾心呢?接下来,就让我们一同深入探索 React 的世界,揭开它神秘的面纱。

React 初相识​

什么是 React​

React 是一个用于构建用户界面的 JavaScript 库,它采用了一种声明式的编程风格,让开发者可以用简洁直观的方式描述用户界面的结构和行为。React 的核心思想是将用户界面拆分成一个个独立的、可复用的组件,每个组件都有自己的状态和逻辑,通过组合这些组件来构建复杂的应用程序 。它由 Facebook 开发并开源,如今已经成为了前端开发领域中最受欢迎和广泛使用的库之一。​

React 的核心特性​

1.组件化开发

在 React 中,组件是构建用户界面的基本单元,就像是搭建积木一样,我们可以将一个个小的组件组合起来,构建出复杂的页面。以一个简单的按钮组件为例,我们可以这样定义:

import React from 'react';

// 定义一个Button组件
const Button = (props) => {
  return <button onClick={props.onClick}>{props.label}</button>;
};

export default Button;

在上述代码中,我们定义了一个名为 Button 的函数组件,它接受两个属性:onClick和label。onClick是按钮的点击事件处理函数,label是按钮上显示的文本。通过这种方式,我们将按钮的功能和样式封装在了一个组件中,在其他地方使用这个按钮时,只需要传入相应的属性即可。

组件化开发的优势在于:​

1.代码复用:减少重复代码,提高开发效率。比如,我们在多个页面中都需要使用一个按钮,只需要定义一次Button组件,然后在不同的页面中引入即可。​

2.易于维护:每个组件都有自己独立的逻辑和状态,当某个组件出现问题时,我们可以快速定位和修复,而不会影响到其他组件。​

3.可扩展性:随着应用程序的不断发展,我们可以方便地添加新的组件,或者对现有组件进行修改和扩展。

2.虚拟 DOM 与 Diff 算法

虚拟 DOM(Virtual DOM)是 React 的另一个重要特性。在传统的前端开发中,当数据发生变化时,我们需要直接操作真实的 DOM,这会导致浏览器频繁地重新渲染页面,从而影响性能。而 React 引入了虚拟 DOM 的概念,它在内存中维护一个与真实 DOM 对应的虚拟树,当数据发生变化时,React 会先计算虚拟 DOM 的差异,然后再将这些差异一次性更新到真实 DOM 上,从而大大减少了对真实 DOM 的直接操作,提高了应用的性能。​

Diff 算法是 React 用于计算虚拟 DOM 差异的算法。它的核心思想是通过对比新旧虚拟 DOM 树,找出最小的变化集,然后只更新这些变化的部分。下面是一个简单的例子,展示了 Diff 算法的工作原理:

import React, { useState } from'react';

const List = () => {
  const [items, setItems] = useState([1, 2, 3]);

  const addItem = () => {
    setItems([...items, items.length + 1]);
  };

  return (
    <div>
      <ul>
        {items.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
      <button onClick={addItem}>添加项目</button>
    </div>
  );
};

export default List;

在上述代码中,我们定义了一个List组件,它包含一个列表和一个按钮。当点击按钮时,会向列表中添加一个新的项目。在这个过程中,React 会使用 Diff 算法来计算虚拟 DOM 的差异,只更新新增的列表项,而不会重新渲染整个列表,从而提高了页面的渲染效率。

单向数据流

单向数据流是 React 的数据管理方式。在 React 中,数据从父组件流向子组件,子组件只能接收父组件传递过来的数据,而不能直接修改这些数据。如果子组件需要修改数据,需要通过回调函数通知父组件,由父组件来更新数据,然后再将新的数据传递给子组件。这种数据流动方式使得数据的流向更加清晰,易于调试和维护。​

以下是一个父子组件数据传递的示例:

import React, { useState } from'react';

// 子组件
const Child = ({ count, increment }) => {
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={increment}>增加计数</button>
    </div>
  );
};

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <Child count={count} increment={incrementCount} />
    </div>
  );
};

export default Parent;

在上述代码中,Parent组件通过count和incrementCount两个属性将数据和回调函数传递给Child组件。Child组件可以通过count属性读取数据,通过increment回调函数通知父组件更新数据。这种单向数据流的设计使得组件之间的依赖关系更加明确,降低了组件之间的耦合度。

搭建 React 开发环境

环境准备​

在开始 React 开发之前,我们需要确保开发环境中安装了 Node.js 和 npm(Node Package Manager)。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时,它允许我们在服务器端运行 JavaScript 代码 。npm 是 Node.js 的包管理器,用于安装和管理项目所需的依赖包。​

你可以通过以下步骤安装 Node.js 和 npm:​

1.下载安装包:访问 Node.js 官方网站(Node.js — Run JavaScript Everywhere),根据你的操作系统下载对应的安装包。​

2.安装 Node.js:运行下载的安装包,按照安装向导的提示完成安装。在安装过程中,npm 会自动被安装。​

3.验证安装:打开命令行工具,输入node -v和npm -v,如果显示出版本号,则说明安装成功。例如:

$ node -v
v18.12.1
$ npm -v
8.19.2

Node.js 和 npm 对于 React 开发至关重要。Node.js 提供了运行 React 应用所需的 JavaScript 环境,而 npm 则用于安装 React 及其相关的依赖包,如 React Router、Redux 等。通过 npm,我们可以轻松地管理项目的依赖关系,确保项目在不同环境中能够稳定运行。

创建 React 项目

为了快速创建一个 React 项目,我们可以使用 Create React App,它是一个官方提供的脚手架工具,用于快速搭建 React 应用的基础架构。​

使用 Create React App 创建新项目的步骤如下:​

1.打开命令行工具:在你希望创建项目的目录下,打开命令行工具(如 Windows 下的命令提示符或 PowerShell,Mac 下的终端)。​

2.运行创建项目命令:输入以下命令来创建一个名为my - react - app的项目(你可以将my - react - app替换为你想要的项目名称):

npx create-react-app my-react-app

npx是 npm 5.2 及以上版本自带的命令,用于运行 npm 包中的可执行文件。这里,它会自动下载并运行create - react - app,创建一个新的 React 项目。​

3. 等待项目创建完成:命令执行后,create - react - app会自动下载并安装项目所需的依赖包,这个过程可能需要一些时间,具体取决于你的网络速度。​

4. 进入项目目录:项目创建完成后,进入项目目录:

cd my-react-app

5.启动项目:在项目目录下,运行以下命令启动开发服务器:

npm start

执行npm start后,开发服务器会启动,并自动打开浏览器,访问http://localhost:3000,你将看到一个默认的 React 应用界面,这表明你的 React 项目已经成功创建并运行。

项目结构解析

当我们使用 Create React App 创建一个 React 项目后,项目的目录结构如下:

my-react-app
├── node_modules
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   ├── reportWebVitals.js
│   └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
└── README.md

下面我们来详细解释各文件和文件夹的作用:​

  • node_modules:该目录包含了项目的所有依赖包,由 npm 自动管理。在项目中,我们一般不需要手动修改这个目录。​
  • public:该目录存放静态资源,如 HTML 文件、图标等。其中,index.html是 React 应用的入口 HTML 文件,它提供了一个基本的 HTML 结构,React 应用最终会被渲染到这个文件中的root节点上。favicon.ico是网站的图标,manifest.json用于配置渐进式 Web 应用(PWA)的相关信息,robots.txt用于控制搜索引擎爬虫的行为。​
  • src:这是我们开发的主要目录,存放所有的 React 组件、样式和其他资源。其中,index.js是项目的入口文件,它负责渲染 React 应用的根组件到index.html中的root节点上。App.js是应用的主要组件,通常包含应用的主要逻辑和布局。App.css用于定义App.js组件的样式。index.css是全局样式文件。App.test.js用于编写App.js组件的测试用例,reportWebVitals.js用于报告应用的性能指标,setupTests.js用于配置测试环境。​
  • .gitignore:该文件用于指定哪些文件和目录不需要被 Git 版本控制系统追踪,例如node_modules目录通常会被忽略,以避免将庞大的依赖包上传到版本库中。​
  • package.json:这是项目的配置文件,包含了项目的元数据、依赖包信息、脚本命令等。例如,我们可以在scripts字段中定义各种脚本命令,如start用于启动开发服务器,build用于打包生产环境代码等。dependencies字段列出了项目运行时所依赖的包,devDependencies字段列出了项目开发时所依赖的包。​
  • package - lock.json:该文件用于锁定依赖包的版本,确保在不同环境中安装的依赖包版本一致,避免因版本差异导致的兼容性问题。​
  • README.md:这是项目的说明文档,用于记录项目的基本信息、使用方法、安装步骤等,方便其他开发者了解和使用项目。

React 基础语法与核心概念

JSX 语法​

基本语法规则​

JSX(JavaScript XML)是一种 JavaScript 的语法扩展,它允许我们在 JavaScript 代码中直接编写类似 HTML 的代码。React 使用 JSX 来描述用户界面,它可以很好地描述 UI 应该呈现出的交互本质形式,并且能和 JavaScript 完美融合。例如:

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

在上述代码中,我们使用 JSX 创建了一个<h1>元素,它看起来就像普通的 HTML 标签,但实际上它是一个 JavaScript 表达式。在编译时,JSX 会被转换为React.createElement函数调用,上述代码等价于:

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

这种转换使得我们可以在 JavaScript 中更直观地构建用户界面,同时利用 JavaScript 的强大功能来动态生成和操作 UI。​

属性与表达式​

在 JSX 中,我们可以像在 HTML 中一样设置元素的属性。例如,为<img>标签设置src和alt属性:

const imgUrl = 'https://example.com/image.jpg';
const imgAlt = '示例图片';

const imageElement = <img src={imgUrl} alt={imgAlt} />;

注意,当属性值是一个 JavaScript 表达式时,我们需要使用大括号{}将其包裹起来。这里的src={imgUrl}和alt={imgAlt},imgUrl和imgAlt都是 JavaScript 变量,它们的值会在运行时被计算并赋值给相应的属性。​

除了属性,我们还可以在 JSX 中使用 JavaScript 表达式来动态生成内容。例如,使用三元运算符根据条件渲染不同的内容:

const isLoggedIn = true;

const greetingElement = (
  <div>
    {isLoggedIn ? <p>欢迎回来!</p> : <p>请登录。</p>}
  </div>
);

在上述代码中,根据isLoggedIn的值,greetingElement会渲染不同的<p>元素。​

注意事项​

使用 JSX 时,有一些语法要求需要注意:​

1.必须有一个根元素:JSX 中所有的元素必须包含在一个外层元素中,例如<div>、<section>等。如果不需要实际的 DOM 元素包裹,可以使用空标签<></>(也称为 React Fragment)。例如:

// 正确,使用div作为根元素
const validElement1 = (
  <div>
    <p>段落1</p>
    <p>段落2</p>
  </div>
);

// 正确,使用React Fragment作为根元素
const validElement2 = (
  <>
    <p>段落1</p>
    <p>段落2</p>
  </>
);

// 错误,没有根元素
const invalidElement = (
  <p>段落1</p>
  <p>段落2</p>
);

2.标签必须闭合:在 JSX 中,所有标签都必须正确闭合,无论是单标签(如<img>、<input>)还是双标签(如<div></div>、<p></p>)。例如:

// 正确,单标签闭合
const validSingleTag = <input type="text" />;

// 错误,单标签未闭合
const invalidSingleTag = <input type="text">;

// 正确,双标签闭合
const validDoubleTag = <div><p>内容</p></div>;

// 错误,双标签未闭合
const invalidDoubleTag = <div><p>内容</p>;

3.区分大小写:JSX 中标签和属性名是区分大小写的。一般来说,HTML 标签使用小写字母,而自定义组件使用大写字母开头。例如:

// 正确,HTML标签小写
const htmlElement = <div>这是一个div</div>;

// 错误,HTML标签大写
const wrongHtmlElement = <DIV>这是错误的</DIV>;

// 定义一个自定义组件
const MyComponent = () => {
  return <p>这是一个自定义组件</p>;
};

// 正确,自定义组件大写
const customElement = <MyComponent />;

// 错误,自定义组件小写
const wrongCustomElement = <myComponent />;

React 组件

函数式组件

函数式组件是定义 React 组件的一种简洁方式,它是一个普通的 JavaScript 函数,接收props(属性)作为参数,并返回一个 React 元素。例如,我们定义一个简单的函数式组件Welcome:

import React from'react';

// 定义一个函数式组件
const Welcome = (props) => {
  return <p>欢迎,{props.name}!</p>;
};

export default Welcome;

在上述代码中,Welcome组件接收一个props对象,其中包含name属性。组件通过props.name获取传入的名字,并渲染在<p>标签中。​

使用函数式组件时,我们可以通过属性传递数据。例如,在另一个组件中使用Welcome组件:

import React from'react';
import Welcome from './Welcome';

const App = () => {
  return (
    <div>
      <Welcome name="张三" />
    </div>
  );
};

export default App;

在App组件中,我们将name属性设置为"张三",并传递给Welcome组件,Welcome组件会显示"欢迎,张三!"。

类组件

类组件是基于 ES6 类的方式定义的 React 组件,它具有自己的状态(state)和生命周期方法。类组件需要继承React.Component类,并实现render方法来返回组件的 UI。下面是一个简单的类组件示例,实现一个计数器功能:

import React, { Component } from'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    // 初始化状态
    this.state = {
      count: 0
    };
  }

  // 定义一个方法用于更新状态
  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  };

  render() {
    return (
      <div>
        <p>计数: {this.state.count}</p>
        <button onClick={this.increment}>增加计数</button>
      </div>
    );
  }
}

export default Counter;

在上述代码中:​

1.constructor方法是类的构造函数,用于初始化组件的状态和绑定方法。通过super(props)调用父类的构造函数,确保this.props正确初始化。​

2.this.state用于定义组件的状态,这里初始化count为 0。​

3.increment方法使用setState方法来更新状态,setState会触发组件的重新渲染,从而更新 UI。​

4.render方法返回组件的 UI,展示当前的计数和一个按钮,点击按钮会调用increment方法增加计数。

组件通信​

1.父子组件通信:在 React 中,父子组件通信是通过属性(props)来实现的。父组件可以将数据和方法通过 props 传递给子组件。例如:

import React, { useState } from'react';

// 子组件
const Child = ({ count, increment }) => {
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={increment}>增加计数</button>
    </div>
  );
};

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <Child count={count} increment={incrementCount} />
    </div>
  );
};

export default Parent;

在上述代码中,Parent组件通过count属性将当前的计数值传递给Child组件,通过increment属性将incrementCount方法传递给Child组件。Child组件可以通过props.count读取计数值,通过props.increment调用父组件的方法来更新计数。​

2.兄弟组件通信:兄弟组件之间不能直接通信,通常需要通过它们的共同父组件作为中介来实现通信。例如,有两个兄弟组件ComponentA和ComponentB,它们的父组件ParentComponent可以管理共享的状态,并将状态和更新状态的方法传递给两个子组件。以下是一个简单的示例:

import React, { useState } from'react';

// 组件A
const ComponentA = ({ count, increment }) => {
  return (
    <div>
      <p>组件A: {count}</p>
      <button onClick={increment}>组件A增加计数</button>
    </div>
  );
};

// 组件B
const ComponentB = ({ count, increment }) => {
  return (
    <div>
      <p>组件B: {count}</p>
      <button onClick={increment}>组件B增加计数</button>
    </div>
  );
};

// 父组件
const ParentComponent = () => {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <ComponentA count={count} increment={incrementCount} />
      <ComponentB count={count} increment={incrementCount} />
    </div>
  );
};

export default ParentComponent;

在这个示例中,ParentComponent管理着count状态,并将count和incrementCount方法传递给ComponentA和ComponentB。当ComponentA或ComponentB中的按钮被点击时,都会调用incrementCount方法,从而更新共享的count状态,两个组件的 UI 也会相应更新。​

状态管理(State)​

状态的定义与初始化​

在 React 类组件中,状态(state)是一个包含组件数据的 JavaScript 对象。状态的变化会触发组件的重新渲染,从而更新用户界面。通常在组件的构造函数中初始化状态。例如,我们创建一个简单的Toggle组件,用于切换一个开关的状态:

import React, { Component } from'react';

class Toggle extends Component {
  constructor(props) {
    super(props);
    // 初始化状态,isOn为false表示开关关闭
    this.state = {
      isOn: false
    };
  }

  render() {
    return (
      <div>
        <button>{this.state.isOn? '关闭' : '打开'}</button>
      </div>
    );
  }
}

export default Toggle;

在上述代码中,Toggle组件的state包含一个isOn属性,初始值为false。在render方法中,根据isOn的值来显示按钮上的文本。​

状态更新​

要更新组件的状态,我们使用setState方法。setState方法接受一个新的状态对象,并将其合并到当前状态中。React 会在状态更新后自动重新渲染组件。继续上面的Toggle组件示例,我们添加点击按钮切换状态的功能:

import React, { Component } from'react';

class Toggle extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isOn: false
    };
    // 绑定this,确保handleClick方法中的this指向组件实例
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 使用setState更新状态
    this.setState((prevState) => ({
      isOn:!prevState.isOn
    }));
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>
          {this.state.isOn? '关闭' : '打开'}
        </button>
      </div>
    );
  }
}

export default Toggle;

在上述代码中:​

1.handleClick方法中使用setState来更新isOn状态。这里使用了函数形式的setState,它接受一个函数参数,该函数的参数prevState是上一个状态,返回值是新的状态。这样可以确保在状态更新依赖于前一个状态时,能得到正确的结果。​

2.在constructor中,通过this.handleClick = this.handleClick.bind(this);将handleClick方法的this绑定到组件实例,因为在 React 中,事件处理函数中的this默认是未定义的,需要手动绑定。​

需要注意的是,setState是异步的,React 可能会将多个setState调用合并为单个更新,以提高性能。因此,不要依赖于setState调用后立即获取最新的状态值。例如:

// 错误的做法,无法保证获取到最新的count值
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); 

// 正确的做法,使用回调函数获取更新后的状态
this.setState({ count: this.state.count + 1 }, () => {
  console.log(this.state.count); 
}); 

函数式组件中的状态(Hooks)​

在 React 16.8 及以上版本中,函数式组件可以使用 Hooks 来管理状态。useState是一个常用的 Hook,它允许我们在函数式组件中添加状态。例如,我们将上面的Toggle组件用函数式组件和useState实现:

import React, { useState } from'react';

const Toggle = () => {
  // 使用useState定义状态,isOn初始值为false
  const [isOn, setIsOn] = useState(false);

  const handleClick = () => {
    // 使用setIsOn更新状态
    setIsOn(!isOn);
  };

  return (
    <div>
      <button onClick={handleClick}>
        {isOn? '关闭' : '打开'}
      </button>
    </div>
  );
};

export default Toggle;

在上述代码中:​

1.const [isOn, setIsOn] = useState(false);使用useState定义了一个状态变量isOn,初始值为false,并返回一个更新状态的函数setIsOn。​

2.handleClick函数中通过setIsOn(!isOn)来更新isOn状态。​

3.与类组件中的setState不同,useState的更新函数setIsOn不会自动合并新的状态,而是直接替换旧的状态。如果需要合并状态,可以使用展开运算符(...)。例如:

const [obj, setObj] = useState({ key1: 'value1' });

const updateObj = () => {
  setObj({
  ...obj,
    key2: 'value2'
  });
};