【超全】React学习笔记 中:进阶语法与原理机制

React学习笔记

React系列笔记学习

上篇笔记地址:【超全】React学习笔记 上:基础使用与脚手架

下篇笔记地址:【超全】React学习笔记 下:路由与Redux状态管理

React进阶组件概念与使用

1. React 组件进阶导读

在掌握了 React 的基础知识后,我们将进一步深入探讨 React 组件的进阶特性和技巧。这些进阶知识将帮助我们更好地理解和使用 React,为构建复杂的前端应用奠定坚实的基础。下面是本阶段学习的主要目标和相关的导读内容:

  1. Props 接收数据

Props(属性)是 React 组件之间交互的主要方式之一。通过 Props,我们可以将数据从父组件传递到子组件。理解和掌握 Props 的使用是 React 开发的基本技能。

javascript 复制代码
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(element, document.getElementById('root'));
  1. 父子组件之间的通讯

父子组件之间的通讯是 React 组件交互的基础。通常,我们通过 Props 将数据从父组件传递到子组件,通过回调函数将事件从子组件传递到父组件。

javascript 复制代码
class ParentComponent extends React.Component {
  state = { message: '' };

  handleMessage = (message) => {
    this.setState({ message });
  };

  render() {
    return (
      <div>
        <ChildComponent onMessage={this.handleMessage} />
        <div>Message from child: {this.state.message}</div>
      </div>
    );
  }
}
  1. 兄弟组件之间的通讯

在React中,兄弟组件之间的通讯是相对复杂的一部分。由于React的数据流是单向的,通常情况下,组件之间的数据是从上至下传递的。当两个组件需要共享相同的数据或状态时,通常的做法是将共享状态提升到它们共同的父组件中,然后通过props将状态传递给它们。但是,这种方法在组件树变得复杂时可能会变得很繁琐。为了解决这个问题,我们可以使用一些状态管理库,如Redux或者Context API。

通过共同父组件传递数据,这种方式是将两个兄弟组件需要共享的数据提升到它们的共同父组件的状态中,然后通过props将数据传递给这两个兄弟组件。

  1. Props 校验

为确保组件的正确使用,我们可以为组件的 Props 添加类型校验。React 提供了 propTypes 库来帮助我们实现这一目标。

javascript 复制代码
import PropTypes from 'prop-types';

class CustomComponent extends React.Component {
  // ...
}

CustomComponent.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number
};
  1. 生命周期钩子函数

生命周期钩子函数允许我们在组件的不同阶段执行特定的操作,例如在组件挂载、更新或卸载时。

javascript 复制代码
class LifecycleExample extends React.Component {
  componentDidMount() {
    // 组件挂载时执行
  }

  componentDidUpdate(prevProps, prevState) {
    // 组件更新时执行
  }

  componentWillUnmount() {
    // 组件卸载时执行
  }

  render() {
    // ...
  }
}
  1. 高阶组件 (Higher-Order Components, HOC)

高阶组件是一种用于复用组件逻辑的高级技术。通过高阶组件,我们可以将共享逻辑抽取出来,应用到其他组件上。

