React 之 组件化开发

news2024/9/20 16:09:27
本文主讲解类组件,函数组件会在后续文章中学习

一、组件化开发

1. 概念

组件化是一种分而治之的思想:

  • 如果将一个页面中所有的处理逻辑放在一起,处理起来会变得非常复杂,不利于后续的管理以及扩展

  • 但如果讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么页面的管理和维护就变得非常容易了

2. react组件化

组件化是React的核心思想

3. 组件的应用

在开发中充分的利用组件化的思想 :

  • 尽可能的将页面拆分成一个个小的、可复用的组件

  • 让我们的代码更加方便组织和管理,并且扩展性也更强

4. 组件的分类

React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
  • 根据组件的定义方式

  • 分成:函数组件(Functional Component )和类组件(Class Component) => 主要!

  • 根据组件内部是否有状态需要维护

  • 分成:无状态组件(Stateless Component )和有状态组件(Stateful Component)

  • 根据组件的不同职责

  • 分成:展示型组件(Presentational Component)和容器型组件(Container Component);

  • 还有很多组件的其他概念:比如异步组件、高阶组件...

这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离
  • 函数组件、无状态组件、展示型组件主要关注 => UI的展示

  • 类组件、有状态组件、容器型组件主要关注 => 数据逻辑

01 - 类组件

概念

类组件的定义有如下要求
  • 组件的名称是大写字符开头(无论类组件还是函数组件)

  • 类组件需要继承自 React.Component

  • 类组件必须实现render函数

在ES6之前,可以通过create-react-class 模块来定义类组件
但是目前官网建议我们使用ES6的class类定义

使用class定义一个组件:

  • constructor是可选的,我们通常在constructor中初始化一些数据

  • this.state中维护的就是我们组件内部的数据

  • render() 方法是 class 组件中唯一必须实现的方法

/**
 * 内部有单独导出Component
 * import React from 'react';
 * import { Component } from 'react';
 * 所以可以合并
 */

import React, { Component } from 'react';

export default class App extends Component {
  constructor() {
    super();
    this.state = {};
  }

  render() {
    return (
      <div>
        <h2>title</h2>
      </div>
    );
  }
}

render函数的返回值

React 元素
通过jsx编写的代码就会被编译成React.createElement,所以返回的就是一个React元素
render() {
  return (
    <div>
      <h2>title</h2>
    </div>
  );
}
数组或 fragments
react内部会一次遍历数组,展示在界面中
render() {
  // return [1, 2, 3, 4];
  return[
    <h1>h1</h1>,
    <h21>h2</h21>,
    <h3>h3</h3>,
    <h4>h4</h4>
  ]
}
字符串或数值类型
它们在 DOM 中会被渲染为文本节点
render() {
  // return 'afdsafdsa';
  return 123;
}
Portals
可以渲染子节点到不同的 DOM 子树中
布尔类型或 null

什么都不渲染

render() {
  // 界面上什么都没有 => undefined、null、boolean;
  
  // return undefined;
  // return null;
  // return true;
  return false;
}

02 - 函数组件

函数组件是使用function来进行定义的函数
只是这个函数会返回和类组件中render函数返回一样的内容

函数组件有自己的特点(当然,后面有hooks后,就不一样了):

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数

  • this关键字不能指向组件实例(因为没有组件实例)

  • 没有内部状态(state)

// 函数式组件 => 很单纯,只是为了展示数据
function App(props) {
  // 返回值: 和类组件中render函数返回的是一致
  return <h1>App Functional Component</h1>
}

export default App

5. 题外话

Snippets 快捷键 - 插件

  • imr

  • import React from 'react'

  • rce

  • 生成类组件

  • rpce

  • 生成更优化的类组件

二、组件生命周期

1. 生命周期概念

很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期

生命周期和生命周期函数的关系:

  • 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段

  • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程

  • 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程

  • 比如卸载过程(Unmount),组件从DOM树中被移除的过程;

React内部为了告诉我们当前处于哪些阶段,会对组件内部实现的某些函数进行回调
这些函数就是生命周期函数:
  • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调

  • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调

  • 比如实现componentWillUnmount函数:组件即将被移除时,就会回调

  • 可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能

ps : React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的

2. 常用生命周期函数

图解析

constructor

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数

constructor中通常只做两件事情

  • 通过给 this.state 赋值对象来初始化内部的state

  • 为事件绑定实例(this)

import { Component } from 'react';

export default class App extends Component {
  // 1. 第一执行
  constructor() {
    super();
    // 初始化内部的state
    this.state = {};
    // 给方法绑定实例(this)
    this.onClick = this.onClick.bind(this);
  }

  onClick() {
    console.log('click', this);
  }
  
  // 2. 第二执行
  render() {
    return 'ohohoh';
  }
}

componentDidMount

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用

通常进行的操作 :

  • 依赖于DOM的操作可以在这里进行

  • 在此处发送网络请求就最好的地方(官方建议)

  • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)

import { Component } from 'react';

export default class App extends Component {
  // 1. 第一执行
  constructor() {
    super();
    this.state = {};
  }

  // 2. 第二执行
  render() {
    return 'ohohoh';
  }

  // 3. 第三执行 : 组件被渲染到dom,已挂载
  componentDidMount() {
    // 操作dom
    document.querySelector('#root');

    // 网络请求
    // axios.get().then().catch()

    // 添加订阅
  }
}

componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法

通常进行的操作 :

  • 当组件更新后,可以在此处对 DOM 进行操作

  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求

  • 例如,当 props 未发生变化时,则不会执行网络请求)

import { Component } from 'react';

export default class App extends Component {
  // 1. 第一执行
  constructor() {
    super();
    this.state = {
      message: '冲啊,宇智波海绵宝宝'
    };
    console.log('第一执行 : ', 'constructor');
  }

  // 2. 第二执行 || 第五执行
  render() {
    console.log('第二 || 第五执行 : ', 'render');

    const { message } = this.state;
    return (
      <div>
        <h2>{message}</h2>
        <button onClick={_ => this.onClick()}>修改</button>
      </div>
    );
  }

  // 4. 第四执行
  onClick() {
    // 更改完成后,会重新执行render函数
    this.setState({
      message: '宇智波火炎阵!!!'
    });
    console.log('第四执行 : ', 'onClick');
  }

  // 3. 第三执行
  componentDidMount() {
    console.log('第三执行 : ', 'componentDidMount');
  }

  // 5. 第六执行
  // 组件的Dom被更新完成 : Dom发生更新
  componentDidUpdate() {
    console.log('第六执行 : ', 'componentDidUpdate');
  }
}

componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用
import { Component } from 'react';

export default class App extends Component {
  render() {
    return (
      <div>
        <h2>我显示了</h2>
      </div>
    );
  }

  // 当App组件被移除时,会被调用该生命周期
  componentWillUnmount() {
    console.log('第五执行 : ', 'componentWillUnmount');
  }
}

// 其他页面的操作
{
  {
    /* 比如刚开始isShowCommp这个为true,后面变为false */
  }
  isShowCommp && <App />;
}

通常进行的操作 :

  • 在此方法中执行必要的清理操作

  • 清除 time

  • 取消网络请求

  • 清除在 componentDidMount() 中创建的订阅等

