React教程 - 组件

202 阅读3分钟

函数组件

原理

创建

在src目录中,创建一个 xxx.js 的文件,就是要创建一个组件;在此文件中,创建一个函数,让函数返回JSX视图【或者JSX元素、virtualDOM虚拟DOM对象】;这就是创建一个“函数组件”

const DemoOne = function DemoOne(props) {
    console.log(props);
    return <div className="demo-box">Demo-One</div>;
};
export default DemoOne;

调用

基于ES6规范,但如创建的组件【可以忽略 .jsx 后缀名】,然后像写标签一样调用这个组件即可

<Component/> // 单闭合调用

<Component>...</Component> // 双闭合调用

<DemoOne title="我是标题" x={10} data={[100, 200]} className="box" style={{ fontSize: '20px' }} />

如果设置的属性值不是字符串格式,需要基于“{}胡子语法”进行嵌套

调用组件的时候,我们可以把一些数据/信息基于 属性props 的方式,传递给组件!

渲染机制

基于`babel-preset-react-app`把调用的组件转换为`createElement格式`
    React.createElement(DemoOne, {
        title: "\u6211\u662F\u6807\u9898",
        x: 10,
        data: [100, 200],
        className: "box",
        style: {
            fontSize: '20px'
        }
    })
把`createElement`方法执行,创建出一个`virtualDOM对象`
    {
        $$typeof: Symbol(react.element),
        key: null,
        props: {title: '我是标题', x: 10, data: 数组, className: 'box', style: {fontSize: '20px'}}, //如果有子节点「双闭合调用」,则也包含children!!
        ref: null,
        type: DemoOne
    }

基于`root.render``virtualDOM`变为`真实的DOM`
`type`值不再是一个字符串,而是一个函数了,此时:
    把函数执行 -> DemoOne()
    把virtualDOM中的props,作为实参传递给函数 -> DemoOne(props)
    接收函数执行的返回结果「也就是当前组件的virtualDOM对象」
    最后基于render把组件返回的虚拟DOM变为真实DOM,插入到#root容器中!!

命名

组建的命名,一般都采用PascalCase【大驼峰命名法】 调用组件的时候,我们可以给调用的组件设置(传递)各种各样的属性

props 属性

调用

  • 调用组件,传递进来的属性是 只读的【原理:props对象被冻结了】
    • 获取:props.xxx
    • 修改:props.xxx = xxx(错误)
      • 如果就想把传递的属性值进行修改,我们可以:
        • 把props中的某个属性赋值给其他内容【例如:变量、状态...】
        • 我们不能直接操作props.xxx = xxx,但是我们可以修改变量/状态值

作用

父组件(index.jsx)调用子组件(DemoOne.jsx)的时候,可以基于属性,把不同的信息传递给子组件,子组件接收响应的属性值,呈现出不同的效果,让组件的复用性更强

规则校验

通过把函数当作对象,设置静态的私有属性方法,来给其设置属性的校验规则

  • 设置默认值
函数组件.defaultProps = {
    x: 0,
    ...
};
  • 设置其他规则,例如:数据值格式,是否必传...【依赖于官方的一个插件:prop-types】
import PropTypes from 'prop-types';

函数组件.propTypes = {
    // 类型是字符串、必传
    title: PropTypes.string.isRequired,
    // 类型是数字
    x: PropTypes.number,
};

  • 传递进来的属性,首先会经历规则的校验,不管校验成功还是失败,最后都会把属性给形参props,只不过如果不符合设定的规则,控制台会抛出警告错误(不影响属性值的获取)

关于对象的规则设置

  • 冻结
    • 冻结对象: Object.freeze(obj)
    • 检测对象是否被冻结:Object.isFrozen(obj)【值为 true/false】
      • 被冻结的对象:不能修改成员值,不能新增成员,不能删除现有成员,不能给成员做劫持【Object.defineProperty】
  • 密封
    • 密封对象: Object.seal(obj)
    • 检测是否被密封: Object.isSealed(obj)
      • 被密封的对象:可以修改成员的值,但不能删除,不能新增,不能劫持
  • 扩展
    • 把对象设置为不可扩展:Object.preventExtensions(obj)
    • 检测是否可扩展:Object.isExtensible(obj)
      • 被设置不可扩展对象:除了不能新增成员,其余的操作都可以处理
  • 被冻结的对象,既是不可扩展的,也是密封的;同理,被密封的对象,也是不可扩展的