javascript 复制代码
function withLogging(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log(`${WrappedComponent.name} is mounted`);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

const LoggedComponent = withLogging(CustomComponent);

通过以上的导读和示例,你将会对 React 组件的进阶知识有一个基本的理解。在接下来的学习中,我们将深入探讨每一个目标,以确保你能够熟练掌握这些进阶技巧和知识。

2.组件通讯概念与操作

在React应用开发中,组件是基本的构建块,它们是独立、可复用的代码片段,可以被组合来构建复杂的用户界面。每个组件都有自己的状态和属性,而组件通讯是指在不同组件之间共享这些状态和属性的过程。组件通讯对于构建有组织、可维护和可扩展的React应用至关重要。下面我们将介绍React中几种常见的组件通讯方式。

2.1 组件通讯props概念介绍与基本使用

在React中,组件是独立且封闭的代码结构,它们不能直接访问或修改外部数据。为了实现组件之间的数据交换,React提供了props(属性)机制。通过props,组件可以接收外部传递的数据,并在内部使用这些数据。下面我们将通过示例来详细介绍props的基本用法和组件通讯的基本概念。

在React中,props(属性)是组件之间通信的主要手段,它允许我们将数据从父组件传递给子组件。每个React组件都可以接收props,并在内部使用这些props。下面我们来详细介绍组件props的特点和使用方法。

2.1.1 传递数据

在React中,可以通过为组件标签添加属性来传递数据。每个属性对应一个数据项,属性的名称就是数据项的名称,属性的值就是数据项的值。

javascript 复制代码
<Hello name="Jack" age={19} />

在上述代码中,我们为Hello组件传递了两个数据项:nameagename的值是字符串"Jack"age的值是数字19

2.1.2 接收数据

组件可以通过props对象接收外部传递的数据。函数组件和类组件的接收方式略有不同:

  • 函数组件
    函数组件可以通过参数直接接收props对象。在组件内部,可以通过props对象访问传递的数据。
javascript 复制代码
function Hello(props) {
  console.log(props);  // 输出:{ name: 'Jack', age: 19 }
  return <div>接收到的数据: {props.name}, {props.age}</div>;
}
  • 类组件
    类组件可以通过this.props对象接收传递的数据。在类组件的方法中,可以通过this.props对象访问传递的数据。
javascript 复制代码
class Hello extends React.Component {
  render() {
    console.log(this.props);  // 输出:{ name: 'Jack', age: 19 }
    return <div>接收到的数据: {this.props.name}, {this.props.age}</div>;
  }
}
2.1.3 使用示例

以下是一个完整的示例,展示了如何通过props传递和接收数据:

javascript 复制代码
// 引入React库
import React from 'react';

// 定义函数组件
function Hello(props) {
  return <div>接收到的数据: 姓名 - {props.name}, 年龄 - {props.age}</div>;
}

// 使用组件,并传递数据
const element = <Hello name="Jack" age={19} />;

// 渲染组件
ReactDOM.render(element, document.getElementById('root'));

在这个示例中,我们定义了一个Hello函数组件,并通过nameage属性向其传递了数据。Hello组件接收到数据后,将数据显示在页面上。

2.2 组件通讯props特点
  1. 多样化的数据类型 :
    • 可以传递任意类型的数据给组件,包括基本数据类型(如字符串、数字、布尔值),复杂数据类型(如对象、数组),以及函数等。
javascript 复制代码
<MyComponent stringProp="hello" numberProp={42} arrayProp={[1, 2, 3]} objectProp={{ key: 'value' }} functionProp={() => console.log('hello')} />
  1. 只读性 :
    • props是只读的,组件不能修改自己的props。尝试修改props中的值会导致错误。
javascript 复制代码
function MyComponent(props) {
  props.stringProp = "new value";  // Error: Cannot assign to read only property 'stringProp' of object
  return <div>{props.stringProp}</div>;
}
  1. 构造函数中的props :
    • 在类组件的构造函数中,应该将props传递给super(),以确保this.props在构造函数中可用。
javascript 复制代码
class Hello extends React.Component {
  constructor(props) {
    super(props);  // 将props传递给父类构造函数
    console.log(this.props);  // 输出: { age: 19 }
  }

  render() {
    return <div>接收到的数据: {this.props.age}</div>;
  }
}

// 使用组件,并传递props
ReactDOM.render(<Hello age={19} />, document.getElementById('root'));

在上述代码中,Hello组件的构造函数接收一个props参数,并将props传递给super()。这样,Hello组件的实例就可以在构造函数和其他方法中通过this.props访问传递的props。

2.3 组件通讯props总结

通过上述介绍,我们了解了React组件props的基本特点和使用方法。记住,props是只读的,不能在组件内部修改。同时,应该始终将props传递给super(),以确保this.props在类组件的构造函数和其他方法中可用。通过正确使用props,可以实现组件之间的有效通信,从而创建出功能丰富、结构清晰的React应用。

3. 组件通讯的三种方式

React 的组件模型为数据流提供了清晰的方向:父组件可以将其状态作为属性传递给它的子组件,这种单向数据流使得组件的数据传递和管理变得直接且易于理解。下面我们会依次介绍父组件传递数据给子组件、子组件传递数据给父组件和兄弟组件之间的通讯。

3.1 父组件传递数据给子组件

父组件向子组件传递数据是最基本也是最直接的组件通信方式。通过这种方式,我们可以将父组件的stateprops数据传递给子组件。

步骤:

  1. 父组件提供数据 :
    • 在父组件中定义需要传递给子组件的state数据。
javascript 复制代码
class Parent extends React.Component {
  state = { lastName : '王'}
  // ...
}
  1. 添加属性到子组件标签 :
    • 在子组件的标签上添加属性,属性的值就是要传递的数据。
javascript 复制代码
<Child name={this.state.lastName}/>
  1. 子组件接收数据 :
    • 在子组件中通过props接收父组件传递的数据。
javascript 复制代码
function Child(props){
  return <div>子组件接收到数据: {props.name}</div>;
}

完整示例:

javascript 复制代码
class Parent extends React.Component {
  state = { lastName : '王'}

  render() {
    return (
      <div>
        传递数据给子组件:
        <Child name={this.state.lastName}/>
      </div>
    );
  }
}

function Child(props){
  return <div>子组件接收到数据: {props.name}</div>;
}

ReactDOM.render(<Parent />, document.getElementById('root'));

在这个例子中,Parent组件通过<Child name={this.state.lastName}/>lastName数据传递给Child组件,Child组件通过props接收到了Parent组件传递的数据,并在组件内部显示该数据。通过父子组件的数据传递,我们可以实现组件之间的通信,将数据从一个组件传递到另一个组件,从而实现组件的复用和应用的功能拆分。

3.2 子组件传递数据给父组件

在某些情况下,我们可能需要将子组件的数据传递回父组件。这通常通过在父组件中定义一个回调函数,并将该回调函数作为props传递给子组件来实现。当子组件中的某些操作触发时,子组件调用传递给它的回调函数,并将需要传递的数据作为参数提供给该函数。

步骤:

  1. 父组件提供回调函数 :
    • 在父组件中定义一个回调函数,该函数接收一个参数,该参数是子组件需要传递给父组件的数据。
javascript 复制代码
class Parent extends React.Component {
  getMsg = (msg) => {
    console.log('Received message from child:', msg);
  }

  // ...
}
  1. 将回调函数传递给子组件 :
    • 通过props将回调函数传递给子组件。
javascript 复制代码
<Child getMsg={this.getMsg}/>
  1. 子组件调用回调函数 :
    • 在子组件中,定义一个方法来触发回调函数,并将需要传递的数据作为参数提供给回调函数。
javascript 复制代码
class Child extends React.Component {
  state = { childMsg: 'React' }

  handleClick = () => {
    this.props.getMsg(this.state.childMsg);
  }

  render() {
    return (
      <button onClick={this.handleClick}>点我,给父组件传递数据</button>
    );
  }
}

完整示例:

javascript 复制代码
class Parent extends React.Component {
  getMsg = (msg) => {
    console.log('Received message from child:', msg);
  }

  render() {
    return (
      <div>
        <Child getMsg={this.getMsg}/>
      </div>
    );
  }
}

class Child extends React.Component {
  state = { childMsg: 'React' }

  handleClick = () => {
    this.props.getMsg(this.state.childMsg);
  }

  render() {
    return (
      <button onClick={this.handleClick}>点我,给父组件传递数据</button>
    );
  }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

在这个例子中,Parent组件提供了一个getMsg回调函数,并通过props将其传递给Child组件。Child组件中定义了一个handleClick方法,该方法通过this.props.getMsg(this.state.childMsg)调用了传递给它的回调函数,并将childMsg数据作为参数传递给该函数。这样,当用户点击Child组件中的按钮时,Parent组件的getMsg方法会被调用,并接收到Child组件传递的数据。

3.3 兄弟组件之间的通讯

当多个兄弟组件需要共享某些状态时,一个常见的解决方案是将共享的状态提升到它们的最近公共父组件中。这种技术通常被称为状态提升 。公共父组件负责管理共享的状态,并通过props将状态和用于操作状态的方法传递给兄弟组件。

思想:

  • 状态提升 :将状态从子组件提升到公共父组件中,由公共父组件统一管理,并通过props将状态和操作状态的方法传递给需要它的子组件。

公共父组件职责:

  1. 提供共享状态。
  2. 提供操作共享状态的方法。

兄弟组件职责:

  1. 通过props接收共享的状态或操作状态的方法,并使用它们来实现组件的功能。

示例 :

假设我们有两个兄弟组件,分别是IncrementButtonDecrementButton,以及一个显示计数值的Display组件。我们希望IncrementButton能够增加计数值,DecrementButton能够减少计数值。

javascript 复制代码
class Counter extends React.Component {
  state = { count: 0 }

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

  decrement = () => {
    this.setState(prevState => ({ count: prevState.count - 1 }));
  }

  render() {
    return (
      <div>
        <IncrementButton increment={this.increment}/>
        <DecrementButton decrement={this.decrement}/>
        <Display count={this.state.count}/>
      </div>
    );
  }
}

class IncrementButton extends React.Component {
  render() {
    return (
      <button onClick={this.props.increment}>+</button>
    );
  }
}

class DecrementButton extends React.Component {
  render() {
    return (
      <button onClick={this.props.decrement}>-</button>
    );
  }
}

class Display extends React.Component {
  render() {
    return (
      <div>Count: {this.props.count}</div>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById('root'));

在这个示例中,我们将共享的count状态提升到Counter组件中,并通过props将状态和用于操作状态的incrementdecrement方法传递给IncrementButtonDecrementButtonDisplay组件。这样,无论用户点击哪个按钮,Display组件都能显示正确的count值。

关系框架图:
increment decrement count Counter: count IncrementButton DecrementButton Display

在上图中,Counter组件是IncrementButtonDecrementButtonDisplay组件的公共父组件。Counter组件通过propsincrementdecrement方法和count状态传递给子组件,从而实现了兄弟组件之间的通讯。

数据流向流程图

在这个流程图中,我们可以看到兄弟组件IncrementButtonDecrementButton通过调用父组件Counter提供的incrementdecrement方法来影响共享的count状态。而Display组件则通过父组件Counter接收到更新后的count状态并显示它。这种通过公共父组件来实现兄弟组件之间数据通讯的方式,是React中常用的状态管理模式。
increment decrement increment count decrement count count Counter: Count IncrementButton DecrementButton Display

在这个图中:

  1. IncrementButton通过调用父组件Counterincrement方法来增加count值。
  2. DecrementButton通过调用父组件Counterdecrement方法来减少count值。
  3. Display组件从父组件Counter接收count值并显示它。
  4. 父组件Counter负责管理和更新count状态,并将其传递给子组件。

4. Context多层嵌套的组件通讯方式

在复杂的React应用中,我们通常会遇到需要在组件树中传递数据的情况,而不仅仅是单纯的父子组件通讯。例如,如果App组件想要传递数据给深层嵌套的Child组件,通常的做法可能会导致多层的props传递,这既不优雅也不易维护。为了解决这个问题,React提供了Context API,它允许我们在组件树中更加方便地传递和接收数据。

4.1 使用Context传递数据

Context API主要由两个组件组成:ProviderConsumer

1. 创建Context:

首先,我们需要使用React.createContext方法创建一个Context。

javascript 复制代码
const MyContext = React.createContext();

2. Provider组件:

Provider组件负责提供数据。我们将Provider组件放在组件树的外层,并通过value属性传递我们想要共享的数据。

javascript 复制代码
class App extends React.Component {
  state = {
    theme: 'dark'
  }

  render() {
    return (
      <MyContext.Provider value={this.state.theme}>
        <Child />
      </MyContext.Provider>
    );
  }
}

3. Consumer组件:

Consumer组件负责消费数据。我们可以在任何需要接收数据的组件内部使用Consumer组件来接收Provider组件提供的数据。

javascript 复制代码
class Child extends React.Component {
  render() {
    return (
      <MyContext.Consumer>
        {theme => <div>The theme is {theme}</div>}
      </MyContext.Consumer>
    );
  }
}

通过上述代码,我们实现了在App组件和Child组件之间通过Context传递数据,而无需手动在每层组件之间传递props。

4.2 总结:
  • 跨组件通讯: 当两个组件是远方亲戚(例如,嵌套多层)时,可以使用Context实现组件通讯,避免了繁琐的props逐层传递。
  • 提供和消费数据: Context提供了ProviderConsumer两个组件,分别用于提供和消费数据,使得数据的传递变得清晰和方便。
  • 应用场景: Context非常适用于那些需要在组件树中共享状态的场景,例如主题切换、语言切换等。

5. Props 深入学习与使用

Props 是 React 组件的输入,它们可以是任意的值,包括简单的数据类型(如字符串、数字和布尔值)或复杂的数据类型(如对象、数组和函数)。其中一个特殊的 prop 是 children,它表示组件标签的子节点。

5.1 children 属性

children 是一个特殊的 prop,它代表了组件标签内部的内容。children 属性与普通的 props 一样,它的值可以是任意类型,包括文本、React 元素、组件,甚至是函数。

基本用法:

当我们在 JSX 中嵌套组件时,嵌套的内容将作为 children prop 传递给外层组件。

javascript 复制代码
function Hello(props) {
  return (
    <div>
      组件的子节点: {props.children}
    </div>
  );
}

// 使用
<Hello>我是子节点</Hello>

在上述代码中,Hello 组件接收一个 children prop,其值为 "我是子节点"。然后,Hello 组件在其渲染输出中包含这个 children prop,从而显示 "组件的子节点: 我是子节点"。

高级用法:

children prop 还可以接收一个函数,并将该函数作为一个渲染 prop 使用。渲染 prop 是一种将可配置性传递给组件的技术。

javascript 复制代码
function RenderPropComponent(props) {
  return props.children('Render Prop');
}

// 使用
<RenderPropComponent>
  {value => <div>{value}</div>}
</RenderPropComponent>

在上述代码中,RenderPropComponent 组件接收一个 children prop,该 prop 是一个函数。RenderPropComponent 组件调用这个函数,并传递一个参数 'Render Prop'。然后,这个函数返回一个 React 元素,该元素随后被渲染。

通过 children prop,我们可以实现组件之间的灵活交互,甚至可以构建高级的组件,这些组件能够接收和渲染任意内容。这种模式在 React 社区中非常流行,并且被广泛用于构建灵活和可复用的组件。

5.2 Props 校验

Props 校验是一种在运行时检查传递给组件的 props 是否符合预期类型的机制。它对于开发和维护大型项目非常有用,可以在早期发现和修复问题。

为什么要用Props校验

例如,在正常使用中,组件A应该接受传入组件B的数组,以此使用map方式去渲染页面。但是组件B传入了一个整数类型的数据进入组件A,组件A强行调用map方法就会发生了报错。这个时候我们就需要检查类型是否符合我们预期类型。

  • 对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据
  • 如果传入的数据格式不对,可能会导致组件内部报错
  • 关键问题:组件的使用者不知道明确的错误原因
js 复制代码
function App(props)
{
    const arr = props.colors;
    const lst = arr.map(item, index) => <li key=index>item</li>
    return <ul>{lst}</ul>
}

<App colors={1} /> // App预期期待arr类型,但是传入一个整型数字1

使用步骤:

  1. 安装 prop-types

    使用 npm 或 yarn 安装 prop-types 包。

    bash 复制代码
    yarn add prop-types
    # 或
    npm i prop-types
  2. 导入 prop-types

    javascript 复制代码
    import PropTypes from 'prop-types';
  3. 为组件添加 props 校验规则

    使用 ComponentName.propTypes 对象为组件的 props 添加校验规则。propTypes 是一个特殊的属性,它告诉 React,这个组件期望接收何种类型的 props。

    javascript 复制代码
    function App(props) {
      return (
        <h1>Hi, {props.colors}</h1>
      );
    }
    
    // 设置 props 校验规则
    App.propTypes = {
      colors: PropTypes.array
    };

在这个示例中,我们为 App 组件的 colors prop 指定了一个校验规则,该规则要求 colors 必须是一个数组。如果 colors 不是数组,React 将在控制台中显示警告。

进一步的校验

PropTypes 提供了许多其他校验器,允许我们进行更详细的校验。例如,我们可以要求某个 prop 是特定的 JavaScript 类型(如 arrayboolfuncnumberobjectstring),也可以要求它是一个 React 元素、或者是一个枚举类型的值。

javascript 复制代码
App.propTypes = {
  colors: PropTypes.arrayOf(PropTypes.string),
  isVisible: PropTypes.bool,
  onToggle: PropTypes.func,
  value: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.string
  ]),
  theme: PropTypes.oneOf(['light', 'dark']),
  customProp: (props, propName, componentName) => {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        `Invalid prop \`${propName}\` supplied to` +
        ` \`${componentName}\`. Validation failed.`
      );
    }
  }
};

