React 快速入门

215 阅读7分钟

引言

本文章主要介绍 React 开发中所涉及的常用概念与知识点,初学 React 并使用其开发应用时可以方便的通过该文章找到相关介绍,本文也不会过多深入介绍细节,主打快速上手开发,先囫囵吞枣快速掌握基础进入实战,再细嚼慢咽深入理解细节剖析原理。

JavaScript ES6+ 简介(必备基础)

ECMAScript 6.0 是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。 ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。

常量(const) 和块级作用域变量(let)

// ES5 - 没有块级作用域,常量概念不明确
var a = 1; // 可以重新赋值
if (true) {
  var b = 2; // 全局可见
}
console.log(a); // 1
console.log(b); // 2

// ES6+
const a = 1; // 常量,不能重新赋值
if (true) {
  let b = 2; // 块级作用域变量,在if语句块外部不可见
}
console.log(a); // 1
console.log(b); // undefined

箭头函数

// ES5
var add = function(a, b) {
  return a + b;
};

// ES6+
const add = (a, b) => a + b;

模板字符串

// ES5
var name = "John";
console.log("Hello, " + name);

// ES6+
let name = "John";
console.log(`Hello, ${name}`);

解构赋值

// ES5
var obj = { first: 'John', last: 'Doe' };
var first = obj.first;
var last = obj.last;

var date = ['2024-1-29 18:31:29', '2024-1-29 18:31:32'];
var start = data[0];
var end= data[1];


// ES6+
const obj = { first: 'John', last: 'Doe' };
const { first, last } = obj;

const date = ['2024-1-29 18:31:29', '2024-1-29 18:31:32']
const [start, end] = date;

类(class)

// ES5 - 使用构造函数实现类
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
}

// ES6+
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log('Hello, I am ' + this.name);
  }
}

模块系统(export/import)

// person.js
export default class Person {
  // ...
}

// main.js
import Person from './person.js';

在 ES6 之前,JavaScript并没有内置的模块系统。为了实现代码复用和模块化管理,开发者不得不依赖第三方库或工具来处理模块导入导出。 CommonJS(Node.js)

// 通过require()函数来导入其他模块,并使用module.exports或exports对象来导出模块内容。

// 导出模块内容 (example.js)
var PI = 3.14159;
function calculateArea(radius) {
  return PI * radius ** 2;
}

// 将函数和变量直接赋值给 exports 或 module.exports
exports.PI = PI;
exports.calculateArea = calculateArea;

// 或者只导出一个默认对象
module.exports = {
  PI,
  calculateArea,
};

// 导入模块 (main.js)
var exampleModule = require('./example.js');

console.log(exampleModule.PI);
console.log(exampleModule.calculateArea(5));

AMD (Asynchronous Module Definition - RequireJS)

// 导出模块内容 (example.js)
define(function () {
  var PI = 3.14159;
  function calculateArea(radius) {
    return PI * radius ** 2;
  }

  // 返回要导出的对象或函数
  return {
    PI: PI,
    calculateArea: calculateArea,
  };
});

// 导入模块 (main.js)
require(['./example'], function(exampleModule) {
  console.log(exampleModule.PI);
  console.log(exampleModule.calculateArea(5));
});

异步编程 和 Promise

为什么要使用异步编程 ?

JS是⼀⻔单线程的语⾔,因为它运⾏在浏览器的渲染主线程中,⽽每个页面的渲染主线程只有⼀个,同时渲染主线程还需要处理很多其他任务,例如解析HTML/CSS、计算样式、处理图层、每秒绘制60次页面等等,如果渲染主线程被阻塞,浏览器将会卡死。

浏览器使用异步来解决代码在执⾏过程中⼀些⽆法⽴即处理的任务,⽐如:

  • 需要通过⽹络通信完成后再执行操作,例如:XHR、Fetch
  • 使用计时器延迟执行或定时执行的操作 例如: setTimeout 、 setInterval

Promise

Promise是ES6引入的一个重要特性,它提供了一种处理异步操作的标准机制。Promise对象代表了一个异步操作的结果,它可以处于pending(进行中)、fulfilled(已成功)或rejected(已失败)状态之一。