插槽

原理

注意:react中其实是没有插槽这个概念的

  1. 默认插槽:通过组件标签体传入结构,固定写法props.children
  2. 具名插槽:通过标签属性props传入结构

reactprops可以是任何需要的东西,比如:某个值、react dom

默认插槽

//子组件
function Son(props) {
    return <div>
        <span>React</span>
        {props.children}
    </div>
}
//父组件
function App(props) {
    return (
        <div>
            <Son>
                <span>Hello World</span>
            </Son>
        </div>
    )
}
//结果
<div>
    <div>
        <span>React</span>
        <span>Hello World</span>
    </div>
</div>

具名插槽

// 父组件
import React from 'react'; // React语法核心
import ReactDOM from 'react-dom/client'; // 构建HTML(WebApp)的核心
import DemoOne from '@/views/DemoOne';

// 获取页面中 #root 的容器,作为“根”容器
const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
    <>
        <DemoOne>
            <span slot='footer'>页脚</span>
            <span>Hello World</span>
            <span slot='header'>页眉</span>
        </DemoOne>
    </>
);

// 子组件
import React from 'react';

const DemoOne = function DemoOne(props) {
    let { children } = props;

    let headerSlot = [],
        footerSlot = [],
        defaultSlot = [];
        
    children = React.Children.toArray(children);
    children.forEach((child) => {
        let { slot } = child.props;
        if (slot == 'header') {
            headerSlot.push(child);
        } else if (slot == 'footer') {
            footerSlot.push(child);
        } else {
            defaultSlot.push(child);
        }
    });
    return (
        <div className="demo-box">
            {headerSlot}
            <p>React</p>
            {footerSlot}
        </div>
    );
};
export default DemoOne;

React.Children.map

this.props.children 的值有三种可能:

  • 如果当前组件没有子节点,它就是 undefined
  • 如果有一个子节点,数据类型是 Object
  • 如果有多个子节点,数据类型就是 Array

所以,处理 this.props.children 的时候要小心 React 提供一个工具方法 React.Children 来处理 this.props.children 。我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object。

import React, { Component } from 'react';
export default class App extends Component {
    render() {
        return <>
            {/* 没有子节点 */}
            <Son></Son>
            {/* 字符串 */}
            <Son>Hello World</Son>
            {/* 一个子节点 */}
            <Son>
                <div>Hello React</div>
            </Son>
            {/* 多个子节点 */}
            <Son>
                <div>apple</div>
                <div>orange</div>
                <div>banana</div>
            </Son>
        </>
    }
}

class Son extends Component {
    state = { name: 'fang' }
    render() {
        return <>
            {console.log(this.props.children)}
            {/* {this.props.children} */}
            {React.Children.map(this.props.children, (el) => {
                return el
            })}
        </>
    }
}
//console.log打印结果分别是
// undefined
// Hello World
// Object
// Array

上面代码我们可以看出this.props.children既可以是对象,数组,undefined,那么按照平时处理数据的方式进行处理他的话,会显得很麻烦,那么我们就可以通过react官方提供的React.Children去进行处理,并且如果你想遍历它的话,可以通过React.Children.map去遍历它。

组件封装

// index.jsx
import React from 'react'; // React语法核心
import ReactDOM from 'react-dom/client'; // 构建HTML(WebApp)的核心
import Dialog from '@/components/Dialog';

// 获取页面中 #root 的容器,作为“根”容器
const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
    <>
        <Dialog content = 'hello,react' />
        <hr />
        <Dialog title='标题' content='你好,react'>
            <button>确定</button>
            <button>取消</button>
        </Dialog>
    </>
);

// Dialog.jsx
import React from 'react';
import PropsTypes from 'prop-types';