在上述代码中,我们展示了如何使用不同的 PropTypes 校验器来校验 props。这样,我们可以确保组件的使用者提供正确的 props,从而减少运行时错误的可能性。

5.2.1 Props 校验约束规则

在 React 应用中,校验组件的 props 对于确保应用的健壮性和易维护性非常重要。prop-types库提供了一系列的验证器,可以帮助我们确保组件接收到了正确类型的 props。常见约束规则如下:

  • 常见类型:array、bool、func、number、object、string;
  • React元素类型:element;
  • 必填项:isRequired;
  • 特定结构的对象:shape({})

下面介绍一些常用的约束规则和示例:

  1. 常见类型校验

    • 使用 PropTypes 对象的属性来校验特定类型的 props。
    javascript 复制代码
    import PropTypes from 'prop-types';
    
    function MyComponent(props) {
      // ...
    }
    
    MyComponent.propTypes = {
      optionalArray: PropTypes.array,
      optionalBool: PropTypes.bool,
      optionalFunc: PropTypes.func,
      optionalNumber: PropTypes.number,
      optionalObject: PropTypes.object,
      optionalString: PropTypes.string,
    };
  2. React 元素类型校验

    • 使用 PropTypes.element 校验 prop 是否是一个 React 元素。
    javascript 复制代码
    MyComponent.propTypes = {
      optionalElement: PropTypes.element,
    };
  3. 必填项校验

    • 使用 isRequired 标识符指定某个 prop 是必须的。
    javascript 复制代码
    MyComponent.propTypes = {
      requiredFunc: PropTypes.func.isRequired,
      requiredAny: PropTypes.any.isRequired,
    };
  4. 特定结构的对象校验

    • 使用 PropTypes.shape 校验 prop 是否符合指定的结构。
    javascript 复制代码
    MyComponent.propTypes = {
      optionalObjectWithShape: PropTypes.shape({
        color: PropTypes.string,
        fontSize: PropTypes.number,
      }),
    };

    在这个示例中,optionalObjectWithShape prop 必须是一个对象,该对象有两个属性:colorfontSize,其中 color 必须是一个字符串,fontSize 必须是一个数字。

这些校验规则提供了强大的方式来确保组件的 props 符合预期,帮助开发者在早期捕捉可能的错误,并清楚地知道每个组件期望的 props 类型。通过利用 prop-types 库的这些功能,可以编写更健壮、更容易维护的 React 应用。

5.3 props默认值

在开发React组件时,可能会遇到一些情况,即使组件的使用者没有明确提供某些props,组件也需要有一些基本的行为。这时,可以为props设置默认值。这样,如果使用者没有提供这些props,React将使用默认值代替。

