函数组件
原理
创建
在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中其实是没有插槽这个概念的
- 默认插槽:通过组件标签体传入结构,固定写法
props.children
- 具名插槽:通过标签属性
props
传入结构
react
的props
可以是任何需要的东西,比如:某个值、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.Component
或React.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/false
;true:
允许更新,会继续执行下一个操作,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>
);
}
}