const Dialog = function Dialog(props) {
    // 获取传递的属性和插槽信息
    let { title, content, children } = props;
    children = React.Children.toArray(children);

    return (
        <div>
            <div className="header">
                <h2>{title}</h2>
                <span>X</span>
            </div>
            <div className="main">{content}</div>
            {children.length > 0 ? <div className="footer">{children}</div> : null}
        </div>
    );
};
Dialog.defaultProps = {
    title: '温馨提示',
};
Dialog.prototypes = {
    title: PropsTypes.string,
    content: PropsTypes.string.isRequired,
};
export default Dialog;

静态组件

函数组件静态组件

  • 不具备状态、生命周期函数、ref等内容

  • 第一次渲染组件,把函数执行

    • 产生一个私有的上下文:EC(V)
    • 把解析出来的props【含children】传递进来【但是被冻结了】
    • 对函数返回的JSX元素【virtualDOM】进行渲染
  • 当点击按钮的时候,会把绑定的小函数执行:

    • 修改上级上下文EC(V)中的变量
    • 私有变量值发生了改变
    • 但是“视图不会更新”
  • 也就是,函数组件第一次渲染完毕后,组件中的内容,不会根据组件内的某些操作,再进行更新,所以称之为静态组件

  • 除非在父组件中,重新调用这个函数组件【可以传递不同的属性信息】

const Vote = function Vote(props) {
    let { title } = props;
    let surNum = 10,
        oppNum = 5;

    return (
        <div>
            <div className="header">
                <h2>{title}</h2>
                <span>总人数: {surNum + oppNum}</span>
            </div>
            <div className="main">
                <p>支持人数: {surNum}人</p>
                <p>反对人数: {oppNum}人</p>
            </div>
            <div className="footer">
                <button
                    onClick={() => {
                        surNum++;
                        console.log(surNum);
                    }}
                >
                    支持
                </button>
                <button
                    onClick={() => {
                        oppNum--;
                        console.log(oppNum);
                    }}
                >
                    反对
                </button>
            </div>
        </div>
    );
};

export default Vote;

动态组件

真实项目中,若有 渲染完成后就不会再变化的 需求,则可以使用函数组件

但是大部分需求在第一次渲染完毕后,会基于组件内部的某些操作,让组件可以更新,以此呈现出不同的效果,此时需要使用动态组件

方法:类组件、Hooks组件(在函数组件中,使用Hooks函数)

  • 要求必须继承React.ComponentReact.PureComponent这个类
  • 我们习惯于使用ES6中的class创建类
  • 必须给当前类设置一个render的方法【放在其原型上】:在render方法中,返回需要渲染的视图
import React from 'react';

class Vote extends React.Component {
    render() {
        return (
            <div>
                <div className="header">
                    <h2>动态组件</h2>
                    <span>总人数: 10</span>
                </div>
                <div className="main">
                    <p>支持人数: 7人</p>
                    <p>反对人数: 3人</p>
                </div>
                <div className="footer">
                    <button>支持</button>
                    <button>反对</button>
                </div>
            </div>
        );
    }
}

export default Vote;

ES6中的class语法和继承的原理

class语法

class Parent{
    // new的时候,执行的构造函数【可写可不写;需要接受传递进来的实参信息,才需要甚至constructor】
    constructor(x,y){
        // this ->创建的实例
        this.total = x + y;
    }
    num = 200; //等价于 this.num = 200 给实例在设置私有属性
    getNum = ()=>{
        // 箭头函数没有自己的this,所用到的this是宿主环境中的
        console.log(this); //this->当前创建的实例
    };
    //================
    sum(){
        // 类似于 sum=function sum(){} 不是箭头函数
        // 它是给Parent.prototype上设置公共的方法【sum函数是不可枚举的】
    }
    //================
    // 把构造函数当做一个普通对象,为其设置静态的私有属性方法 使用时:Parent.xxx
    static avg = 1000;
    static average(){

    }
}

Parent.prototype.y = 2000; //在外部手动给构造函数原生上设置公共的属性

let p = new Parent(10,20);
console.log(p);

继承原理