以下是如何为props设置默认值的示例:

javascript 复制代码
import React from 'react';

function Pagination(props) {
  return (
    <div>
      每页显示条数: {props.pageSize}
    </div>
  );
}

// 设置默认值
Pagination.defaultProps = {
  pageSize: 10
};

export default Pagination;

在上述代码中,我们创建了一个Pagination组件,该组件接受一个pageSize prop,用于指定每页显示的条目数量。我们使用defaultProps静态属性为pageSize prop设置了默认值10。当组件的使用者没有提供pageSize prop时,React将使用这个默认值。

现在,当我们这样使用Pagination组件时:

javascript 复制代码
<Pagination />

即使我们没有提供pageSize prop,组件也将显示"每页显示条数: 10"。

这种方法允许我们为组件的props提供合理的默认值,确保组件在没有明确指定所有props的情况下仍能正常工作。同时,它也为组件的使用者提供了更多的灵活性,允许他们只在需要时提供props,而不是始终提供所有props。

6. 组件的生命周期

6.1 组件的生命周期概述

组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等组件非预期情况处理。

React 组件的生命周期可以分为三大阶段:挂载阶段(Mounting)、更新阶段(Updating)和卸载阶段(Unmounting)。组件从被创建到挂载 到页面中运行,在运行时我们可以更新 组件信息,再到组件不用时卸载的过程。在这些阶段中,React 提供了不同的生命周期方法(或称为生命周期钩子函数),以便开发者在组件的不同时期执行特定的操作。

另外,结合前面所学的,**只有类组件才有生命周期。**因为函数组件的在创建之后就会被销毁了。

  1. 挂载阶段(Mounting) :
    • constructor: 构造函数,用于初始化组件的 state 和绑定事件处理函数等。
    • static getDerivedStateFromProps: 在组件实例化后和重新渲染前调用,返回一个对象来更新 state,或返回 null 表示没有更新。
    • render: 返回组件的 JSX 结构。
    • componentDidMount: 组件挂载到 DOM 后立即调用,通常用于发起网络请求或设置事件监听器。
javascript 复制代码
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { data: null };
  }

  static getDerivedStateFromProps(nextProps, nextState) {
    // ...
  }

  componentDidMount() {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => this.setState({ data }));
  }

  render() {
    return (
      <div>
        {this.state.data ? this.state.data : 'Loading...'}
      </div>
    );
  }
}
  1. 更新阶段(Updating) :
    • static getDerivedStateFromProps: 同上。
    • shouldComponentUpdate: 返回一个布尔值,决定是否继续渲染周期。
    • render: 同上。
    • getSnapshotBeforeUpdate: 在最新的渲染输出提交到 DOM 前调用,返回值将作为 componentDidUpdate 的第三个参数。
    • componentDidUpdate: 组件更新后调用,通常用于在更新后执行网络请求或 DOM 操作。
javascript 复制代码
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.value !== this.props.value;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // ...
  }

  render() {
    return (
      <div>
        {this.props.value}
      </div>
    );
  }
}
  1. 卸载阶段(Unmounting) :
    • componentWillUnmount: 组件卸载前调用,通常用于清理事件监听器或取消网络请求等。
javascript 复制代码
class MyComponent extends React.Component {
  componentWillUnmount() {
    // 清理操作,如取消事件监听、网络请求等
  }

  render() {
    return (
      <div>
        {this.props.value}
      </div>
    );
  }
}

以上各阶段的生命周期方法为开发者提供了在不同时机操作组件的能力,从而能够实现更复杂的功能,以及在必要时进行性能优化或资源清理等操作。

6.2 生命周期的三个阶段 - 创建时(挂载阶段)

在创建(挂载)阶段,组件会经历 constructorrendercomponentDidMount 的执行顺序。下面是这个阶段各生命周期方法的流程图和详细描述:

在此阶段,组件实例被创建并插入到 DOM 中。以下是这个阶段中的生命周期方法的执行顺序和作用:

流程图:

step 1: constructor step 2: render step 3: componentDidMount

表格描述:
钩子函数名称 触发时机 作用
constructor 创建组件时,最先执行 初始化 state,为事件处理程序绑定 this
render 每次组件渲染都会触发 渲染 UI (注意:不能调用setState)
componentDidMount 组件挂载(完成DOM渲染)后 发送网络请求,DOM 操作,设置事件监听器等
代码示例:
javascript 复制代码
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { data: null };
    this.handleEvent = this.handleEvent.bind(this);
  }

  componentDidMount() {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => this.setState({ data }));
  }

  handleEvent() {
    // event handling logic
  }

  render() {
    return (
      <div onClick={this.handleEvent}>
        {this.state.data ? this.state.data : 'Loading...'}
      </div>
    );
  }
}

在上述示例中,我们首先在 constructor 中初始化了 state 并绑定了事件处理函数 handleEvent。随后,在 componentDidMount 中发起了一个网络请求来获取数据。最后,render 方法负责渲染 UI,其中包含了一个点击事件处理器 handleEvent

6.2 生命周期的三个阶段 - 更新时 (更新阶段)

render更新执行时机∶

  1. setState()
  2. forceUpdate()
  3. 组件接收到新的props.

在此阶段,由于 props 的变化、state 的更新或者父组件的重新渲染等原因,组件可能需要更新。

以下是这个阶段中的生命周期方法的执行顺序和作用:

流程图:
return true getDerivedStateFromProps shouldComponentUpdate render getSnapshotBeforeUpdate componentDidUpdate

表格描述:

钩子函数名称 触发时机 作用
getDerivedStateFromProps 在render方法前,包括初次渲染和后续更新 返回对象来更新state,或返回null表示无需更新
shouldComponentUpdate 在渲染前,返回false会跳过后续渲染过程 返回 false 来阻止渲染,用于优化性能
render 每次组件渲染都会触发 渲染 UI (注意:不能调用setState)
getSnapshotBeforeUpdate 在最新的渲染输出提交给DOM之后,立即执行 从 DOM 捕获一些信息,以供 componentDidUpdate 使用
componentDidUpdate 在更新后立即调用,首次渲染不会执行 通常用于网络请求和DOM操作 注意,如果要setState()必须放在if条件下
代码示例:
javascript 复制代码
class UpdateComponent extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.value !== prevState.value) {
      return { value: nextProps.value };  // 更新state
    }
    return null;  // 无需更新
  }

  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.value !== this.props.value;  // 若prop值未变化,则阻止渲染
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 通常在此执行网络请求或DOM操作
  }

  render() {
    return <div>{this.state.value}</div>;
  }
}

在这个示例中,getDerivedStateFromProps 方法检查新的 props.value 是否有变化,如果有,则更新 state.valueshouldComponentUpdate 方法用于比较新旧 props.value,如果它们相同,则阻止组件渲染。componentDidUpdate 通常用于网络请求和DOM操作。

componentDidUpdate注意事项

componentDidUpdate是一个在组件更新后立即调用的生命周期方法。它是在render方法之后,最新的渲染输出被提交到DOM之后立即执行的。这是一个好地方去执行可能需要DOM的任何代码。你也可能会需要在这里做网络请求,但要确保你有一个条件来避免无限循环。如果你需要更新数据,在调用setState时,你必须包裹它在一个条件语句中,以确保不会触发无限的更新循环。

下面是一个示例,展示如何在componentDidUpdate中执行网络请求和DOM操作,并正确地使用setState:

javascript 复制代码
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
      searchTerm: ''
    };
  }

  componentDidUpdate(prevProps, prevState) {
    // 检查是否有新的搜索词,避免无限循环
    if (prevState.searchTerm !== this.state.searchTerm) {
      // 发起网络请求
      fetch('https://api.example.com/data?search=' + this.state.searchTerm)
        .then(response => response.json())
        .then(data => this.setState({ data }));
    }

    // DOM 操作示例
    if (this.state.data && !prevState.data) {
      document.title = 'New Data Received';
    }

    // 如果需要更新状态,请确保它在条件语句中
    if (someCondition) {
      this.setState({ /* ... */ });
    }
  }

  handleSearch = (event) => {
    this.setState({ searchTerm: event.target.value });
  }

  render() {
    return (
      <div>
        <input type="text" onChange={this.handleSearch} />
        {/* ... */}
      </div>
    );
  }
}