3. 不常用生命周期函数

React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用 : React - 过往API

图解析

getDerivedStateFromProps

getDerivedStateFromProps() 在调用 render方法之前调用,在初始化和后续更新都会被调用
返回一个对象来更新 state, 如果返回 null 则不更新任何内容

参数 :

  • 第一个参数为即将更新的 props

  • 第二个参数为上一个状态的 state

  • 可以比较props 和 state来加一些限制条件,防止无用的state更新

getDerivedStateFromProps 是一个静态函数, 不能使用this , 也就是只能作一些无副作用的操作
说实话,不知道干嘛的,先留着

shouldComponentUpdate

shouldComponentUpdate() 在调用 render方法之前调用,控制是否重新执行render函数
该生命周期函数可以做性能优化,
import { Component } from 'react';

export default class App extends Component {
  render() {
    return (
      <div>
        <h2>我显示了</h2>
      </div>
    );
  }

  //  这么设定后,就算执行this.setState()后,render函数不会执行,页面不会刷新
  shouldComponentUpdate() {
    return false;
  }
}

getSnapshotBeforeUpdate

在React更新DOM 之前回调的一个函数
可以获取DOM更新前的一些信息(比如说滚动位置)

三、父子组件间的通信

1. 父组件传递子组件

  • 父组件通过 属性=值 的形式来传递给子组件数据

  • 子组件通过 props 参数获取父组件传递过来的数据

  • 也可以直接展开

代码 : code

父组件

import React, { Component } from 'react';

import Text from './Text';

export class App extends Component {
  render() {
    const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    return (
      <div>
        App
        {/** 传递属性 */}
        <Text name='start' arr={arr} />
      </div>
    );
  }
}

export default App;

子组件

import React, { Component } from 'react';

export class Text extends Component {

  // 如果这里不定义state,也可以不写contructor,内部会自动做这一步操作
  constructor(props) {
    // 相当于this.props = props
    super(props);
  }
  render() {
    // 直接使用
    const { name, arr } = this.props;
    console.log('props', name, arr);
    return (
      <div>
        <span>{name}</span>
        <ul className='item'>
          {arr.map(item => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      </div>
    );
  }
}

export default Text;

参数验证 : propTypes

  • 可以使用Flow或者TypeScrip进行类型验证

  • 也可以通过 prop-types来进行参数验证

import React, { Component } from 'react';
// 1. 导入这个验证的包
import PropTypes from 'prop-types';

export class Text extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'start'
    };
  }
  render() {
    const { name, arr } = this.props;
    console.log('props', name, arr);
    return <div>{name}</div>;
  }
}

// 2. 给类绑定一个propTypes
Text.propTypes = {
  // 3. 设定name => 是个string类型,且是个必穿类型
  name: PropTypes.string.isRequired,
  // 4. 设定arr => 是个array类型
  arr: PropTypes.array
};

export default Text;
更多的验证方式,可以参考官网: proptypes
  • 比如验证数组,并且数组中包含哪些元素

  • 比如验证对象,并且对象中包含哪些key以及value是什么类型

  • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired

参数默认值 : defaultProps

方式一

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export class Text extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'start'
    };
  }
  render() {
    const { name, arr } = this.props;
    console.log('props', name, arr);
    return <div>{name}</div>;
  }
}

Text.propTypes = {
  name: PropTypes.string,
  arr: PropTypes.array
};

// 给参数设定默认值
Text.defaultProps = {
  name: '我是默认值',
  arr: ['我', '是', '默', '认', '值']
};
export default Text;

方式二

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export class Text extends Component {
  // 给参数设定默认值
  static defaultProps = {
    name: '我是默认值',
    arr: ['我', '是', '默', '认', '值']
  };
  constructor(props) {
    super(props);
    this.state = {
      name: 'start'
    };
  }
  render() {
    const { name, arr } = this.props;
    console.log('props', name, arr);
    return <div>{name}</div>;
  }
}

Text.propTypes = {
  name: PropTypes.string,
  arr: PropTypes.array
};

export default Text;

2. 子组件传递父组件

通过props传递消息 : 父组件给子组件传递一个回调函数,子组件中调用这个函数即可

父组件

import React, { Component } from 'react';
import Text from './Text';

export class App extends Component {
  constructor() {
    super();
    this.state = {
      counter: 100
    };
  }

  // 2. 子组件回调该方法
  changeCount(count) {
    this.setState({
      counter: this.state.counter + count
    });
  }

  render() {
    const { counter } = this.state;
    return (
      <div>
        当前计数 : {counter}
        {/** 1. 传递方法到子组件 */}
        <Text addClick={count => this.changeCount(count)} />
      </div>
    );
  }
}

export default App;

子组件

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export class Text extends Component {
  btnClick(count) {
    console.log('count', count);
    // 2. 调用父组件传递过来的方法,并且传递参数过去
    this.props.addClick(count);
  }
  render() {
    return (
      <div>
        <button onClick={() => this.btnClick(1)}>+1</button>
        <button onClick={() => this.btnClick(10)}>+10</button>
        <button onClick={() => this.btnClick(30)}>+30</button>
      </div>
    );
  }
}

// 1. 设定接受参数为方法,也可不设定
Text.propTypes = {
  addClick: PropTypes.func
};

export default Text;

3. 案例

效果

父组件

import React, { Component } from 'react';
import Text from './Text';

export class App extends Component {
  constructor() {
    super();
    this.state = {
      list: ['首页', '详情', '个人'],
      currentIndex: 0
    };
  }
  itemChange(index) {
    this.setState({
      currentIndex: index
    });
  }

  render() {
    const { list, currentIndex } = this.state;
    return (
      <div>
        <Text list={list} itemChange={(index) => this.itemChange(index)} />
        {list[currentIndex]}
      </div>
    );
  }
}

export default App;

子组件

css

.nav{
  display: flex;
  align-items: center;
  justify-content: center;
  list-style: none;
  margin: 0;
  padding: 0;
}
.nav .item{
  flex: 1;
  font-size: 25px;
  padding: 10px;
  text-align: center;
}
.nav .item.active{
  color:red;
  border-bottom: 1px solid red;
}

jsx

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './style.css';

export class Text extends Component {
  constructor(props) {
    super(props);

    this.state = {
      currentIndex: 0
    };
  }
  itemClick(index) {
    this.setState({
      currentIndex: index
    });
    this.props.itemChange(index);
  }
  render() {
    const { currentIndex } = this.state;
    const { list } = this.props;
    return (
      <ul className='nav'>
        {list.map((item, index) => {
          return (
            <li
              key={item}
              className={`item ${currentIndex === index ? 'active' : ''}`}
              onClick={() => this.itemClick(index)}>
              <span>{item}</span>
            </li>
          );
        })}
      </ul>
    );
  }
}

Text.propTypes = {
  list: PropTypes.array.isRequired
};

export default Text;

四、组件中的插槽 - slot

插槽 : 让使用者可以决定某一块区域到底存放什么内容

1. children实现插槽

每个组件都可以获取到 props.children:包含组件的开始标签和结束标签之间的内容
例如 : <子组件> 内容 </子组件>
  • 当内容只有一个元素时 : children === 内容

  • 当内容有多个元素时 : chindren 是一个数组