/*基于extends 实现继承
1.首先基于call继承 React.Component.call(this) //this->Parent类的实例p
    function Component(props,context,updater){
        this.props = props;
        this.context = context;
        this.refs = emptyObject;
        this.updater = updater || ReactNoopUpdateQueue;
    }
    给创建的实例p设置四个私有属性:props/context/emptyObject/updater

2.在基于原型继承 Parent.prototype._proto_ === React.Component.prototype
实例 -> Parent.prototype ->  React.Component.prototype -> object.prototype
实例除了具备Parent.prototype提供的方法之外,还具备了React.Component.prototype原型上提供的方法:isReactComponent、setState、forceUpdate

3.只要自己设置了constructor,则内部第一句话一定要执行super()
*/

class Parent 基于extends React.Component{
    constructor(props){
        // this -> p props->获取的属性
        // super(); 等价于React.Component.call(this)
        // this.props=undefined thhis.context=undefined this.refs={} ...
        super(props)
        // this.props=props this.context=undefined ...
    }
    x=100;
    getX(){

    }
}
let p = new Parent();
console.log(p);

类组件

渲染的底层逻辑

  • render 函数在渲染的时候,如果type是:
    • 字符串:创建一个标签
    • 普通函数:把函数执行,并且把props传递给函数
    • 构造函数:把构造函数基于new执行「也就是创建类的一一个实例」,也会把解析出来的props传递过去
    • 每调用-次类组件都会创建一个单独的实例
    • 把在类组件中编写的render函数执行,把返回的jsx「virtualDOM」 当做组件视图进行渲染! !
    • 例如:
      new Vote({
          title; 'React其实还是很好学的! '
      })
      

从调用类组件「new Vote({...})」开始,类组件内部发生的事情

初始化属性&规则校验

  • 方案一:
constructor(props) {
    super(props); //会把传递进来的属性挂载到this实例上
    console. log(this. props); //获取到传递的属性
}
  • 方案二:
    • 即便我们自己不在constructor中处理「或者constructor都没写」,在constructor处理完毕后,React内部也会把传递的props挂载到实例上;所以在其他的函数中,只要保证this是实例,就可以基于this.props获取传递的属性!
    • 同样this.props获取的属性对象也是被冻结的{只读的} object. isFrozen(this . props) -> true

初始化状态

  • 状态:后期修改状态,可以触发视图的更新

    • 需要手动初始化,如果我们没有去做相关的处理,则默认会往实例上挂载一个state, 初始值是null => this.state=null
    • 手动处理:
      state = {
          ...
      };
      
  • 修改状态,控制视图更新

    • this.state.xxx=xxx :这种操作仅仅是修改了状态值,但是无法让视图更新
    • 想让视图更新,我们需要基于React.Component.prototype提供的方法操作:
      • this.setState(partialState) 既可以修改状态,也可以让视图更新「推荐」
        // partialState:部分状态
        this. setState({
            xxx:Xxx
        });
        
      • this. forceUpdate()强制更新

触发 componentWillMount 周期函数(钩子函数)

  • 组件第一次渲染之前触发
  • 钩子函数:在程序运行到某个阶段,我们可以基于提供一个处理函数,让开发者在这个阶段做一些自定义的事情
    • 此周期函数,目前是不安全的【虽然可用,但是未来可能要被移除,所以不建议使用】;若使用的话,则控制台抛出黄色警告,为了不抛出警告,可以使用 UNSAFE_componentWillMount
    • 如果开启了 React.StrictMode【React的严格模式】,则使用UNSAFE_componentWillMount这样的周期函数,控制台会直接抛出红色警告错误

触发 render 周期函数

此阶段渲染页面

触发 componentDidMount 周期函数

  • 组件渲染完毕触发
  • 已经把 virtualDOM变为真实DOM,所以可以获取真实DOM

更新的底层逻辑

逻辑一

组件内部的状态被修改,组件会更新

触发 shouldComponentUpdate 周期函数

是否允许更新;此周期函数需要返回true/falsetrue:允许更新,会继续执行下一个操作,false:不允许更新,接下来什么都不处理

shouldComponentUpdate(nextProps, nextState) {
    // nextState: 存储要修改的最新状态
    // this.state: 存储的还是修改前的状态【此时状态没有改变】
    console.log(this.state, nextState);

    return true
}

触发componentWillUpdate周期函数