// 创建一个Promise
function fetchData(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function () {
      if (xhr.status === 200) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.statusText));
      }
    };
    xhr.onerror = function () {
      reject(new Error(xhr.statusText));
    };
    xhr.send();
  });
}

// 使用Promise进行异步操作
fetchData('https://api.example.com/data')
  .then(responseText => {
    const data = JSON.parse(responseText);
    console.log(data);
  })
  .catch(error => {
    console.error('Error fetching data:', error);
  });

React 简介

React 起源于 Facebook,是该公司为了解决在开发用户界面时遇到的问题而创建的。2011年,Jordan Walke 开始了 React 的初步工作,并于2013年5月开源。最初,React 是作为一个JavaScript库设计的,专注于构建用户界面(UI)层,特别是在处理大型单页面应用时的数据动态变化。

  • 高效性能:得益于虚拟DOM和高效的更新机制,React可以确保即便在大规模应用程序中也能实现快速且流畅的用户交互体验。
  • 清晰架构:组件化的设计模式使得代码组织有序,易于维护和扩展。
  • 灵活性和可组合性:React遵循“组合优于继承”的原则,组件可以任意嵌套和复用,形成了高度灵活的组件体系。
  • 强大的社区支持:Facebook作为背后的大公司持续投入和支持,加上广泛的社区贡献,让React始终保持活跃且稳定更新,拥有丰富的教程、插件和解决方案。
  • 跨平台能力:React不仅用于Web开发,还被用于原生移动应用(React Native)和桌面应用(Electron)的开发,进一步拓宽了其适用范围。

React 核心概念

组件(Components)

  • React 最重要的思想是组件化开发。组件是UI中可重用、独立和可组合的部分,负责渲染一部分HTML结构。组件可以接收输入数据(props)并根据这些数据决定如何呈现输出。

JSX

  • JSX 是一种在JavaScript中编写类似XML语法的扩展,允许开发者在React中更直观地描述用户界面。Babel编译器会将JSX转换为React.createElement函数调用,从而创建React元素对象。

Props(属性)

  • Props 是从父组件向子组件传递数据的一种方式。它们是只读的,并且在组件生命周期内不会改变。子组件通过props来获取配置信息或者父组件的数据。

State(状态)

  • State 是组件内部管理的可变数据,用来表示组件自身的状态。当state发生变化时,组件会重新渲染以反映新的状态。组件可以通过this.setState()方法更新其状态,并触发视图的更新。

虚拟DOM(Virtual DOM)

  • React 使用虚拟DOM来优化实际DOM操作。每当组件的状态或props发生变化时,React都会重新计算整个组件树,生成一个新的虚拟DOM树并与上一次的树进行比较,找出最小化的差异后,再高效地更新实际的DOM。

生命周期方法

  • React 组件有多个不同阶段的生命周期方法,如componentDidMount、componentDidUpdate、componentWillUnmount等,它们分别对应组件的不同生命时刻,开发者可以在这些方法里执行特定的操作,例如初始化数据请求、添加订阅、清理资源等。

Hooks

  • 自React 16.8版本起引入了Hooks特性,如useState、useEffect、useContext等,使得无需使用类即可实现状态管理和生命周期逻辑,极大地简化了无状态函数组件的复杂性。

Context API

  • Context API 提供了一种跨多层级组件共享状态的方式,避免了“prop drilling”问题,让组件中的状态更容易在整个应用范围内被访问和修改。 :::info Prop drilling(属性传递钻取)是React中用来描述在组件层级之间通过 props 逐层向下传递数据的一种现象,随着组件层级的加深,开发者不得不在多个中间组件通过props一级级向下传递,即使这些中间组件本身并不关心传递的数据。 :::

React Class

React Class 组件是 React 中最早且至今仍然广泛使用的组件形式之一,它是基于 ES6 的类(Class)语法来创建有状态和生命周期方法的组件。在 React 16.8 版本之前,如果需要组件拥有自己的状态、生命周期钩子函数以及通过 this 关键字访问的方法,必须使用 Class 组件。

状态管理(State)

  • Class组件能够拥有内部状态,通过this.state属性存储状态,并通过this.setState()方法更新状态。状态的改变会触发组件重新渲染,从而更新界面。
jsxclass MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

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

  // state 改变 render 则会重新执行
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    );
  }
}