效果

父组件

import React, { Component } from 'react';
import NavBar from './NavBar';

export class App extends Component {
  render() {
    return (
      <div>
        <NavBar>
          <button>按钮</button>
          <h2>h2</h2>
          <i>i</i>
        </NavBar>

        <NavBar>
          <h3>h3</h3>
          <b>b</b>
          <span>span</span>
        </NavBar>
      </div>
    );
  }
}

export default App;

子组件

jsx

import React, { Component } from 'react';

import './index.css';

export class index extends Component {
  render() {
    // 当有多个子元素时,children是一个数组
    const { children } = this.props;
    return (
      <ul className='nav'>
        {children.map((child, index) => {
          return (
            <li className='item' key={index}>
              {child}
            </li>
          );
        })}
      </ul>
    );
  }
}

export default index;

css

*{
  margin: 0;
  padding: 0;
}
.nav{
  display: flex;
  align-items: center;
  justify-content: center;
  list-style: none;
  margin: 0;
  padding: 0;
  height: 40px;
  margin-bottom: 10px;
  line-height: 40px;
}
.nav .item{
  flex: 1;
  height: 100%;
  font-size: 25px;
  text-align: center;
}
.nav .item:nth-child(1){
  background-color: #f00;
}
.nav .item:nth-child(2){
  background-color: #0f0;
}
.nav .item:nth-child(3){
  background-color: #00f;
}    

2. props实现插槽

通过children实现的方案虽然可行,但是有一个弊端
通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生

五、祖孙组件通信 - Context

如果层级很多的话,一层层传递是非常麻烦,并且代码是非常冗余的
所以React提供了一个API:Context
  • Context 提供了一种在组件之间共享此类值的方式

  • 而不必显式地通过组件树的逐层传递 props

  • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据

  • 例如当前认证的用户、主题或首选语言

相关API

React.createContext

创建一个需要共享的Context对象
  • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值

  • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值

// 封装成一个单独的js
import React from 'react';
export const MyContext = React.createContext();

Context.Provider

包裹需要使用数据的子组件
ps : 每个 Context 对象都会返回一个Provider React 组件,它允许消费组件订阅 context 的变化:
  • Provider 接收一个 value 属性,传递给消费组件

  • 一个 Provider 可以和多个消费组件有对应关系

  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据

  • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染

import React, { Component } from 'react';
// 1. 导入
import { MyContext } from './context/MyContext';

import One from './One';

export class App extends Component {
  render() {
    const obj = { name: 'avc', age: 19 };
    return (
      <div>
        {/* 2. 包裹 =>  value:需要传递的数据*/}
        <MyContext.Provider value={obj}>
          {/* 3. 子组件 */}
          <One></One>
        </MyContext.Provider>
      </div>
    );
  }
}

export default App;

Class.contextType

在子组件中挂载一下context
ps : 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
  • 可以使用 this.context 来消费最近 Context 上的那个值

  • 可以在任何生命周期中访问到它,包括 render 函数中

import React, { Component } from 'react';
import Two from './Two';

// 1. 导入相同的contenxt
import { MyContext } from './context/MyContext';

export class One extends Component {
  render() {
    // 3. 使用context
    console.log(this.context); // {name: 'avc', age: 19}
    return (
      <div>
        One
        {/* 子组件中的子组件,里面进行相同的操纵后也能拿到context传递的数据 */}
        <Two />
      </div>
    );
  }
}

// 2. 挂载过来
One.contextType = MyContext;

export default One;

Context.Consumer

如果子组件是函数式组件的时候,使用Consumer
多个context的时候,也可以使用
// 1. 导入context
import { MyContext } from './context/MyContext';

function Two() {
  return (
    <div>
      <h2>Two</h2>
      {/* 2. 使用MyContext.Consumer */}
      <MyContext.Consumer>
        {/* 3. 这里使用一个函数,value就是传递过来的数据 */}
        {(value) => {
          return <h2>{value.name}</h2>;
        }}
      </MyContext.Consumer>
    </div>
  );
}

export default Two;

多个Context使用

祖元素

import React, { Component } from 'react';
import { MyContext } from './context/MyContext';
import { MyContextTwo } from './context/MycontextTwo';

import One from './One';

export class App extends Component {
  render() {
    const obj = { name: 'aaa', age: 10 };
    return (
      <div>
        {/* 外层context*/}
        <MyContext.Provider value={obj}>
          {/* 内层context*/}
          <MyContextTwo.Provider value={{ name: 'ccc', age: 20 }}>
            {/* 子组件 */}
            <One></One>
          </MyContextTwo.Provider>
        </MyContext.Provider>
      </div>
    );
  }
}

export default App;

子元素 : 类组件

import React, { Component } from 'react';

// 1. 导入相同的contenxt
import { MyContext } from './context/MyContext';
import { MyContextTwo } from './context/MycontextTwo';

export class One extends Component {
  render() {
    // 3. 使用挂载的context
    console.log(this.context); // {name: 'avc', age: 19}
    return (
      <div>
        {/* 4. 使用其他的context */}
        <MyContextTwo.Consumer>
          {/* {(value) => <h5>{value.name}</h5>} */}
          {(value) => this.otherContext(value)}
        </MyContextTwo.Consumer>
      </div>
    );
  }
  // 可以提取出来
  otherContext(value) {
    return <h5>{value.name}</h5>;
  }
}

// 2. 挂载其中一个context过来
One.contextType = MyContext;

export default One;

子元素 : 函数组件

// 1. 导入context
import { MyContext } from './context/MyContext';
import { MyContextTwo } from './context/MycontextTwo';

function Two() {
  return (
    <div>
      {/* 其中一个context */}
      <MyContext.Consumer>
        { (value) => <h2>{value.name}</h2> }
      </MyContext.Consumer>

      {/* 另外一个context */}
      <MyContextTwo.Consumer>
        { (value) => <h2>{value.name}</h2> }
      </MyContextTwo.Consumer>
    </div>
  );
}

export default Two;

默认值defaultValue

context

import React from 'react';
// 定义context的默认值
const defaultValue = { name: '我是默认值' };
export const MyContext = React.createContext(defaultValue);

祖元素

import React, { Component } from 'react';
// 1. 导入context
import { MyContext } from './context/MyContext';

import One from './One';

export class App extends Component {
  render() {
    const obj = { name: 'aaa', age: 10 };
    return (
      <div>
        {/* 2. 使用context.provider */}
        <MyContext.Provider value={obj}></MyContext.Provider>

        {/*3. 子组件没有在context的包裹中 */}
        <One></One>
      </div>
    );
  }
}

export default App;

子元素

import React, { Component } from 'react';

// 1. 导入相同的contenxt
import { MyContext } from './context/MyContext';

export class One extends Component {
  render() {
    // 3. 这里拿到的就是默认数据
    console.log(this.context); // {name: '我是默认值'}

    return <div>哈哈哈</div>;
  }
}

// 2. 挂载context过来
One.contextType = MyContext;

export default One;

六、setState

1. 为什么使用setState

  • 开发中并不能直接通过修改state的值来让界面发生更新

  • React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化

  • 必须通过setState来告知React数据已经发生了变化

  • 在组件中并没有实现setState的方法,为什么可以调用

  • 原因很简单,setState方法是从Component中继承过来的