更新之前

  • 此周期函数也是不安全的
  • 在这个阶段,状态/属性还没有被改变

修改状态值/属性值

让 this.state.xxx 改为最新的值

触发render周期函数

组件更新

  • 按照最新的状态/属性,把返回的JSX编译为 virtualDOM
  • 和上一次渲染出来的 virtualDOM 进行对比 「DOM-DIFF」
  • 把差异的部分进行渲染「渲染为真实的DOM」

触发 componentDidUpdate周期函数

组件更新完毕

特殊说明,如果是基于 this.foreUpdate() 强制更新视图,会跳过 shouldComponentUpdate 周期函数的教研,直接从 willUpdate 开始进行更新「也就是视图一定会更新」

逻辑二

父组件更新,触发的子组件更新

触发 componentWillReceiveProps 周期函数

接收到最新属性之前

  • 此周期函数是不安全的
componentWillReceiveProps(nextProps) {
    // this.props: 存储之前的属性
    // nextProps: 传递进来的最新属性值
    console.log('componentWillReceiveProps', this.props, nextProps);
}

相较于逻辑一,逻辑二只是在 shouldComponentUpdate 周期函数前增加了一个上述周期函数

父子组件嵌套,处理机制上遵循深度优先原则:父组件在操作中,遇到子组件,一定是把子组件处理完,父组件才能继续处理

  • 父组件第一次渲染
    • 父 willMount -> 父 render子willMount -> 子 render -> 子 didMount】 -> 父 didmount
  • 父组件更新
    • 父 shouldUpdate -> 父 willUpdate -> 父 render子 willReceiveProps -> 子 shouldUpdate -> 子 willUpdate -> 子 render -> 子 didUpdate】 -> 父 didUpdate

卸载的逻辑

组件卸载的逻辑

  • 触发 componentWillUnmount 周期函数:组件销毁之前
  • 销毁

父子组件嵌套销毁逻辑

  • 父 willUnmount -> 处理中 【子 willUnmount -> 子销毁 -> 父销毁

总结

函数组件是“静态组件”

  • 组件第一次渲染完毕后,无法基于“内部的某些操作”让组件更新「无法实现“自更新”」;但是,如果调用它的父组件更新了,那么相关的子组件也一定会更新「可能传递最新的属性值进来」;

  • 函数组件具备:属性...「其他状态等内容几乎没有」

  • 优势:比类组件处理的机制简单,这样导致函数组件渲染速度更快! ! 类组件是“动态组件”

  • 组件在第一-渲染完毕后,除了父组件更新可以触发其更新外,我们还可以通过: this. setState修改状态或者this. forceUpdate等方式,让组件实现“自更新”! !

  • 类组件具备:属性、状态、周期函数、ref... 「几乎组件应该有的东西它都具备」

  • 优势:功能强大! !

Hooks组件「推荐」

  • 具备了函数组件和类组件的各自优势,在函数组件的基础上,基于hooks函数, 让函数组件也可以拥有状态、周期函数等,让函数组件也可以实现自更新「动态化」! !

PureComponent 和 Component 的区别

  • PureComponent 会给类组件默认加一个shouldComponentUpdate周期函数
    • 在此周期函数中,它对新老的属性/状态,会做一个浅比较
    • 如果经过浅比较,发现属性和状态并没有改变,则返回false【也就是不继续更新组件】,有变化才会去更新

浅比较 源码


// 用原型链的方法
const hasOwn = Object.prototype.hasOwnProperty

// 这个函数实际上是Object.is()的polyfill
function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {

  // 首先对基本数据类型的比较
  // 若是同引用便会返回 true
  if (is(objA, objB)) return true

  // 由于Obejct.is()可以对基本数据类型做一个精确的比较, 所以如果不等
  // 只有一种情况是误判的,那就是object,所以在判断两个对象都不是object
  // 之后,就可以返回false了
  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false
  }

  // 过滤掉基本数据类型之后,就是对对象的比较了
  // 首先拿出key值,对key的长度进行对比
  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  // 长度不等直接返回false
  if (keysA.length !== keysB.length) return false
  
  // key相等的情况下,在去循环比较
  for (let i = 0; i < keysA.length; i++) {

  // key值相等的时候
  // 借用原型链上真正的 hasOwnProperty 方法,判断ObjB里面是否有A的key的key值
  // 属性的顺序不影响结果也就是{name:'daisy', age:'24'} 跟{age:'24',name:'daisy' }是一样的
  // 最后,对对象的value进行一个基本数据类型的比较,返回结果
    if (!hasOwn.call(objB, keysA[i]) ||
        !is(objA[keysA[i]], objB[keysA[i]])) {
      return false
    }
  }
  return true
}