生命周期方法

  • Class组件有多个不同的生命周期阶段,并提供了相应的钩子函数以供开发者在特定阶段执行代码:
    • constructor: 构造函数,在创建组件实例时调用,用于初始化state和绑定事件处理器等操作。
    • componentDidMount: 组件首次挂载到DOM后立即调用,通常用于网络请求、订阅数据或添加定时器等操作。
    • componentDidUpdate: 组件完成更新后调用,可以在这里对比新的props和state与旧的进行差异处理。
    • componentWillUnmount: 组件即将卸载并从DOM中移除之前调用,用于清理工作,如取消网络请求、清除定时器或解绑事件监听器等。
    • 其他生命周期方法还包括shouldComponentUpdate, getDerivedStateFromProps, getSnapshotBeforeUpdate等。

Props

  • 类组件通过this.props访问父组件传递的属性,这些属性是只读的,并且在组件生命周期内保持不变,除非父组件再次渲染并传递了新的props。

方法绑定

  • 在JavaScript中,类的方法默认不会自动绑定到类的实例上,这意味着如果直接在事件处理器或其他地方引用this,它并不会指向当前组件实例。
  • 在 Class 组件中定义的方法如果需要用于事件处理或其他地方使用this引用当前组件实例,通常需要在构造函数中手动绑定,或者使用箭头函数来避免this丢失上下文的问题。

继承

  • Class 组件支持JavaScript原生的类继承机制,允许一个组件扩展另一个组件的功能。
  • React 有十分强大的组合模式,官方推荐使用组合而非继承来实现组件间的代码重用。

一个基本的 React Class 组件结构

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    // 如下这种操作都是不符合规范的!!!
    this.state.count = 1;
    this.props.yourName = '李四';
  }

  // state 改变 render 则会重新执行
  render() {
    return (
      <button onClick={this.handleClick}>Click</button>
    );
  }
}
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };

    // 手动绑定方法
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 在这个方法内部,this现在会正确地指向MyComponent实例
    this.setState(prevState => ({ count: prevState.count + 1 }));
  }

  handleClickArrow = () => {
    // 箭头函数自动绑定了this
    this.setState(prevState => ({ count: prevState.count + 1 }));
  }

  render() {
    return <>
      <button onClick={this.handleClick}>Increment</button>
      <button onClick={this.handleClickArrow}>Increment Arrow</button>
    </>;
  }
}
import React from 'react';

class MyComponent extends React.Component {
  // 构造函数,在组件实例化时调用,用于设置初始状态或绑定方法
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };

    // 如果需要在方法中使用到 this,并确保上下文正确,可以在这里进行方法绑定
    this.handleClick = this.handleClick.bind(this);
  }

  // 生命周期方法示例
  componentDidMount() {
    // 组件挂载到DOM后执行
    // 通常用于网络请求、订阅或添加事件监听器等操作
  }

  componentDidUpdate(prevProps, prevState) {
    // 组件更新后执行,可对比前一次的状态和props来决定是否做某些操作
  }

  componentWillUnmount() {
    // 组件卸载前执行,用于清理工作如取消网络请求、移除事件监听器等
  }

  handleClick() {
    // 点击事件或其他自定义方法
    this.setState((prevState) => ({
      count: prevState.count + 1,
    }));
  }

  // 必须实现的方法
  render() {
    const { somePropFromParent } = this.props;
    const { count} = this.state;

    return (
      <div onClick={this.handleClick}>
        Hello, {somePropFromParent}! The state is: {count}
      </div>
    );
  }
}

// 使用组件
<MyComponent somePropFromParent="World" />

React Hooks

React Hooks 是 React 16.8 版本引入的一个重大特性,它允许开发者在函数组件中使用状态(state)和生命周期方法,从而极大地简化了无类组件的开发,并提升了代码的可复用性和可维护性。

状态管理 (useState)

useState 钩子是用于在函数组件内添加简单状态的基础钩子。它接受一个初始值作为参数,并返回一个包含当前状态值及其更新函数的状态对。例如:

import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

在这里,count 是状态变量,setCount 是用于更新 count 的函数。

useEffect (代替生命周期)