2. setState用法

用法一

原理 : Object.assign(this.state,newState)
ps : 把新传入的对象和原来的对象合并,若有相同的,取代
this.setState({
   name: 'bbb',
   age: 18
});

用法二

/**
 * 传入回调函数 => 返回一个新对象
 *    好处一 : 可以进行逻辑编写
 *    好处二 : 当前回调函数会将组件之前的state和组件接收的props传递进来
 */
this.setState((preState, props) => {
  // 返回一个新对象
  return {
    name: 'bbb',
    age: 18
  };
});

3. setState异步更新

setState的更新是异步的
this.setState({
  name: 'bbb',
  age: 18
});
console.log(this.state.name, this.state.age); // aaa 10
  • 最终打印结果是aaa 10

  • 可见setState是异步的操作,并不能在执行完setState之后立马拿到最新的state的结果

01 - 优点

  • setState设计为异步,可以显著的提升性能

  • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低

  • 最好的办法应该是获取到多个更新,之后进行批量更新

  • 如果是同步,更新了state,但是还没有执行render函数,那么state和props不能保持同步

  • state和子组件的props不能保持一致性,会在开发中产生很多的问题

02 - 获取异步的结果

回调函数

setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行
itemClick() {
  this.setState(
    {
      name: 'bbb',
      age: 18
    },
    // 使用回调函数
    () => {
      console.log(this.state.name, this.state.age); // bbb 18
    }
  );
}

生命周期

itemClick() {
  this.setState({
    name: 'bbb',
    age: 18
  });
}
componentDidUpdate() {
  // 生命周期获取修改后的值
  console.log(this.state.name, this.state.age); // bbb 18
}

03 - React版本

React18之前

分成两种情况:

  • 在组件生命周期或React合成事件中,setState是异步

  • 在setTimeout或者原生dom事件中,setState是同步

React18之后

在React18之后,默认所有的操作都被放到了批处理中(异步处理)
默认是异步,可以通过 flushSync 设置成同步
import React, { Component } from 'react';
// 1. 导入
import { flushSync } from 'react-dom';

export class App extends Component {
  constructor() {
    super();
    this.state = {
      name: 'aaa',
      age: 10
    };
  }
  itemClick() {
    // 2. 包裹
    flushSync(() => {
      this.setState({
        name: 'bbb'
      });
    });
    // 3. 获取到值 => 会先执行rander,然后再执行这里
    console.log(this.state); // {name:'bbb',age:10}
  }

七、React性能优化

1. Diff算法优化

概念

渲染流程

  • rander函数中返回JSX

  • JSX会创建对应的ReactElement

  • ReactElement最终会形成一个树结构,这个树结构就是虚拟DOM

  • 最后React会根据虚拟DOM渲染成真实DOM

更新流程

diff算法

  • React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树

  • React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI

  • 这个判断的过程就是diff算法

优化

React对这个算法进行了优化,将其优化成了O(n)

同层节点之间相互比较,不会垮节点比较

不同类型的节点,产生不同的树结构

开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定

2. 类组件性能优化SCU

问题

只要是修改了 App中的数据,所有的组件都需要重新render,进行diff算法,性能必然是很低的

事实上,很多的组件没有必须要重新render
调用render应该有一个前提,就是依赖的数据(state、 props)发生改变时,再调用自己的render方法
import React, { Component } from 'react';
import Home from './Home';

export class App extends Component {
  constructor() {
    super();
    this.state = {
      name: 'aaa',
      age: 10
    };
  }
  itemClick() {
    this.setState({
      name: 'bbb'
    });
  }

  /**
   * 问题一 : 调用this.setState后,就算没有相关依赖的变量,
              Home子组件乃至子组件的子组件,它们的render方法也会执行

   * 问题二 : this.setState({
                name: 'aaa'
              })  没有修改内容,同样会全部重新渲染

   * 效率极其低下
   */

  render() {
    console.log('render执行');
    const { name, age } = this.state;
    return (
      <div>
        <h1 onClick={() => this.itemClick()}>
          {name} - {age}
        </h1>
        

        {/* 可以不必重新渲染 */}
        <Home />
      </div>
    );
  }
}

export default App;

方法

通过shouldComponentUpdate方法来控制render方法是否被调用

shouldComponentUpdate

import React, { Component } from 'react';
import Home from './Home';

export class App extends Component {
  constructor() {
    super();
    this.state = {
      name: 'aaa',
      age: 10
    };
  }
  itemClick() {
    this.setState({
      name: 'bbb'
    });
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log(nextProps, '没有props就为空');
    console.log(`旧 : ${this.state.name}, 新 : ${nextState.name}`); // aaa  bbb

    // 当不同时,才重新渲染, render函数执行
    // return this.state.name !== nextState.name || this.state.age !== nextState.age;

    // 如果作为子组件,有props的情况下,还需要队props进行对比
    return (
      this.state.name !== nextState.name ||
      this.state.age !== nextState.age ||
      // props
      this.props.message !== nextProps.message
    );
  }

  render() {
    console.log('render执行');
    const { name, age } = this.state;
    return (
      <div>
        <h1 onClick={() => this.itemClick()}>
          {name} - {age}
        </h1>
        {/* 可以不必重新渲染 */}
        <Home />
      </div>
    );
  }
}

export default App;

PureComponent

如果所有的类,都需要手动来实现 shouldComponentUpdate,那么会给开发增加非常多的工作量
React已经考虑到了这一点,所以React已经默认实现好了
将class继承自PureComponent
// 1. 不导入Component,导入PureComponent
import React, { PureComponent } from 'react';
import Home from './Home';

// 2. 不继承Component,继承PureComponent
export class App extends PureComponent {

  render() {
    console.log('render执行');
    return (
      <div>
        <h1>App</h1>
        <Home />
      </div>
    );
  }
}

export default App;
PureComponent : 本质是进行了一个浅层比较 => 只比较第一层,不比较深层

3. 函数组件性能优化SCU

需要使用一个高阶组件memo => 包裹一下函数组件即可
import { memo } from 'react';

// 未使用memo的函数组件
// export default function Home(props) {
//   console.log('home render');
//   return <h2>123:{props.name}</h2>;
// }

// 使用了memo的函数组件
export default memo(function Home(props) {
  console.log('home render');
  return <h2>123:{props.name}</h2>;
});

4. 不可变数据的力量

不要直接操作,而是赋予新的地址
ps : 在setState中,进行结构即可,不管是整体,还是修改单个属性依旧如此
import React, { PureComponent } from 'react';

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {
      friend: ['a', 'b', 'c']
    };
  }
  addClick() {
    const newEl = 'd';
    /**
     * 直接修改原有state,重新设置一遍
       在PureComponent中,是不会重新渲染页面的
       因为shouldComponentUpdate回调函数里,nextState - this.state两个值是一样的,就不会触发页面的刷新
     */
    // this.state.friend.push(newEl);
    // this.setState({
    //   friend: this.state.friend
    // });

    /**
     * 正确方式一
     */
    // this.state.friend.push(newEl);
    // this.setState({
    //   // 相当于拿新的数组,覆盖了原来的数组,地址不一样了
    //   friend: [...this.state.friend]
    // });
    /**
     * 正确方式二
     */
    const friend = [...this.state.friend];
    friend.push(newEl);
    this.setState({
      friend
    });
  }

  render() {
    console.log('render执行');
    const { friend } = this.state;
    return (
      <div>
        <ul>
          {friend.map((item, index) => {
            return <li key={index}>{item}</li>;
          })}
        </ul>
        <button onClick={(e) => this.addClick()}>增加元素</button>
      </div>
    );
  }
}