扩充知识点: Object.is()  方法判断两个值是否为同一个值

ref详细操作

方式

  • 受控组件:基于修改数据/状态,让视图更新,达到需要的效果【推荐】
  • 非受控组件:基于ref获取DOM元素,通过操作DOM元素,来实现需求和效果【偶尔】

方法一

  • 给需要获取的元素设置 ref='xxx',后期基于 this.refs.xxx去获取相应的元素
<h2 className="title" ref="titleBox">
    温馨提示
</h2>
  • 获取:this.refs.titleBox
import React from 'react';

class Vote extends React.Component {
    render() {
        return (
            <div>
                <h2 className="title" ref="titleBox">
                    温馨提示
                </h2>
            </div>
        );
    }

    componentDidMount() {
        console.log(this.refs.titleBox);
    }
}
export default Vote;

方法二

  • 把ref属性值设置为一个函数ref={(currentNode) => (this.xxx = currentNode)}
    • currentNode是函数的形参:存储的就是当前的DOM元素
    • 然后将获取的DOM元素currentNode直接挂载到实例的某个属性上(例如:currentNode)
import React from 'react';

class Vote extends React.Component {
    render() {
        return (
            <div>
                <h2 className="title" ref={(currentNode) => (this.currentNode = currentNode)}>
                    友情提示
                </h2>
            </div>
        );
    }

    componentDidMount() {
        console.log(this.currentNode);
    }
}
export default Vote;

方法三

  • 基于React.createRef()方法创建一个REF对象
  • this.xxx = React.createRef();等价于this.xxx = {current: null}
  • ref = {REF对象(this.xxx)}
  • 获取: this.xxx.current
import React from 'react';

class DemoTwo extends React.Component {
    titleBox = React.createRef()
    render() {
        return (
            <div>
                <h2 className='title' ref={this.titleBox}></h2>
            </div>
        );
    }

    componentDidMount() {
        console.log(this.titleBox.current);
    }
}
export default DemoTwo;

原理:在render渲染的时候,会获取virtualDOM属性;

如果属性值是一个字符串,则会给this.refs增加这样一个成员,成员值就是当前的DOM元素;

如果属性值是一个函数,则会执行函数,把当前DOM元素传递给这个函数,而在函数执行的内部,一般都会把DOM元素直接挂载到实例的某个属性上

如果属性值是一个REF对象,则会把DOM元素赋值给对象的current属性

组件设置ref

  • 给元素标签设置ref,目的是获取对应的DOM元素
  • 给类组件设置ref,目的是获取当前调用组件1创建的实例【后续可以根据实例获取子组件中的相关信息】
  • 给函数组件设置ref,会直接报错
    • 可以让其配合 React.forwardRef 实现 ref 的转发
    • 目的是获取函数子组件内部的某个元素
import React, { Component } from 'react';

class ComponentChild1 extends Component {
    render() {
        return <div>类组件</div>;
    }
}

const ComponentChild2 = React.forwardRef(function ComponentREF(props, ref) {
    return <div>
        <span>函数组件</span>
        <button ref={ref}></button>
    </div>;
});

export default class ComponentREF extends Component {
    render() {
        return (
            <div>
                <ComponentChild1
                    ref={(currentNode) => {
                        this.child1 = currentNode;
                    }}
                ></ComponentChild1>
                <ComponentChild2
                    ref={(currentNode) => {
                        this.child2 = currentNode;
                    }}
                ></ComponentChild2>
            </div>
        );
    }
    componentDidMount() {
        console.log(this.child1);  // 存储的是子组件的实例对象
        console.log(this.child2);  // 存储的是子组件内部的button按钮
    }
}