useEffect 钩子替代了类组件中的生命周期方法,如 componentDidMount, componentDidUpdate, 和 componentWillUnmount。它接收两个参数:一个回调函数和一个依赖数组(可选)。回调函数会在组件渲染后执行,并且可以根据依赖数组的变化来决定是否再次执行。

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;

    // 清理函数可以用来取消订阅、定时器等
    return () => {
      document.title = 'React App';
    };
  }, [count]); // 当count变化时重新运行此效应

  return (
    // ...
  );
}

Props

在React Hooks中,组件依然可以通过函数组件的参数来接收props。与Class组件一样,函数组件可以直接访问传入的props,并根据这些props进行渲染和逻辑处理。

function MyComponent(props) {
  const { name, age } = props;
  return <div>Hello, {name}! You are {age} years old.</div>;
}

// 使用时
<MyComponent name="John" age={30} />


// 默认 Props 值
function MyComponent({ name = 'Guest', age = 0 }) {
  // 当 name 未传递时 name 为 Guest, age 为 0。
  return <div>Hello, {name}! You are {age} years old.</div>;
}

// 使用时
<MyComponent />

useContext

useContext 钩子使得函数组件能够消费上下文(Context API)中的数据,无需通过组件树逐层传递props。

const MyContext = React.createContext(defaultValue);

function App() {
  const [state, setState] = useState(someInitialState);

  return (
    <MyContext.Provider value={{ state, setState }}>
      {/* 在这里放置需要使用 MyContext 的子组件 */}
      <DeepComponent/>
    </MyContext.Provider>
  );
}

// 假设这是一个被嵌套很深的组件
function DeepComponent() {

  // 通过 Context 可以直接拿到最顶层的 state
  const contextValue = useContext(MyContext);

  return (
    <div>
      {contextValue?.state}
    </div>
  )
}

useReducer

useReducer 钩子与 useState 类似,但更适合管理更复杂的逻辑和多个相关的状态项。它接收一个reducer函数和一个初始状态,返回当前状态和 dispatch 函数。

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}

自定义 Hook

除了上述内置钩子外,React 还鼓励开发者创建自己的自定义 Hook,这些 Hook 可以封装可重用的逻辑,如数据获取、订阅服务或任何具有副作用的操作。

import { useState, useEffect } from 'react';

function useUserDetails(userId) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
    }

    fetchUser();
  }, [userId]);

  return user;
}

以上仅是部分主要的 React Hooks,还有其他诸如 useCallback, useMemo, useRef, useImperativeHandle 等,它们分别对应不同的应用场景,共同构建了一个强大的无状态组件功能集合。

一个基本的 React Hook 组件

import React, { useState, useEffect } from 'react';

const MyComponent = (props) => {
  // 使用 useState 初始化状态
  const [count, setCount] = useState(initialState);

  // useEffect 可以替代 componentDidMount、componentDidUpdate 和 componentWillUnmount
  useEffect(() => {
    // 类似 componentDidMount
    // 在组件挂载后执行,通常用于网络请求、订阅或添加事件监听器等操作

    return () => {
      // 类似 componentWillUnmount
      // 组件卸载前执行,用于清理工作如取消网络请求、移除事件监听器等
    };
  }, []); // 如果没有依赖项数组([]),则仅在挂载时执行一次

  useEffect(() => {
    // 类似 componentDidUpdate
    // 当 count 或 somePropFromParent 变化时执行
    // 注意:这里可以根据实际需要决定是否包含 somePropFromParent
  }, [count, props.somePropFromParent]);

  const handleClick = () => {
    // 更新状态的逻辑
    setCount(prevState => prevState + 1);
  };

  return (
    <div onClick={handleClick}>
      Hello, {props.somePropFromParent}! The state is: {count}
    </div>
  );
};

// 使用组件
<MyComponent somePropFromParent="World" />

React 组件设计原则

单一职责原则 (Single Responsibility Principle, SRP)

每个组件都应该专注于一个特定的任务,只处理与其自身视图和逻辑相关的数据和行为。避免让组件承担过多职责,这有助于提高代码的可读性和复用性。