export default App;

八、Ref获取DOM和组件

Ref获取DOM

方式一 : 传入字符串

使用时通过 this.refs.传入的字符串格式 获取对应的元素 => 已经被废弃了
import React, { PureComponent } from 'react';

export class App extends PureComponent {
  onNativeClick() {
    // 2. 通过ref属性获取到DOM节点 不过已经被废弃了
    console.log('this.refs.coderRef', this.refs.coderRef);
  }
  render() {
    return (
      <div>
        {/* 1. 增加一个ref属性 */}
        <h2 ref='coderRef' onClick={() => this.onNativeClick()}>
          hello world
        </h2>
      </div>
    );
  }
}

export default App;

方式二 : 传入一个对象 => 推荐

对象是通过 React.createRef() 方式创建出来的
使用时获取到创建的对象其中有一个current属性就是对应的元素
// 通过createRef获取dom元素
import React, { PureComponent, createRef } from 'react';

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {};
    // 1. 通过createRef创建一个ref对象,因为不需要响应式,所以不需要放在state中
    this.coderRef = createRef();
  }
  onNativeClick() {
    // 3. 通过current属性获取dom元素
    console.log('this.coderRef', this.coderRef.current);
  }
  render() {
    return (
      <div>
        {/* 2. 绑定到ref属性上 */}
        <h2 ref={this.coderRef} onClick={() => this.onNativeClick()}>
          hello world
        </h2>
      </div>
    );
  }
}

export default App;

方式三 : 传入一个函数

该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存
使用时,直接拿到之前保存的元素对象即可
import React, { PureComponent } from 'react';

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {};
    // 1. 创建一个ref对象
    this.coderRef = null;
  }
  onNativeClick() {
    // 3. 拿到dom元素
    console.log('this.coderRef', this.coderRef);
  }
  render() {
    return (
      <div>
        <h2
          // 2. 挂载后,会自动进行回调该函数,可以在这直接获取dom元素
          ref={(el) => (this.coderRef = el)}
          onClick={() => this.onNativeClick()}>
          hello world
        </h2>
      </div>
    );
  }
}

export default App;

Ref获取类组件

ref 的值根据节点的类型而有所不同:
  • 当 ref 属性用于 HTML 元素时

  • 构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性

  • 当 ref 属性用于自定义 class 组件时

  • ref 对象接收组件的挂载实例作为其 current 属性

import React, { PureComponent, createRef } from 'react';

// 子组件
class Text extends PureComponent {
  text() {
    console.log('Text组件的text方法');
  }
  render() {
    return <h2>Text</h2>;
  }
}

// 父组件
export class App extends PureComponent {
  constructor() {
    super();
    this.state = {};
    // 1. 通过createRef创建一个ref对象
    this.textRef = createRef();
  }
  onComponentClick() {
    // 3. 拿到子组件实例
    console.log('this.textRef', this.textRef.current);
    // 4. 调用子组件的方法
    this.textRef.current.text();
  }
  render() {
    return (
      <div>
        {/* 2. 绑定到ref属性上*/}
        <Text ref={this.textRef} />
        <button onClick={() => this.onComponentClick()}>获取子组件</button>
      </div>
    );
  }
}

export default App;

Ref获取函数组件

  • 不能在函数组件上使用 ref 属性,因为函数组件没有实例

  • 但是某些时候,可能想要获取函数式组件中的某个DOM元素

  • 这个时候可以通过 React.forwardRef 高阶组件

import React, { PureComponent, createRef, forwardRef } from 'react';

// 子组件
/**
 *  forwardRef 用于转发 ref,使得 ref 可以在函数组件中传递
 * 1. forwardRef 接受一个渲染函数,函数的参数为 props 和 ref
 * 2. forwardRef 返回一个组件,这个组件接受 props 和 ref 两个参数
 */
const Text = forwardRef(function (props, ref) {
  return (
    <div>
      <h2>hello - 001</h2>
      {/* 3. 把传递过来的ref绑定到这个元素上 */}
      <h3 ref={ref}>hello - 002</h3>
      <h4>hello - 003</h4>
    </div>
  );
});

// 父组件
export class App extends PureComponent {
  constructor() {
    super();
    this.state = {};
    // 1. 通过createRef创建一个ref对象
    this.textRef = createRef();
  }
  onComponentClick() {
    // 4. 拿到子组件的某一个dom元素
    console.log('this.textRef', this.textRef.current);
  }
  render() {
    return (
      <div>
        {/* 2. 绑定到ref属性上*/}
        <Text ref={this.textRef} />
        <button onClick={() => this.onComponentClick()}>获取子组件</button>
      </div>
    );
  }
}

export default App;

九、受控组件 && 非受控组件

1. 受控组件

一般来说,当给表单组件绑定了value属性后,该表单组件就变成了受控组件
只能通过React来使用 setState()进行更新
  • 在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value

  • 这使得 React 的 state 成为唯一数据源

  • handleUsernameChange 在每次按键时都会执行并更新 React 的 state

  • 因此显示的值将随着用户输入而更新

  • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件

input

import React, { PureComponent } from 'react';

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {
      // 1. 定义需要绑定到输入框的数据
      username: '',
      password: ''
    };
  }
  // 3. 监听到值改变后,更新message的值,再修改输入框的值
  inputHandleChange(e) {
    this.setState({
      // 使用计算属性
      [e.target.name]: e.target.value
    });
  }
  submitHandle() {
    console.log(this.state);
  }
  render() {
    const { username, password } = this.state;
    return (
      <div>
        {/* 2. 绑定到value属性上 => 监听onChange方法(不监听的话,无法输入值) */}
        <label htmlFor='username'>
          账号 :
          <input
            id='username'
            type='text'
            name='username'
            value={username}
            onChange={(e) => this.inputHandleChange(e)}
          />
        </label>
        <label htmlFor='password'>
          密码 :
          <input
            id='password'
            type='password'
            name='password'
            value={password}
            onChange={(e) => this.inputHandleChange(e)}
          />
        </label>
        <button onClick={(e) => this.submitHandle(e)}>注册</button>
      </div>
    );
  }
}

export default App;

checkbox