在这个示例中,我们在componentDidUpdate中检查searchTerm是否有变化,如果有,我们发起一个网络请求。同时,我们检查data状态是否有变化,如果有,我们更新文档的标题。当满足某些条件时,我们也在componentDidUpdate中调用setState,但确保它是在一个条件语句中,以避免无限循环。

6.3 生命周期的三个阶段 - 销毁时 (销毁阶段)

当组件即将从DOM中被移除时,componentWillUnmount生命周期方法将会被调用。这是执行任何必要清理的好时机,比如无效的定时器、取消网络请求,或清理任何在componentDidMount中创建的订阅等。

下面是一个示例,展示了如何在componentWillUnmount方法中清理一个定时器:

javascript 复制代码
class TimerComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      time: 0
    };
  }

  // 当组件挂载时,创建一个定时器
  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000  // 每秒更新一次
    );
  }

  // 当组件即将卸载时,清除定时器
  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  // 更新组件的state,触发重新渲染
  tick() {
    this.setState((prevState) => ({
      time: prevState.time + 1
    }));
  }

  render() {
    return (
      <div>
        <h1>Elapsed Time: {this.state.time} seconds</h1>
      </div>
    );
  }
}

// 在其他地方使用TimerComponent
// ...

在上述示例中,我们在componentDidMount生命周期方法中创建了一个定时器,并在componentWillUnmount生命周期方法中清除了定时器。这样确保了不会有任何泄漏,例如,如果定时器继续运行,即使组件不再在DOM中也无法清除它,那将是一个问题。通过在componentWillUnmount中清除定时器,我们可以确保当组件被卸载时释放所有的资源。

7. render-props 和高阶组件 (HOC)

7.1 React组件复用概述

在 React 应用中,复用代码是非常重要的。特别是,有时候我们会在多个组件中遇到相似或相同的功能逻辑,这时候就需要考虑如何将这些逻辑抽离出来,形成可复用的代码。React 社区中主要有两种方式来实现组件逻辑的复用:render-props高阶组件 (Higher-Order Components, HOC) 。这两种方式允许我们在不同的组件中复用某些逻辑,而不必改变组件的结构。

思考︰如果两个组件中的部分功能相似或相同,该如何处理?

处理方式∶复用相似的功能(联想函数封装)

复用什么?

  1. state设置状态
  2. 操作state的方法(组件状态逻辑)

两种方式

  1. render props模式
  2. 高阶组件(HOC )

注意∶这两种方式不是新的APi,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)

7.2 Render Props 模式

Render Props 模式是 React 中的一个非常强大的模式,它允许我们在不同的组件中共享某些状态或逻辑,同时还能保留组件的灵活性,使得我们可以自定义渲染的 UI。下面通过一个实际的示例来深入了解 Render Props 模式的运用。

思路分析:

  1. 问题1: 如何拿到该组件中复用的 state?

    • 解决方案: 我们可以创建一个组件,并在这个组件中封装我们想要共享的 state 和操作 state 的方法。然后,我们提供一个函数作为 prop,这个函数接受封装的 state 作为参数。
  2. 问题2: 如何渲染任意的 UI?

    • 解决方案: 使用该函数的返回值作为要渲染的 UI 内容。这样,我们就能在函数中自由地定义我们想要渲染的 UI,并且可以访问到共享的 state。

另外,在Render Props模式中,props.render是一个约定,它是通过组件的props传递一个函数到组件内部,然后在组件内部调用这个函数并传递一些参数给它,最后将这个函数的返回值渲染到DOM中,所以,这意味着你可以使用其他名字代替render,只要你能在需要使用函数的地方正确的调用它与传入它。

示例:

假设我们有一个Mouse组件,它能够追踪用户的鼠标位置。我们想把这个鼠标追踪的功能复用在不同的组件中,但是渲染的内容可能不同,这时我们可以使用Render Props模式来实现:

javascript 复制代码
import React, { Component } from 'react';

class Mouse extends Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

function App() {
  return (
    <Mouse render={mouse => (
      <p>鼠标的位置是 {mouse.x}, {mouse.y}</p>
    )}/>
  );
}

export default App;

在上面的代码中:

  • Mouse 组件通过 onMouseMove 事件来追踪用户的鼠标位置,并将位置信息保存在 state 中。
  • Mouse 组件接受一个名为 render 的 prop,这个 prop 是一个函数。在 Mouse 组件的 render 方法中,我们调用 this.props.render 函数,并将 this.state 作为参数传递给它。这样,我们就能够在外部获取到 Mouse 组件内部的 state 信息。
  • App 组件中,我们使用 Mouse 组件,并提供一个 render prop。这个 render prop 是一个函数,它接受 mouse 参数,并返回一个 React 元素。通过这种方式,我们就能够自定义渲染的 UI,并且可以访问到 Mouse 组件内部的 state 信息。

这个示例展示了如何通过 render props 模式来复用组件逻辑,同时保持组件的灵活性和可定制性。在实际开发中,render props 模式是一个非常有用的模式,它能够帮助我们更好地组织和复用代码。

7.2 高阶组件 (Higher-Order Components, HOC)

高阶组件(High Order Component, HOC)是React中用于组件逻辑复用的一种模式。它是一个接收组件并返回新组件的函数。通过这种方式,可以在不同的组件间共享相同的逻辑。在使用高阶组件时,通常需要遵循以下步骤:

创建高阶组件
  1. 创建一个函数 :这个函数是高阶组件的主体,它会接收一个组件作为参数,并返回一个新的组件。通常约定是以with为前缀来命名这个函数。
javascript 复制代码
function withExampleFeature(WrappedComponent) {
  // ...
}
  1. 指定函数参数 :函数的参数应该是一个组件,通常以大写字母开头来命名,例如WrappedComponent
javascript 复制代码
function withExampleFeature(WrappedComponent) {
  // ...
}
  1. 在函数内部创建一个类组件:在这个类组件内,可以定义和管理你想要复用的状态和逻辑。
javascript 复制代码
function withExampleFeature(WrappedComponent) {
  return class extends React.Component {
    // 这里可以定义和管理状态
    state = {
      featureEnabled: false,
      // ...
    };

    // 可以定义任何你想要复用的逻辑
    enableFeature = () => {
      this.setState({ featureEnabled: true });
    };

    // ...
  };
}
  1. 在类组件内渲染WrappedComponent :同时将你想要共享的状态和逻辑通过props传递给WrappedComponent
javascript 复制代码
function withExampleFeature(WrappedComponent) {
  return class extends React.Component {
    // ...

    render() {
      // 将状态和逻辑传递给 WrappedComponent
      return <WrappedComponent {...this.props} {...this.state} enableFeature={this.enableFeature} />;
    }
  };
}
使用高阶组件
  1. 调用高阶组件:传递你想要增强的组件作为参数,并将返回的新组件渲染到页面上。
javascript 复制代码
const EnhancedComponent = withExampleFeature(OriginalComponent);

// 在你的组件树中使用 EnhancedComponent
<EnhancedComponent />
为高阶组件设置displayName

当你使用高阶组件时,可能会发现在React Developer Tools中,包装后的组件和原始组件具有相同的displayName。为了避免混淆,并使得调试更为简单,可以为高阶组件设置一个明确的displayName