容器组件与展示组件分离 (Container vs Presentational Components)

  • 展示组件(Presentational Components):专注于UI呈现,不包含业务逻辑或直接的数据访问,通过props接收数据并渲染视图。
  • 容器组件(Container Components):负责管理数据获取、状态管理和事件处理等逻辑,并将数据以props的形式传递给展示组件。

组件组合而非继承 (Composition over Inheritance)

React鼓励通过组件嵌套的方式重用代码,而不是通过类继承。通过组合不同组件,可以构建出复杂的应用程序界面,同时保持每个组件的独立性和灵活性。

提升组件可预测性

  • 尽可能使组件具有确定性的输出,根据其props总是能生成一致的UI。
  • 避免直接操作DOM,而是通过改变state和props来触发UI更新。

最小化副作用

例如使用useEffect Hook进行副作用操作(如订阅、定时任务、DOM修改等),并确保它们遵循一定的清理机制。

合理管理组件状态

  • 将状态尽可能上移至最接近共享该状态的组件层级,减少不必要的子组件间通信。
  • 对于跨多个组件共享的状态,可以考虑使用Context API或Redux等状态管理库。

工具链及环境配置

安装 Node.js 与包管理工具

  • Node.js 官网下载进行安装,Node.js带有包管理工具npm (Node Package Manager)
  • 配置包管理工具镜像源
// 查看当前使用源
npm config get registry

// 切换镜像源
npm config set registry https://registry.npmmirror.com
  • 安装 yarn
    • Yarn 通过使用更高效的缓存机制和并行下载来提高安装速度。当项目需要多次安装相同的依赖时,Yarn 能够从本地缓存中读取,避免重复下载。
    • Yarn 使用确定性安装,确保在不同的机器或环境中安装同一个项目时得到完全一致的依赖树。
npm install -g yarn

// 查看当前使用源
yarn config get registry

// 切换镜像源
yarn config set registry https://registry.npmmirror.com

创建你自己的 React 项目

  • 通过 npm 安装 React 应用脚手架工具 create-react-app
// 全局安装 create-react-app 
npm install -g create-react-app

// 如下名称会创建一个名称为 my-app 的项目文件
npx create-react-app my-app

启动 React 开发项目

  • 进入项目根目录后执行运行命令
cd my-app

// 启用项目 或者是 yarn start
npm start

// 访问地址 (默认) 即可看到如下页面
http://localhost:3000
  • React 项目初始页

构建与部署项目

  • 在项目根目录下执行打包命令 npm run build
// 执行打包命令
npm run build
  • 打包成功 !

打包成功后会出现build目录,该目录下则是整个React项目压缩打包后的文件,将该目录文件部署在你的服务器中,即可完成部署。image.png

实战环节

完成一个简易的 TodoList

import React, { useState } from "react";

const TodoList = () => {
  // 使用useState管理状态
  const [todos, setTodos] = useState([]);

  // 添加新的待办事项
  const addTodo = (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    setTodos([...todos, newTodo]);
  };

  // 更新某个待办事项的状态
  const toggleTodo = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
               )
    );
  };

  // 删除待办事项
  const removeTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <div className="todo-list">
      {/* 输入框用于添加新的待办事项 */}
      <input
        type="text"
        placeholder="请输入待办事项"
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            const text = e.target.value.trim();
            if (text) {
              addTodo(text);
              e.target.value = "";
            }
          }
        }}
        />

      {/* 显示当前所有的待办事项 */}
      <ul>
        {todos.map((todo) => {
      return (
        <TodoItem
          itemData={todo}
          toggleTodo={toggleTodo}
          removeTodo={removeTodo}
          />
      );
    })}
      </ul>
    </div>
  );
};

export default TodoList;

/** 任务项的组件 */
function TodoItem(props) {
  const { itemData, toggleTodo, removeTodo } = props;
  return (
    <li key={itemData.id}>
      <span
        style={{
          textDecoration: itemData.completed ? "line-through" : "none",
        }}
        >
        {itemData.text}
      </span>
      {/* 每个待办事项旁边有完成/未完成切换按钮 */}
      <button onClick={() => toggleTodo(itemData.id)}>
        {itemData.completed ? "未完成" : "完成"}
      </button>
      {/* 提供删除按钮 */}
      <button onClick={() => removeTodo(itemData.id)}>删除</button>
    </li>
  );
}