import React, { PureComponent } from 'react';

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {
      // 1. 定义需要绑定到输入框的数据
      agree: false,
      hobbies: [
        { key: 'sing', value: '唱', isChecked: false },
        { key: 'dance', value: '跳', isChecked: false },
        { key: 'rap', value: 'rap', isChecked: false }
      ]
    };
  }
  // 3. 监听到值改变后,更新message的值,再修改输入框的值
  checkHandleChange(e, index) {
    // 单选
    if (index === -1) {
      return this.setState({
        [e.target.name]: e.target.checked
      });
    }
    // 多选
    const { hobbies } = this.state;
    hobbies[index].isChecked = e.target.checked;
    this.setState({
      // 使用计算属性
      hobbies: [...hobbies]
    });
  }
  submitHandle() {
    console.log(this.state);
    console.log(
      '爱好',
      this.state.hobbies.filter((item) => item.isChecked).map((item) => item.key)
    );
  }
  render() {
    const { agree, hobbies } = this.state;
    return (
      <div>
        {/* 2. 绑定到value属性上 => 监听onChange方法(不监听的话,无法输入值) */}
        <div className='one'>
          <span>单选 === </span>
          <label htmlFor='agree'>
            <input
              id='agree'
              type='checkbox'
              name='agree'
              checked={agree}
              onChange={(e) => this.checkHandleChange(e, -1)}
            />
            同意
          </label>
        </div>
        <div className='one'>
          <span>多选 === </span>
          {hobbies.map((item, index) => {
            return (
              <label htmlFor={item.key} key={item.key}>
                <input
                  id={item.key}
                  type='checkbox'
                  name={item.key}
                  checked={item.isChecked}
                  onChange={(e) => this.checkHandleChange(e, index)}
                />
                {item.value}
              </label>
            );
          })}
        </div>

        <button onClick={(e) => this.submitHandle(e)}>注册</button>
      </div>
    );
  }
}

export default App;

select

import React, { PureComponent } from 'react';

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {
      fruits: 'banana',
      fruitsArr: ['banana']
    };
  }
  // 3. 监听到值改变后,更新message的值,再修改输入框的值
  selectHandleChange(e, type) {
    // 单选
    if (type === 'single') {
      return this.setState({
        fruits: e.target.value
      });
    }

    // 多选  => e.target.selectedOptions 可以拿到所有多选的数据,按住shift多选
    const options = Array.from(e.target.selectedOptions);
    const optionsValue = options.map((item) => item.value);
    this.setState({
      // 使用计算属性
      fruitsArr: [...optionsValue]
    });
  }
  submitHandle() {
    console.log(this.state);
  }
  render() {
    const { fruits, fruitsArr } = this.state;
    return (
      <div>
        {/* 2. 绑定到value属性上 => 监听onChange方法(不监听的话,无法输入值) */}
        <div className='one'>
          <span>单选 === </span>
          <select value={fruits} onChange={(e) => this.selectHandleChange(e, 'single')}>
            <option value='apple'>苹果🍎</option>
            <option value='banana'>香蕉🍌</option>
            <option value='orange'>橘子🍊</option>
          </select>
        </div>
        <hr />
        <hr />
        <hr />
        <div className='one'>
          <span>多选 === </span>
          <select
            value={fruitsArr}
            multiple
            onChange={(e) => this.selectHandleChange(e, 'multiple')}>
            <option value='apple'>苹果🍎</option>
            <option value='banana'>香蕉🍌</option>
            <option value='orange'>橘子🍊</option>
          </select>
        </div>

        <button onClick={(e) => this.submitHandle(e)}>注册</button>
      </div>
    );
  }
}

export default App;

2. 非受控组件

React推荐大多数情况下使用 受控组件 来处理表单数据
  • 一个受控组件中,表单数据是由 React 组件来管理的

  • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理

  • 如果要使用非受控组件中的数据,可以使用 ref 来从DOM节点中获取表单数据

代码

import React, { PureComponent, createRef } from 'react';

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {
      message: '123'
    };
    // 1. 初始化ref
    this.inputRef = createRef();
  }

  componentDidMount() {
    // 3. 可以通过ref获取到DOM元素,然后绑定事件,监听值的变化
    this.inputRef.current.addEventListener('input', (e) => {
      console.log(e.target.value);
    });
  }

  submitHandle() {
    // 4. 通过ref获取到值
    console.log(this.inputRef.current.value);
  }
  render() {
    const { message } = this.state;
    return (
      <div>
        {/* 2. 绑定ref,并使用defaultValue */}
        <input type='text' defaultValue={message} ref={this.inputRef} />

        <button onClick={(e) => this.submitHandle(e)}>注册</button>
      </div>
    );
  }
}

export default App;

效果

十、高阶组件

1. 定义

高阶组件是参数为组件,返回值为新组件的函数
  • 高阶组件的英文是 Higher-Order Components,简称为 HOC

  • 高阶组件本身不 是一个组件,而是一个函数

  • 这个函数的参数是一个组件,返回值也是一个组件


/**
 * 高阶组件是一个函数,参数是一个组件,返回值也是一个组件
 *
 * 高阶组件的作用是为了增强组件,给组件添加一些额外的功能
 * 比如:给组件添加一些公共的状态、公共的逻辑、公共的生命周期
 * 或者是给组件添加一些公共的UI
 * 或者是给组件添加一些公共的数据
 * 或者是给组件添加一些公共的方法等等
 * 总之,高阶组件的作用就是为了增强组件,给组件添加一些额外的功能。
 */
// 定义高阶组件,参数是一个组件,返回值也是一个组件,
function hoc(wrapperComponent) {
  // 1. 定义类组件
  return class extends PureComponent {};
  
  // 2. 定义函数组件
  // return function () {
  //   return <div>函数组件</div>;
  // }
}
高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式

2. 应用

props的增强

不修改原有代码的情况下,添加新的props

代码

import React, { PureComponent } from 'react';

// 1. 定义高阶组件
function enhanceProps(OriginComponent) {
  return class extends PureComponent {
    constructor(porps) {
      super(porps);
      this.state = { name: 'coder', age: 18 };
    }
    render() {
      // 2. 注入props 本身的props和传入的props,都要注入到OriginComponent中
      return <OriginComponent {...this.props} {...this.state} />;
    }
  };
}

// 3. 函数组件使用高阶组件
const Home = enhanceProps(function (props) {
  const str = 'home';
  return (
    <div>
      <h2>Home</h2>
      <h3>
        父组件传递过来的props ={'>'} level : {props.level}
      </h3>
      <h3>
        增强的props ={'>'} name : {props.name} - age : {props.age}
      </h3>
      <h3>
        本身定义的数据 ={'>'} str : {str}
      </h3>
    </div>
  );
});

// 4. 类组件使用高阶组件
const Text = enhanceProps(
  class extends PureComponent {
    constructor(props) {
      super(props);
      this.state = { str: 'text' };
    }
    render() {
      const { name, age, level } = this.props;
      const { str } = this.state;
      return (
        <div>
          <h2>Text:</h2>
          <h3>
            父组件传递过来的props ={'>'} level : {level}{' '}
          </h3>
          <h3>
            增强的props ={'>'} name : {name} - age : {age}
          </h3>
          <h3>
            本身定义的数据 ={'>'} str : {str}{' '}
          </h3>
        </div>
      );
    }
  }
);

export class App extends PureComponent {
  render() {
    return (
      <div>
        <Home level='66' />
        <Text level='99' />
      </div>
    );
  }
}

export default App;

效果

Context共享

共享全局主题颜色

context

import React from 'react';

export const ThemeContext = React.createContext();

父组件

import React, { PureComponent } from 'react';
import { ThemeContext } from './context/ThemeContext';
import Home from './components/Home';

export class App extends PureComponent {
  render() {
    return (
      <div>
        App
        {/* 使用context提供主题数据 */}
        <ThemeContext.Provider value={{ color: 'red', size: 30 }}>
          <Home />
        </ThemeContext.Provider>
      </div>
    );
  }
}