javascript 复制代码
function withExampleFeature(WrappedComponent) {
  class WithExampleFeature extends React.Component {
    // ...
  }

  WithExampleFeature.displayName = `WithExampleFeature(${getDisplayName(WrappedComponent)})`;

  return WithExampleFeature;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

在上面的代码中,WithExampleFeature.displayName设置了一个明确的displayName,该displayName包含了原始组件的名称。getDisplayName函数是一个简单的辅助函数,用于获取组件的名称。这样,在React Developer Tools中,你就能明确地看到每个组件是由哪个高阶组件包装的。

解决props丢失问题:

在使用高阶组件(HOC)时,需要注意props的传递问题。如果不正确地传递props,那么被包装的组件可能无法访问到外部传递给高阶组件的props,这会导致预期之外的行为。为了解决这个问题,应该确保在渲染被包装组件时传递所有接收到的props。

正确传递props的关键是使用JavaScript的展开操作符(...)来传递props和state。

下面是一个例子,展示了如何在高阶组件中正确传递props:

javascript 复制代码
function withExampleFeature(WrappedComponent) {
  return class extends React.Component {
    state = {
      featureEnabled: false,
    };

    enableFeature = () => {
      this.setState({ featureEnabled: true });
    };

    render() {
      // 使用展开操作符将this.props和this.state传递给WrappedComponent
      return <WrappedComponent {...this.props} {...this.state} enableFeature={this.enableFeature} />;
    }
  };
}

const EnhancedComponent = withExampleFeature(OriginalComponent);

// 使用EnhancedComponent,并传递props
<EnhancedComponent someProp="value" />

在上述代码中,withExampleFeature是一个高阶组件,它返回一个新的组件类。在这个新组件的render方法中,我们使用展开操作符...this.propsthis.state传递给WrappedComponent。这样,无论外部如何传递props,WrappedComponent都能接收到所有的props和state。

这种方式确保了WrappedComponent能够接收到所有从外部传递来的props,以及高阶组件内部管理的state和方法。通过这种方式,可以解决高阶组件中的props丢失问题,确保被包装组件能够正确地工作。

8. useEffect钩子函数使用

useEffect的概念理解

useEffect是一个React Hook函数,用于在React组件中创建不是由事件引起而是由渲染本身引起的操作,比如发送AJAX请求,更改DOM等等。

说明:上面的组件中没有发生任何的用户事件,组件渲染完毕之后就需要和服务器要数据,整个过程属于**"只由渲染引起的操作"** 。

useEffect的基础使用

需求:在组件渲染完毕之后,立刻从服务端获取频道列表数据并显示到页面中语法:

js 复制代码
useEffect(()=>{}, [])
  • 参数1是一个函数,可以把它叫做副作用函数,在函数内部可以放置要执行的操作
  • 参数2是一个数组(可选参),在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次.

useEffect依赖项参数说明

useEffect副作用函数的执行时机存在多种情况,根据传入依赖项的不同,会有不同的执行表现

依赖项 副作用函数调用时机
没有依赖项 组件初始化渲染+组件更新时渲染
空数组依赖项 只有组件初始化时渲染
添加特定依赖项 组件初始化渲染+特定依赖项发生变化时

参考代码

react 复制代码
import {userEffect, useState} from "react"

const App = () => 
{
    // 1. 没有依赖项:初始化+组件更新时
    const [count, setCount] = useState(0);
    useEffect(() => {
        console.log("副作用函数执行了")
    });
    
    // 2.传入空数组依赖项:仅在初始执行一次
    useEffect(()=>{
        console.log("副作用函数执行了")
    }, []);
    
    // 3. 传入特定依赖项:初始化+依赖项发生变化时执行
    useEffect(()=>{
        console.log("副作用函数执行了")
    }, [count]);
    
    return (
    	<div>
        	<button onClick={()=>setCount(count + 1)}>+{count}</button>
        </div>
    );
}

export default App;
useEffect-清理副作用

在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用。

语法格式:

react 复制代码
useEffect(()=>
{
    // 副作用函数逻辑
	return () => 
    {
		// 清理副作用函数逻辑
	}
}, [])

说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行

示例代码:

react 复制代码
import {useEffect, useState} from "react";

const Son = () =>
{
    useEffect
    (
        // 1. 渲染时开启一个定时器
        () =>
        {
            // 副作用函数逻辑
			const timer = setInterval(() => 
            {
                console.log("定时器执行中")
            }, 1000)
            
            // 2.返回副作用
            return () => 
            {
                // 副作用函数逻辑
                clearInterval(timer)
            }
        }, 
        []
    )
    return <div>this is Son</div>
}

const App = () =>
{
    const [show, setShow] = useState(true)
    return (
    	<div>
        	{show && <Son />}
        	<button onClick={()=>setShow(false)}>卸载Son组件</button>
        </div>
    )
}

useEffect 具有以下优势:

  1. 控制副作用执行时机useEffect 提供了一种在组件渲染后执行副作用(例如数据获取、订阅或者手动更改 DOM)的方式。这样可以确保你的副作用在 DOM 更新完毕后执行,避免了因 DOM 还未准备好而导致的错误。
  2. 依赖项数组useEffect 的第二个参数是一个依赖项数组,它告诉 React 只有在依赖项发生变化时才重新执行副作用。在上述代码中,依赖项数组包含 dispatch,这意味着只有当 dispatch 函数发生变化时,useEffect 内的代码才会重新执行。由于 dispatch 函数通常不会变化,所以 useEffect 内的代码基本上只会在组件第一次渲染时执行。

9. React组件进阶总结

React的组件进阶主要涵盖了组件通讯、props的应用、状态提升、组件生命周期、钩子函数、render props模式、高阶组件以及组件的简洁模型。下面分别总结这些方面的核心内容:

  1. 组件通讯:

组件通讯是React应用的基础,主要分为以下几种:

  • 父子组件通讯

    父组件通过props向子组件传递数据,子组件通过回调函数向父组件传递数据。

    javascript 复制代码
    // 父组件
    class Parent extends React.Component {
        state = { data: 'hello' };
        render() {
            return <Child data={this.state.data} />;
        }
    }
    // 子组件
    function Child(props) {
        return <div>{props.data}</div>;
    }
  • 兄弟组件通讯

    兄弟组件间的通讯通常通过共同的父组件来中转,或使用状态管理库如 Redux。

    javascript 复制代码
    // 公共父组件
    class CommonParent extends React.Component {
        state = { sharedData: 'hello' };
        render() {
            return (
                <>
                    <SiblingOne data={this.state.sharedData} />
                    <SiblingTwo data={this.state.sharedData} />
                </>
            );
        }
    }
  1. Props的应用:
  • Props校验 :通过prop-types库进行props的类型校验,以确保组件接收到正确格式的props。
  • Props默认值 :通过defaultProps为props设置默认值,确保在未传入props时组件能正常工作。
  1. 状态提升:

状态提升是一种将状态数据提升到公共父组件,然后通过props将状态传递给子组件的模式。它可以解决多个组件需要共享状态的问题。

  1. 组件生命周期:

理解组件的生命周期是掌握React的关键,包括挂载阶段、更新阶段和卸载阶段,以及在这些阶段中可以使用的钩子函数如componentDidMount, componentDidUpdatecomponentWillUnmount

  1. 钩子函数:

钩子函数提供了在特定时机执行某些操作的能力,如在组件挂载后发送网络请求等。

  1. Render Props模式和高阶组件:
  • Render Props:通过一个函数prop向子组件传递数据,使得子组件可以灵活渲染不同的内容。
  • 高阶组件(HOC):通过包装组件的方式共享组件逻辑,使得组件更加复用和模块化。
javascript 复制代码
// 高阶组件示例
function withEnhancement(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent {...this.props} enhancedProp="enhanced" />;
        }
    };
}

const EnhancedComponent = withEnhancement(OriginalComponent);
  1. 组件的极简模型:

React组件的核心是根据state和props渲染UI,即(state, props) => UI。理解这一模型有助于编写简洁、高效的React代码。

通过以上的总结和示例,可以更好地理解React组件的进阶概念,为构建复杂的React应用奠定基础。

React原理机制学习

1. 引入