setState进阶

基础示例

import React, { Component } from 'react';

export default class Demo_setState extends Component {
    state = {
        num1: 10,
        num2: 20,
    };
    handleClick = () => {
        let { num1, num2 } = this.state;
        // 同时修改状态值,只会触发一次更新
        this.setState(
            {
                num1: num1 + 10,
                num2: num2 + 10,
            },
            () => {
                console.log('num1被修改了');
            }
        );
    };

    componentDidUpdate() {
        console.log('组件内容更新');
    }

    render() {
        let { num1, num2 } = this.state;
        return (
            <div>
                num1: {num1}
                <br />
                num2: {num2}
                <br />
                <button onClick={this.handleClick}>按钮</button>
            </div>
        );
    }
}

this.setState([partialState],[callback])

  • [partialState]:支持部分状态更改 +this.setState( { num1: 30 });不论总共有多少状态,只修改了num1,其余的状态不懂
  • [callback]:在状态更改/视图更新完毕后触发执行,也可以说只要执行了setState,callback一定会执行
    • 发生在componentDidUpdate周期函数之后,DidUpdate会在任何状态更改后都触发执行;而回调函数方式,可以在指定状态更新后处理一些事情
    • 特殊:即便我们基于shouldComponentUpdate阻止了状态/视图的更新,DidUpdate周期函数肯定不会执行了,但是设置的这个callback回调函数依然会被触发执行
    • 类似于Vue框架中的$nextTick

目的

  • 在React18中,setState在任何地方执行,都是"异步操作",React18中有一套更新队列的机制,基于异步操作,实现状态的"批处理"
  • 带来的好处则是减少了视图更新的次数,降低渲染消耗的性能,让更新的逻辑和流程更清晰和稳健

更新队列机制

  • 在当前相同的时间段内【浏览器此时可以处理的事情中】,遇到setState会立即放入到更新队列中
  • 此时状态/视图还未更新
  • 当所有的操作代码结束,会"刷新队列"【通知更新队列中的任务执行】:把所有放入的setState合并在一起执行,只触发一次视图更新【批处理操作】

在 React18 和 React16 中,关于 setState 是同步还是异步,是有一些区别的

React18中:不论在什么地方执行setState,它都是异步的【都是基于update更新队列机制,实现的批处理】

React16中:如果在合成事件【jsx元素中基于onXxx绑定的事件】、周期函数中,setState的操作是异步的;但是如果setState出现在其他异步操作中【例如:定时器、手动获取DOM元素做的事件绑定等】,它将变为同步操作【立即更新状态和视图渲染】

flushSync

如果想在 React 18 退出批处理该怎么做呢?官方提供了一个 API flushSync

flushSync<R>(fn: () => R): R 它接收一个函数作为参数,并且允许有返回值。

function handleClick() {
  flushSync(() => {
    setCount(3);
  });
  // 会在 setCount 并 render 之后再执行 setFlag
  setFlag(true);
}

注意:flushSync 会以函数为作用域,函数内部的多个 setState 仍然为批量更新,这样可以精准控制哪些不需要的批量更新:

function handleClick() {
  flushSync(() => {
    setCount(3);
    setFlag(true);
  });
  // setCount 和 setFlag 为批量更新,结束后
  setLoading(false);
  // 此方法会触发两次 render
}

setState进阶

setState 不仅能够传入新的 state 对象;还能传入回调函数,并在回调函数里面返回新的 state 对象

this.setState((prevState) => {return {xxx:prevState.xxx}})

  • prevState:存储之前的状态值
  • return的对象:就是想要修改的新状态值【支持修改部分状态】
import React, { Component } from 'react';

export default class Demo_setState2 extends Component {
    state = {
        num: 0,
    };
    handleClick = () => {
        for (let i = 0; i < 20; i++) {
            this.setState((prevState) => {
                return {
                    num: prevState.num + 1,
                };
            });
        }
    };

    componentDidUpdate() {
        console.log('组件内容更新');
    }

    render() {
        return (
            <div>
                num: {this.state.num}
                <button onClick={this.handleClick}>按钮</button>
            </div>
        );
    }
}