export default App;

高阶组件

// 使用context
import { ThemeContext } from '../context/ThemeContext';

export default function withTheme(OriginComponent) {
  // 返回一个函数组件
  return (props) => {
    // 渲染的内容
    return (
      <ThemeContext.Consumer>
        {/* 使用context共享数据 */}
        {
          (value) => {
            return <OriginComponent {...value} {...props} />;
          }
        }
      </ThemeContext.Consumer>
    );
  };
}

子组件

import React, { PureComponent } from 'react';
import withTheme from '../hoc/with-theme';

export class Home extends PureComponent {
  render() {
    return (
      <div>
        Home
        {/* 使用高阶组件中context的数据 */}
        {this.props.color} {/* red */}
        {this.props.size} {/* 30 */}
      </div>
    );
  }
}

// 实际导出的是一个高阶组件
export default withTheme(Home);

渲染判断鉴权

父组件

import React, { PureComponent } from 'react';
import Home from './components/Home';

export class App extends PureComponent {
  loginClick() {
    // 模拟登录
    sessionStorage.setItem('token', 123321);
    
    // 因为没有调用setState,不会重新渲染页面
    // 所以进行强制刷新, 重新渲染组件, 但是不推荐使用
    this.forceUpdate();
  }
  render() {
    return (
      <div>
        App
        <Home />
        <button onClick={() => this.loginClick()}>登录</button>
      </div>
    );
  }
}

export default App;

高阶组件

export default function loginAuth(OriginComponent) {
  return (props) => {
    // 1. 从localStorage中获取token
    const token = sessionStorage.getItem('token');
    // 2. 判断token是否存在
    if (token) {
      // 2.1 如果存在,渲染组件
      return <OriginComponent {...props} />;
    } else {
      // 2.2 如果不存在,跳转到登录页面
      return <h2>请先登录</h2>;
    }
  };
}

子组件

import React, { PureComponent } from 'react';
import loginAuth from '../hoc/login-auth';

export class Home extends PureComponent {
  render() {
    return <div>Home</div>;
  }
}

// 实际导出的是一个高阶组件
export default loginAuth(Home);

3. 意义

利用高阶组件可以针对某些React代码进行更加优雅的处理

01 - mixin

早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:
  • Mixin 可能会相互依赖,相互耦合,不利于代码维护

  • 不同的Mixin中的方法可能会相互冲突

  • Mixin非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

02 - hoc

HOC也有自己的一些缺陷:
  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套

  • 这让调试变得非常困难

  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突

03 - hooks

Hooks的出现,是开创性的,它解决了很多React之前的存在的问题
  • 比如this指向问题

  • 比如hoc的嵌套复杂度问题

十一、Portals

某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中
默认都是挂载到id为root的DOM元素上的

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:

  • 第一个参数(child)是任何可渲染的 React 子元素

  • 例如一个元素,字符串或 fragment

  • 第二个参数(container)是一个 DOM 元素

1. 基本使用

在根组件新创建一个
<div id="root"></div>
<div id="coder"></div>

代码

import React, { PureComponent } from 'react';
// 1. 导入 createPortal,用于创建一个传送门
import { createPortal } from 'react-dom';

export class App extends PureComponent {
  render() {
    return (
      <div>
        App
        {/* 2. 使其挂载到根组件之外的coder -> div中 */}
        {createPortal(<h2>我是传送门</h2>, document.querySelector('#coder'))}
      </div>
    );
  }
}

export default App;

效果

2. 封装

根文件index.html

父组件

import React, { PureComponent } from 'react';
import Coder from './components/Coder';

export class App extends PureComponent {
  render() {
    return (
      <div>
        App
        {/* 在子组件中间写内容,类似插槽的效果 */}
        <Coder>
          <h2>coder</h2>
          <div>hello world</div>
        </Coder>
      </div>
    );
  }
}

export default App;

子组件

import { PureComponent } from 'react';
import { createPortal } from 'react-dom';

export class Coder extends PureComponent {
  render() {
    // createPortal(要渲染的内容, 挂载的DOM节点)
    // this.props.children => 是从父组件传递过来的内容
    return createPortal(this.props.children, document.getElementById('coder'));
  }
}

export default Coder;

效果

十二、fragment

之前的开发中,总是在一个组件中返回内容时包裹一个div元素
如果希望可以不渲染这样一个div应该如何操作
  • 使用Fragment

  • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点

  • 类似vue的template、小程序的block

1. 基本使用

import React, { PureComponent, Fragment } from 'react';

export class App extends PureComponent {
  render() {
    return (
      // 把Fragment当做一个占位符使用,不会渲染到页面上
      <Fragment>
        <h2>App标题</h2>
        <p>冲啊</p>
      </Fragment>
    );
  }
}

export default App;

2. 语法糖🍬

Fragment的短语法:
  • 它看起来像空标签 <> </>

  • 但是,如果需要在Fragment中添加key,那么就不能使用短语法

render() {
  return (
    // Fragment语法糖🍬
    <>
      <h2>App标题</h2>
      <p>冲啊</p>
    </>
  );】
}

十三、StrictMode

StrictMode 是一个用来突出显示应用程序中潜在问题的工具:
  • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI

  • 它为其后代元素触发额外的检查和警告

  • 严格模式检查仅在开发模式下运行

  • 它们不会影响生产构建

可以为应用程序的任何部分启用严格模式
不会对 Header 和 Footer 组件运行严格模式检查

1. 全局启动

import ReactDOM from 'react-dom/client';

import App from './App.jsx';
import { StrictMode } from 'react';

const root = ReactDOM.createRoot(document.querySelector('#root'));

// 开启严格模式,检查不安全的生命周期,过时的ref等
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

2. 部分启动

import React, { PureComponent, StrictMode } from 'react';
import Home from './components/Home';
import Coder from './components/Coder';

export class App extends PureComponent {
  render() {
    return (
      <>
        <h2>App</h2>
        {/* 给Home组件开启严格模式 => Home以及它的所有后代元素都将进行检查 */}
        <StrictMode>
          <Home />
        </StrictMode>
        <Coder />
      </>
    );
  }
}

export default App;

3. 严格模式检查的内容

识别不安全的生命周期

使用过时的ref API

检查意外的副作用

constructor、render、以及很多的生命周期函数,会在第一次渲染的时候, 刻意执行两次
  • 这个组件的constructor会被调用两次

  • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用

  • 在生产环境中,是不会被调用两次的

使用废弃的findDOMNode方法

在之前的React API中,可以通过findDOMNode来获取DOM,已经不推荐使用了

检测过时的context API

  • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的

  • 目前这种方式已经不推荐使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/715159.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web安全-AntSword(中国蚁剑)Webshell管理工具使用

为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 工具下载地址02 工具介绍03 使用案例04 参考资料 01 工具下载地址 https://github.com/AntSwordProject/蚂剑工具的下载分为两个部分&#xff0c;一个是项目核心源码antSword&#xff0c;另一个是…

【算法】树形DP ①(树的直径)