在深入学习React框架的时候,理解其背后的机制原理是非常重要的。这不仅仅可以帮助我们编写出更高效、更可靠的代码,而且也可以在遇到问题时,更快地定位并解决问题。在这个阶段,我们将深入探讨React的核心机制和原理,包括以下几个重要的方面:

  1. 异步的setState()

setState()方法是React中最常用的方法之一,它用于更新组件的状态。然而,setState()并不是立即更新状态,而是异步的。理解其异步的机制有助于我们避免因为依赖于立即更新状态而导致的一些常见错误。

  1. JSX语法的转化过程

JSX是React的一种语法糖,它让我们可以用类似于XML的语法来描述组件的结构。但是,JSX最终会被转化为JavaScript代码。了解这个转化过程,可以帮助我们更好地理解React是如何工作的。

  1. React组件的更新机制

React组件的更新机制是保证其性能和效率的关键。理解组件何时以及为什么会更新,以及如何控制组件的更新,对于编写高效的React应用是非常重要的。

  1. 组件性能优化

通过某些优化技巧,如使用shouldComponentUpdateReact.memo,我们可以提高React应用的性能。这一部分,我们将深入探讨如何对React组件进行性能优化。

  1. 虚拟DOM和Diff算法

虚拟DOM是React高效的核心,而Diff算法则是虚拟DOM的基础。通过理解虚拟DOM和Diff算法的原理,我们可以更好地理解React为何能提供如此高的渲染效率。

通过探讨以上的核心机制和原理,我们将能够更加深入地理解React的工作方式,从而编写出更高效、更可维护的React应用。

2. setState()的说明

React中的setState()方法是用于更新组件状态的主要方式。它提供了一个机制,使我们能够以声明式的方式描述组件的状态应该如何随时间变化。下面我们将探讨setState()的一些核心特点和使用方法。

2.1 更新数据
异步更新

setState()是异步更新数据的。这意味着在调用setState()后,状态不会立即更新。而是React会将setState()调用放入一个队列中,并在稍后的一个更合适的时间点统一处理这些更新。这种异步的机制有助于优化性能,减少不必要的渲染。

javascript 复制代码
this.state = { count: 1 };
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);  // 输出: 1

在上面的示例中,由于setState()是异步的,所以在setState()调用之后立即打印this.state.count,结果仍然是1,而不是2。

2.2 推荐语法

在处理setState()时,推荐使用函数式更新,这种方式可以确保你的状态更新是基于最新的状态和属性。函数式更新接受两个参数:最新的stateprops,并应返回一个对象来更新状态。下面是使用函数式更新的一个示例:

javascript 复制代码
this.setState((state, props) => {
  return {
    count: state.count + 1
  };
});

// 在函数式更新之后,状态不会立即更新
console.log(this.state.count);  // 输出: 1

在上面的代码中,我们将一个函数传递给setState(),而不是一个对象。函数接收最新的stateprops作为参数,并返回一个对象,该对象包含我们想要更新的状态值。

2.3 第二个参数

setState()也接受一个可选的回调函数作为它的第二个参数。这个回调函数会在状态更新和组件重新渲染完成后被调用。这可以用于在状态更新后立即执行某些操作。

javascript 复制代码
this.setState(
  (state, props) => {
    return { count: state.count + 1 };
  },
  () => {
    console.log('这个回调函数会在状态更新后立即执行');
  }
);

// 或者,可以使用它来更新文档标题
this.setState(
  (state, props) => { return { count: state.count + 1 }; },
  () => { document.title = '更新state后的标题: ' + this.state.count; }
);

在上述代码中,我们展示了如何使用setState()的回调函数来在状态更新后执行操作。这可以是任何你想要在状态更新后立即执行的操作,例如更新文档的标题或执行其他的副作用。

2.3 批量更新

React有一个优化机制,可以将多个setState()调用合并成一个,以减少渲染的次数。这意味着,即使你多次调用setState(),React也只会触发一次重新渲染。

javascript 复制代码
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 结果: count只会增加1,而不是3

在上面的示例中,尽管我们调用了三次setState(),但count的值只会增加1,而不是3。这是因为React将这三个setState()调用合并成了一个,只触发了一次重新渲染。

3. JSX语法的转化过程

在React中,JSX只是React.createElement()方法的语法糖,它提供了一种更加简洁、易读的方式来创建React元素。但在背后,JSX代码需要通过Babel插件@babel/preset-react被转译成React.createElement()调用。下面是JSX语法转化的三个主要步骤:

  1. 编写JSX:
javascript 复制代码
const element = (
  <h1 className="greeting">
    Hello JSX!
  </h1>
);
  1. JSX转化为createElement()调用:
javascript 复制代码
const element = React.createElement(
  'h1',
  { className: 'greeting' },
  'Hello JSX!'
);
  1. createElement()返回React元素:
javascript 复制代码
// 注意: 这是简化过的结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello JSX!'
  }
};

在这个过程中,首先我们编写了JSX代码。然后,Babel插件@babel/preset-react将JSX代码转化为React.createElement()调用。最后,React.createElement()方法返回一个React元素对象,该对象描述了我们想要在屏幕上渲染的内容。

4. 组件更新机制

setState()方法在React组件中有两大核心作用:

  1. 修改组件的state状态;
  2. 触发组件及其子组件的更新和重新渲染 (UI 更新)。
  3. 当一个父组件被重新渲染时,它的所有子组件也会被重新渲染,但只是在当前组件子树中(包括当前组件及其所有子组件)。

下面的树状结构图示展示了一个**三层二叉树的原始结构和更新过程。**在更新过程中,我们将更新第二层的右侧子树,并突出显示被更新的节点组件。

原始结构:

Left Right Left Right Left Right Root Component Left Child 1 Right Child 1 Left Child 2.1 Right Child 2.1 Left Child 2.2 Right Child 2.2

更新过程:

Left Right Left Right Left Right Root Component Left Child 1 Right Child 1 Left Child 2.1 Right Child 2.1 Left Child 2.2 Right Child 2.2

在上述更新过程图中,我们突出显示了将要被更新的组件节点 - 第二层的右侧子树,包括Right Child 1及其所有子节点Left Child 2.2Right Child 2.2。当setState()被调用时,Right Child 1及其所有子组件会被重新渲染,从而更新UI。

示例代码

根据根据上面的树结构,我们可以创建一个React项目,并为每个组件创建一个按钮,当按钮被点击时,它会更新状态并在控制台输出更新信息。下面是基于该场景的代码示例:

jsx 复制代码
import React, { Component } from 'react';

class Node extends Component {
  constructor(props) {
    super(props);
    this.state = {
      updateCount: 0,
    };
  }

  handleUpdate = () => {
    this.setState(prevState => ({
      updateCount: prevState.updateCount + 1,
    }), () => {
      console.log(`${this.props.name} updated ${this.state.updateCount} times.`);
    });
  }

  render() {
    return (
      <div style={{ border: '1px solid black', padding: '10px', margin: '10px' }}>
        <button onClick={this.handleUpdate}>Update {this.props.name}</button>
        {this.props.children}
      </div>
    );
  }
}

function App() {
  return (
    <Node name="Root Component">
      <Node name="Left Child 1">
        <Node name="Left Child 2.1" />
        <Node name="Right Child 2.1" />
      </Node>
      <Node name="Right Child 1">
        <Node name="Left Child 2.2" />
        <Node name="Right Child 2.2" />
      </Node>
    </Node>
  );
}

export default App;

在这个示例中,我们创建了一个Node组件类,它包含一个按钮和一个handleUpdate方法。当按钮被点击时,handleUpdate方法会被调用,它更新updateCount状态并在控制台输出更新信息。App函数组件作为根组件,并按照提供的树结构嵌套Node组件。

当你运行这个项目并点击任何一个节点的更新按钮时,你会看到对应的节点名和更新次数被打印到控制台中。同时,由于React的组件更新机制,当你更新一个父节点时,它的所有子节点也会被重新渲染。