文章目录 知识准备例题543. 二叉树的直径124. 二叉树中的最大路径和2246. 相邻字符不同的最长路径 相关题目练习687. 最长同值路径 https://leetcode.cn/problems/longest-univalue-path/solution/shi-pin-che-di-zhang-wo-zhi-jing-dpcong-524j4/1617. 统计子树中城市之间最大…

测试的流程,jira工具的使用

目录&#xff1a; 测试流程价值与体系测试计划业务架构分析思路bug基本概念bug处理流程测试总结业务架构分析工具plantuml测试流程管理jira系统-测试流程定制测试流程管理 jira 系统-Bug管理流程定制 1.测试流程价值与体系 软件测试流程 完成软件测试工作的必要步骤 测试流…

用图计算解密大脑,蚂蚁技术研究院与复旦联手启动类脑研究

大脑为什么会产生意识&#xff1f;我们为什么会失眠&#xff1f;帕金森、阿尔兹海默等神经性疾病如何有效治疗&#xff1f;这一切谜题的背后都绕不开脑科学。可以说脑科学问题是人类面临的基础科学问题之一&#xff0c;是我们解密人类自身的“终极疆域”。 我们的大脑由大约86…

第十二章线程池

文章目录 享元模式手写数据库连接池 为什么需要线程池自定义线程池自定义拒绝策略接口自定义任务队列自定义线程池 JDK中的线程池常用的线程池的类和接口的之间的关系线程池状态构造方法线程池的工作流程拒绝策略 ExecuctorsnewFixedThreadPoolnewCachedThreadPoolnewSingleThr…

【Matlab】智能优化算法_平衡优化器算法EO

【Matlab】智能优化算法_平衡优化器算法EO 1.背景介绍2.数学模型2.1 初始化和功能评估2.2 平衡池和候选者&#xff08;Ceq&#xff09;2.3 指数项&#xff08;F&#xff09;2.3 生成率&#xff08;G&#xff09; 3.文件结构4.伪代码5.详细代码及注释5.1 EO.m5.2 Get_Functions_…

Linux基础服务7——lamp架构

文章目录 一、基本了解二、单机部署LAMP2.1 安装httpd2.2 安装mysql2.3 安装php环境2.4 配置apache 三、分离部署四、脚本单机部署 一、基本了解 LAMP架构介绍&#xff1a; lamp是由LinuxApacheMysql/MariaDBPhp/Perl/Python的一组动态网站或者服务器的开源软件。LAMP指Linux&a…

多元回归预测 | Matlab基于深度置信网络(DBN)回归预测,matlab代码回归预测,多变量输入模型

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元回归预测 | Matlab多元回归预测 | Matlab基于深度置信网络(DBN)回归预测,matlab代码回归预测,多变量输入模型 评价指标包括:MAE、RMSE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 部分…

原来工作18年的企业大佬都是这样自定义企业微信扫码登录的样式

前言 由于企业微信扫码登录都是固定样式和模板&#xff0c;每个公司在前期使用的时候可能会使用原样的模版&#xff0c;随着业务场景的复杂及细分场景优化&#xff0c;这个固定样式的模版满足不了企业的需求&#xff0c;所以需要对模版进行改造&#xff0c;使它更加贴合企业业务…

【elementplus】解决el-table开启show-overflow-tooltip后,tooltip的显示会被表格边框遮挡的问题

如图所示&#xff1a; 原因&#xff1a; 1. el-table没有设置高度&#xff1b;2.就是被遮住了 解决&#xff1a; 方法一&#xff1a;给el-table设置高度 方法二: .el-table {overflow: visible !important;}如果不想给el-table设置高度&#xff0c;就直接使用方法二解决即可

Pycharm使用Anoconda配置虚拟环境

目录 1.Anoconda的介绍 2.Anaconda的作用 3.Anaconda的安装 4.Anaconda的配置 4.1添加镜像源 4.2创建、使用并切换虚拟环境 5.pycharm的集成 1.Anoconda的介绍 Anaconda是一个可用于科学计算的 Python 发行版&#xff0c;可以便捷获取和管理包&#xff0c;同时对环境进行…

Java内存结构分析

一、Java内存结构划分 Java虚拟机的运行时数据区域主要包括程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。 &#xff08;1&#xff09;程序计数器&#xff08;Program Counter Register&#xff09; 它是一块较小的内存空间&#xff0c;它可以看作是当前线程所执行的字…

SpringBoot项目整合WebSocket+netty实现前后端双向通信(同时支持前端webSocket和socket协议哦)

目录 前言 技术栈 功能展示 一、springboot项目添加netty依赖 二、netty服务端 三、netty客户端 四、测试 五、代码仓库地址 专属小彩蛋&#xff1a;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家…

OLS回归分析理论基础

前言 由于目前的实证研究中需要对变量间的因果关系进行定量分析&#xff0c;所以以伍德里奇和陈强两版本计量经济学教材为基础&#xff0c;有针对性的整理出OLS回归的相关知识&#xff0c;以解决实证分析中的实际问题。 1&#xff09;本文重点&#xff1a;本文重点研究OLS下面板…

vs code koroFileHeader插件相关配置

https://www.cnblogs.com/melodyjerry/p/14449990.html 一、安装插件 koroFileHeader 插件作用&#xff1a;在文件顶部添加头部注释 VS Code 中搜索并安装插件 koroFileHeader&#xff1b; 点击插件右下方的 设置 按钮 > 扩展设置 > 点击 在settings.json 中编辑&…

数据结构 线性表的定义和基本操作(以顺序表为例)

名人说&#xff1a;一花独放不是春&#xff0c;百花齐放花满园。——《增广贤文》 作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 〇、线性表是什么&#xff1f;1、定义2、特点3、基本操作 一、代码实现二、思路阐明…

记录使用ffmpeg把mp4转换成m3u8

背景:公司需要上一些视频资源,平均每一个都在600m以上,经过考虑以后采取视频分片以后上传到oss上进行加速播放的流程.这里记录一下使用ffmpeg进行转换视频格式的过程中的一些命令. 准备工作: 下载ffmpeg到本地,以及配置ffmpeg到环境变量中,这里就不多说了. 使用的时候先打开…

软考每年成绩几月公布 软考考试历年成绩查询时间

软考成绩一般在考试结束后两个月内公布&#xff0c;上半年软考考试成绩一般在7月查询&#xff0c;下半年软考考试成绩一般在12月查询。软考成绩在中国计算机技术职业资格网公布&#xff0c;从2022年起&#xff0c;软考的合格标准为满分的60%&#xff0c;即45分合格。 软考考试…

MybatisX插件自动生成sql失效问题的详细分析

mybatis框架提供了非常好用的逆向工程插件&#xff0c;但是根据数据库驱动版本的不同会出现一些问题。 在使用mybatisX插件的时候使用Generate mybatis sql无法实现自动生成sql 解决方案&#xff1a; 1.首先检查自己的数据库中表是否有主键&#xff0c;如果没有主键是不会生…

流及其相关操作

本文已收录于专栏 《Java》 目录 概念说明流 流的分类根据数据流向的不同&#xff0c;可以分为输入流和输出流。根据处理单位的不同&#xff0c;可以分为字节流和字符流。根据功能不同&#xff0c;可以分为节点流和处理流。 提供服务过滤操作&#xff08;Filter&#xff09;映射…