虚拟DOM和Diff算法是React中用于优化渲染性能的核心技术。通过使用虚拟DOM和Diff算法,React能够最小化操作真实DOM的次数,从而提高应用的渲染性能。

5. 虚拟DOM (Virtual DOM) 与diff算法

定义:虚拟DOM是一个轻量级的对真实DOM的抽象,它和真实DOM具有相同的结构,但是它存在于内存中,而不是真实的浏览器环境中。

目的:减少直接操作真实DOM所产生的性能消耗。直接操作真实DOM是非常昂贵的,虚拟DOM提供了一种方式,使得我们可以在内存中操作DOM,然后通过最小的变更来更新真实DOM。

示例

jsx 复制代码
// JSX语法
const element = <h1>Hello, world</h1>;

// 转化为虚拟DOM对象
const virtualDOM = {
  type: 'h1',
  props: {
    children: 'Hello, world'
  }
};
5.1 Diff算法

定义:Diff算法是React用于比较两个虚拟DOM树的差异的算法。

目的:找出虚拟DOM树中发生变化的部分,以便只更新真实DOM中变化的部分,而不是重新渲染整个DOM树。

过程

  1. 初次渲染:React根据初始的state创建一个虚拟DOM对象(树),然后根据虚拟DOM生成真实的DOM,并渲染到页面中。
  2. 数据变化 :当数据变化(例如通过setState方法),React会重新根据新的数据创建一个新的虚拟DOM对象(树)。
  3. Diff比较:React会使用Diff算法比较新旧两个虚拟DOM树,找出其中的差异。
  4. 局部更新:根据Diff算法得到的差异,React只会更新真实DOM中变化的部分,而不是重新渲染整个DOM树。
5.2 实例分析

假设我们有一个列表组件,它的内容是根据state中的数据动态生成的。当我们添加一个新的列表项时,React会执行以下步骤:

  1. 创建新的虚拟DOM树:React会根据新的state数据创建一个新的虚拟DOM树。
  2. 执行Diff算法:React会比较新旧两个虚拟DOM树,找出差异。在这个例子中,差异是有一个新的列表项被添加。
  3. 局部更新真实DOM:React会将新的列表项添加到真实DOM的列表中,而不是重新渲染整个列表。

通过这种方式,React能够保证只更新真实DOM中变化的部分,从而提高渲染性能。

5.3 为什么使用虚拟DOM

我们知道,当我们希望实现一个具有复杂状态的界面时,如果我们在每个可能发生变化的组件上都绑定事件,绑定字段数据,那么很快由于状态太多,我们需要维护的事件和字段将会越来越多,代码也会越来越复杂,于是,我们想我们可不可以将视图和状态分开来,只要视图发生变化,对应状态也发生变化,然后状态变化,我们再重绘整个视图就好了。

这样的想法虽好,但是代价太高了,于是我们又想,能不能只更新状态发生变化的视图?于是Virtual Dom应运而生,状态变化先反馈到Virtual Dom上,Virtual Dom在找到最小更新视图,最后批量更新到真实DOM上,从而达到性能的提升。

除此之外,从移植性上看,Virtual Dom还对真实dom做了一次抽象,这意味着Virtual Dom对应的可以不是浏览器的DOM,而是不同设备的组件,极大的方便了多平台的使用。如果是要实现前后端同构直出方案,使用Virtual Dom的框架实现起来是比较简单的,因为在服务端的Virtual Dom跟浏览器DOM接口并没有绑定关系。

基于 Virtual DOM 的数据更新与UI同步机制:

初始渲染时,首先将数据渲染为 Virtual DOM,然后由 Virtual DOM 生成 DOM。

数据更新时,渲染得到新的 Virtual DOM,与上一次得到的 Virtual DOM 进行 diff,得到所有需要在 DOM 上进行的变更,然后在 patch 过程中应用到 DOM 上实现UI的同步更新。

5.4 总结

虚拟DOM和Diff算法是React优化渲染性能的核心技术。虚拟DOM降低了直接操作真实DOM的性能消耗,而Diff算法确保了只更新真实DOM中变化的部分。通过理解和利用这些技术,开发者可以创建高性能的React应用,提供流畅的用户体验。

虚拟DOM的真正价值从来都不是性能。虚拟DOM的主要价值在于它提供了一种抽象,使得开发者可以以声明式的方式描述界面,而不需要直接操作DOM。虽然虚拟DOM也有助于提升性能。同时,虚拟DOM把DOM虚拟成一个React对象,不仅提高了代码的可维护性和可读性,还可以使得React的虚拟DOM能够脱离浏览器来运行,让能运行js的地方都能运行我们的React。这才是真正虚拟DOM带来的真正价值。

6. React原理机制总结

  1. 原理有助于更好地理解React的自身运行机制

了解React的原理,如虚拟DOM和Diff算法,有助于开发者更好地理解React的运行机制,以及为什么React能够提供高性能的渲染。

  1. setState()异步更新数据

React中的setState方法是异步的,这意味着在调用setState后,state不会立即更新,而是在后续的重新渲染过程中更新。这是一个常见的误区,但了解这一点可以帮助开发者避免一些常见的问题。

javascript 复制代码
// 示例
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);  // 输出的是更新前的值
  1. 父组件更新导致子组件更新,纯组件提升性能

当父组件更新时,其子组件也会被重新渲染。但如果子组件的props和state没有变化,重新渲染是不必要的。使用纯组件(PureComponent)或shouldComponentUpdate方法可以避免不必要的重新渲染,从而提升应用的性能。

javascript 复制代码
// 示例
class MyComponent extends React.PureComponent {
  render() {
    return <div>{this.props.value}</div>;
  }
}
  1. 思路清晰简单为前提,虚拟DOM和Diff保效率

虚拟DOM提供了一种能够简单清晰地描述界面的方式,而Diff算法则确保只有必要的部分被更新,从而保证了渲染的效率。

  1. 虚拟DOM -> state + JSX

虚拟DOM是通过state和JSX生成的,它为开发者提供了一种声明式的方式来描述界面,使得代码更易于理解和维护。

  1. 虚拟DOM的真正价值从来都不是性能

虚拟DOM的主要价值在于它提供了一种抽象,使得开发者可以以声明式的方式描述界面,而不需要直接操作DOM。虽然虚拟DOM也有助于提升性能。同时,虚拟DOM把DOM虚拟成一个React对象,不仅提高了代码的可维护性和可读性,还可以使得React的虚拟DOM能够脱离浏览器来运行,让能运行js的地方都能运行我们的React。这才是真正虚拟DOM带来的真正价值。

  1. 结论

React通过其独特的虚拟DOM和Diff算法,以及其组件模型和生命周期方法,为开发者提供了一种高效、简洁和可维护的方式来构建用户界面。通过深入理解React的原理和运行机制,开发者可以更好地利用React的优势,构建高性能和可维护的应用。

相关推荐
duansamve3 分钟前
WebGIS地图框架有哪些?
javascript·gis·openlayers·cesium·mapbox·leaflet·webgis
_jacobfu6 分钟前
mac2024 安装node和vue
前端·javascript·vue.js
Ztiddler7 分钟前
【npm设置代理-解决npm网络连接error network失败问题】
前端·后端·npm·node.js·vue
三天不学习9 分钟前
前端工程化-node/npm/babel/polyfill/webpack 一文速通
前端·webpack·npm
diandian~10 分钟前
[N1CTF 2018]eating_cms
web
羽羽Ci Ci15 分钟前
axios vue.js
前端·javascript·vue.js
halo141617 分钟前
uni-app 界面TabBar中间大图标设置的两种方法
开发语言·javascript·uni-app
货拉拉技术17 分钟前
多元消息融合分发平台
javascript·后端·架构
醉陌离23 分钟前
渗透测试学习笔记—shodan(2)
笔记·学习
岳哥i41 分钟前
前端项目接入单元测试手册
前端·单元测试