跳到主要内容

数据管理

数据管理

1.React setState 调用的原理

image-20220701204358623

具体的执行过程如下(源码级解析):

  • 首先调用了setState 入口函数,入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去;
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, "setState");
}
};
  • enqueueSetState 方法将新的 state 放进组件的状态队列里,并调用 enqueueUpdate 来处理将要更新的实例对象;
enqueueSetState: function (publicInstance, partialState) {
// 根据 this 拿到对应的组件实例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 这个 queue 对应的就是一个组件实例的 state 数组
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate 用来处理当前的组件实例
enqueueUpdate(internalInstance);
}
  • enqueueUpdate 方法中引出了一个关键的对象——batchingStrategy,该对象所具备的isBatchingUpdates 属性直接决定了当下是要走更新流程,还是应该排队等待;如果轮到执行,就调用 batchedUpdates 方法来直接发起更新流程。由此可以推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象。
function enqueueUpdate(component) {
ensureInjected();
// 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
if (!batchingStrategy.isBatchingUpdates) {
// 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}

注意:batchingStrategy 对象可以理解为“锁管理器”。这里的“锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。

2.React setState 是同步还是异步? 调用之后发生了什么?

(1)React 中 setState 后发生了什么

在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。

在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染

如果在短时间内频繁 setState。React 会将 state 的改变压入栈中,在合适的时机,批量更新 state 和视图,达到提高性能的效果。

(2)setState 是同步还是异步的

假如所有 setState 是同步的,意味着每执行一次 setState 时(有可能一个同步代码中,多次 setState),都重新 vnode diff + dom 修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个 setState 合并成一次组件更新。所以默认是异步的,但是在一些情况下是同步的

setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。

  • 异步: 在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略
  • 同步:React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新

一般认为,做异步设计是为了性能优化、减少渲染次数:

  • setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了state,但是还没有执行render函数,那么stateprops不能保持同步。stateprops不能保持一致性,会在开发中产生很多的问题;

3.React 组件的 state 和 props 有什么区别?

(1)props

props 是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的 props 来重新渲染子组件,否则子组件的 props 以及展现形式不会改变。

(2)state

state 的主要作用是用于组件保存、控制以及修改自己的状态,它只能在 constructor 中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的 this.setState 来修改,修改 state 属性会导致组件的重新渲染。

(3)区别

  • props 是传递给组件的(类似于函数的形参),而 state 是在组件内被组件自己管理的(类似于在一个函数内声明的变量)。
  • props 是不可修改的,所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
  • state 是在组件中创建的,一般在 constructor 中初始化 state。state 是多变的、可以修改,每次 setState 都异步更新的。

4.React 中的 setState 批量更新的过程是什么?

调用 setState 时,组件的 state 并不会立即改变, setState 只是把要修改的 state 放入一个队列, React 会优化真正的执行时机,并出于性能原因,会将 React 事件处理程序中的多次React 事件处理程序中的多次 setState 的状态修改合并成一次状态修改。 最终更新只产生一次组件及其子组件的重新渲染,这对于大型应用程序中的性能提升至关重要。

this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务]
});
this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务,count+1的任务]
});

合并 state,[count+1的任务]

执行 count+1的任务

需要注意的是,只要同步代码还在执行,“攒起来”这个动作就不会停止。(注:这里之所以多次 +1 最终只有一次生效,是因为在同一个方法中多次 setState 的合并动作不是单纯地将更新累加。比如这里对于相同属性的设置,React 只会为其保留最后一次的更新)。

5.React 中组件的 props 改变时更新组件的有哪些方法?

在一个组件传入的 props 更新时重新渲染该组件常用的方法是在componentWillReceiveProps中将新的 props 更新到组件的 state 中(这种 state 被成为派生状态(Derived State)),从而实现重新渲染。React 16.3 中还引入了一个新的钩子函数getDerivedStateFromProps来专门实现这一需求。

(1)componentWillReceiveProps(17 已废弃)

在 react 的 componentWillReceiveProps(nextProps)生命周期中,可以在子组件的 render 函数执行前,通过 this.props 获取旧的属性,通过 nextProps 获取新的 props,对比两次 props 是否相同,从而更新子组件自己的 state。

这样的好处是,可以将数据请求放在这里进行执行,需要传的参数则从 componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。

(2)getDerivedStateFromProps(16.3 引入)

这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。

两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。

需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:

static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// 当传入的type发生变化的时候,更新state
if (type !== prevState.type) {
return {
type,
};
}
// 否则,对于state不进行任何操作
return null;
}

6.React 中的 props 为什么是只读的?

this.props是组件之间沟通的一个接口,原则上来讲,它只能从父组件流向子组件。React 具有浓重的函数式编程的思想。

提到函数式编程就要提一个概念:纯函数。它有几个特点:

  • 给定相同的输入,总是返回相同的输出。
  • 过程没有副作用。
  • 不依赖外部状态。

this.props就是汲取了纯函数的思想。props 的不可以变性就保证的相同的输入,页面显示的内容是一样的,并且不会产生副作用

7.React 中 setState 的第二个参数作用是什么?

setState 的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在 componentDidUpdate 生命周期内执行。通常建议使用 componentDidUpdate 来代替此方式。在这个回调函数中你可以拿到更新后 state 的值:

this.setState({
key1: newState1,
key2: newState2,
...
}, callback) // 第二个参数是 state 更新完成后的回调函数

8.React 中的 setState 和 replaceState 的区别是什么?

(1)setState() setState()用于设置状态对象,其语法如下:

setState(object nextState[, function callback])
  • nextState,将要设置的新状态,该状态会和当前的 state 合并
  • callback,可选参数,回调函数。该函数会在 setState 设置成功,且组件重新渲染后调用。

合并 nextState 和当前 state,并重新渲染组件。setState 是 React 事件处理函数中和请求回调函数中触发 UI 更新的主要方法。

(2)replaceState() replaceState()方法与 setState()类似,但是方法只会保留 nextState 中状态,原 state 不在 nextState 中的状态都会被删除。其语法如下:

replaceState(object nextState[, function callback])
  • nextState,将要设置的新状态,该状态会替换当前的 state。
  • callback,可选参数,回调函数。该函数会在 replaceState 设置成功,且组件重新渲染后调用。

总结: setState  是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而 replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。

9.React 中怎么检验 props?验证 props 的目的是什么?

React为我们提供了 PropTypes 以供验证使用。当我们向 Props 传入的数据无效(向 Props 传入的数据类型和验证的数据类型不符)就会在控制台发出警告信息。它可以避免随着应用越来越复杂从而出现的问题。并且,它还可以让程序变得更易读。

import PropTypes from "prop-types";

class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

Greeting.propTypes = {
name: PropTypes.string,
};

当然,如果项目中使用了 TypeScript,那么就可以不用 PropTypes 来校验,而使用 TypeScript 定义接口来校验 props.

10.React 如何获取上一轮的 props 和 state?

react 获取上一轮的 props 和 state ,有的时候 需要 获取 改变前的 state,和 props 做个对比处理,或者其它处理。

效果图:

如果只是 想实现 这个效果 下面的代码 也行 。就不用借助其它的了。 这个思路就是,在 改变 state 之前 就 备份一下 值 。

 hook

import React, { useState, useEffect } from "react";
import { Select } from "antd";
const { Option } = Select;
function Index() {
const [val, setVal] = useState("--"); //name
const [val2, setVal2] = useState("--"); //name

const handleChange = (value) => {
console.log(val, value);
setVal2(val);
setVal(value);
};

return (
<div>
<Select
defaultValue="lucy"
style={{
width: 120,
}}
onChange={handleChange}
>
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</Option>
<Option value="disabled" disabled>
Disabled
</Option>
<Option value="Yiminghe">yiminghe</Option>
</Select>
<h1>
现在的值: {val}, 之前的值: {val2}
</h1>
</div>
);
}

export default Index;

class 

不借助生命周期

import React, { Component } from "react";
import { Select } from "antd";
const { Option } = Select;
class Index extends Component {
constructor(props) {
super(props);
this.state = {
val: "--",
val2: "--",
};
}
componentDidMount() {}

handleChange = (value) => {
this.setState({
val: value,
val2: this.state.val,
});
};

render() {
let { val, val2 } = this.state;
return (
<div>
<Select
defaultValue="lucy"
style={{
width: 120,
}}
onChange={this.handleChange}
>
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</Option>
<Option value="disabled" disabled>
Disabled
</Option>
<Option value="Yiminghe">yiminghe</Option>
</Select>
<h1>
现在的值: {val}, 之前的值: {val2}
</h1>
</div>
);
}
}

export default Index;

借助生命周期  componentDidUpdate

import React, { Component } from "react";
import { Select } from "antd";
const { Option } = Select;
class Index extends Component {
constructor(props) {
super(props);
this.state = {
val: "--",
val2: "--",
};
}
componentDidMount() {}

handleChange = (value) => {
this.setState({
val: value,
});
};
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevProps, prevState, snapshot, "10");
if (prevState.val != this.state.val) {
this.setState({
val2: prevState.val,
});
}
}
// getSnapshotBeforeUpdate(prevProps, prevState) {
// console.log(prevProps, prevState,"09")
// return null;
// }
render() {
let { val, val2 } = this.state;
return (
<div>
<Select
defaultValue="lucy"
style={{
width: 120,
}}
onChange={this.handleChange}
>
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</Option>
<Option value="disabled" disabled>
Disabled
</Option>
<Option value="Yiminghe">yiminghe</Option>
</Select>
<h1>
现在的值: {val}, 之前的值: {val2}
</h1>
</div>
);
}
}

export default Index;

下面的方法是 获取到整个 state 和 props

react 类组(class)件实现  

react Class 组件 很好实现因为有生命周期,可以用  componentDidUpdate(prevProps, prevState, snapshot) 和  getSnapshotBeforeUpdate(prevProps, prevState) 获取到

componentDidUpdate()介绍

componentDidUpdate(prevProps, prevState, snapshot);

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

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

componentDidUpdate(prevProps) {if (this.props.userID !== prevProps.userID) {this.fetchData(this.props.userID);  }}

你也可以在  componentDidUpdate()  中直接调用  setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props “镜像”给 state,请考虑直接使用 props。 欲了解更多有关内容,请参阅为什么 props 复制给 state 会产生 bug

如果组件实现了  getSnapshotBeforeUpdate()  生命周期(不常用),则它的返回值将作为  componentDidUpdate()  的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined。

注意

如果  shouldComponentUpdate()  返回值为 false,则不会调用  componentDidUpdate()


getSnapshotBeforeUpdate 介绍

getSnapshotBeforeUpdate(prevProps, prevState);

getSnapshotBeforeUpdate()  在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给  componentDidUpdate()

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

应返回 snapshot 的值(或  null)。

例如:

class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动​​位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}

render() {
return <div ref={this.listRef}>{/* ...contents... */}</div>;
}
}

在上述示例中,重点是从  getSnapshotBeforeUpdate  读取  scrollHeight  属性,因为 “render” 阶段生命周期(如  render)和 “commit” 阶段生命周期(如  getSnapshotBeforeUpdate  和  componentDidUpdate)之间可能存在延迟。

上面参考于:react 官网 -生命周期

实现代码如下:

import React, { Component } from "react";
import { Select } from "antd";
const { Option } = Select;
class Index extends Component {
constructor(props) {
super(props);
this.state = {
val: "--",
};
}
componentDidMount() {}

handleChange = (value) => {
this.setState({
val: value,
});
};
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevProps, prevState, snapshot, "10");
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log(prevProps, prevState, "09");
return null;
}
render() {
let { val } = this.state;
return (
<div>
<Select
defaultValue="lucy"
style={{
width: 120,
}}
onChange={this.handleChange}
>
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</Option>
<Option value="disabled" disabled>
Disabled
</Option>
<Option value="Yiminghe">yiminghe</Option>
</Select>
<h1>
现在的值: {val}, 之前的值: {}
</h1>
</div>
);
}
}

export default Index;

函数组件(react Hook 实现)

借用 useEffect, useRef 实现 。

useEffect

useEffect(didUpdate);

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

使用  useEffect  完成副作用操作。赋值给  useEffect  的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它  在只有某些值改变的时候  才执行。

清除 effect

通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect  函数需返回一个清除函数。以下就是一个创建订阅的例子:

useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
});

为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。在上述示例中,意味着组件的每一次更新都会创建新的订阅。若想避免每次更新都触发 effect 的执行,请参阅下一小节。

effect 的执行时机

与  componentDidMountcomponentDidUpdate  不同的是,传给  useEffect  的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。

然而,并非所有 effect 都可以被延迟执行。例如,一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的  useLayoutEffect Hook 来处理这类 effect。它和  useEffect  的结构相同,区别只是调用时机不同。

此外,从 React 18 开始,当它是离散的用户输入(如点击)的结果时,或者当它是由  flushSync  包装的更新结果时,传递给  useEffect  的函数将在屏幕布局和绘制之前同步执行。这种行为便于事件系统或  flushSync  的调用者观察该效果的结果。

注意

这只影响传递给  useEffect  的函数被调用时 — 在这些 effect 中执行的更新仍会被推迟。这与  useLayoutEffect  不同,后者会立即启动该函数并处理其中的更新。

即使在  useEffect  被推迟到浏览器绘制之后的情况下,它也能保证在任何新的渲染前启动。React 在开始新的更新前,总会先刷新之前的渲染的 effect。

effect 的条件执行

默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。

然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在  source prop 改变时重新创建。

要实现这一点,可以给  useEffect  传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:

useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
}, [props.source]);

此时,只有当  props.source  改变后才会重新创建订阅。

注意

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。请参阅文档,了解更多关于如何处理函数  以及数组频繁变化时的措施  的内容。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直持有其初始值。尽管传入  []  作为第二个参数有点类似于  componentDidMount  和  componentWillUnmount  的思维模式,但我们有  更好的 方式  来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用  useEffect,因此会使得处理额外操作很方便。

我们推荐启用  eslint-plugin-react-hooks  中的  exhaustive-deps  规则。此规则会在添加错误依赖时发出警告并给出修复建议。

依赖项数组不会作为参数传给 effect 函数。虽然从概念上来说它表现为:所有 effect 函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

useRef

const refContainer = useRef(initialValue);

useRef  返回一个可变的 ref 对象,其  .current  属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

一个常见的用例便是命令式地访问子组件:

function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

本质上,useRef  就像是可以在其  .current  属性中保存一个可变值的“盒子”。

你应该熟悉 ref 这一种访问 DOM  的主要方式。如果你将 ref 对象以  <div ref={myRef} />  形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的  .current  属性设置为相应的 DOM 节点。

然而,useRef()  比  ref  属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而  useRef()  和自建一个  {current: ...}  对象的唯一区别是,useRef  会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef  并不会通知你。变更  .current  属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref  来实现。

实现代码如下:

import React, { useState, useEffect, useRef } from "react";
import { Select } from "antd";
const { Option } = Select;
function Index() {
const prevCountRef = useRef();
const [val, setVal] = useState("--"); //name
useEffect(() => {
prevCountRef.current = val;
});

const handleChange = (value) => {
setVal(value);
};

return (
<div>
<Select
defaultValue="lucy"
style={{
width: 120,
}}
onChange={handleChange}
>
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</Option>
<Option value="disabled" disabled>
Disabled
</Option>
<Option value="Yiminghe">yiminghe</Option>
</Select>
<h1>
现在的值: {val}, 之前的值: {prevCountRef.current}
</h1>
</div>
);
}

export default Index;

如何获取上一轮的 props 或 state?官方的示例

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

const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;

return (
<h1>
Now: {count}, before: {prevCount}
</h1>
);
}

11.setSate 的缺点

调用时机不恰当的话可能引起循环调用的问题:比如在 componentWillUpdate render componentDidUpdate 调用都有可能引起这种问题 setState 可能会引用不必要的 re-render:setState 任何值都会引起组件的 render 函数执行,可能导致性能的浪费

setState 更新数组

你会发现,如果直接使用 push 等方法改变 state,按理来说,push 会改变原数组,数组应该更新,但渲染出来的 state 并不会更改

let newValue = 1;
const [array, setArray] = useState([]);
const handleChange = (newValue: number) => {
array.push(newValue);
setState(array); //array更新了,但无法触发渲染
console.log(array); //[1]
//array增加了newValue,但渲染并未发生改变
};

render: <p>This array is {JSON.stringify(array)}</p>; //[]

这是由于 js 中,数组的赋值是引用传递的,array.push 相当于直接更改了数组对应的内存块,但 react 内部用于对比的 array 的内存并没有更改,是指向同一个内存的,setState 只做 shallow compare,因此没有触发 re-render。 可以使用扩展运算符,创建一个新数组,更改内存引用

const handleChange = (newValue: number) => {
const newArray = [...array, newValue];
setState(newArray); //此处本质上是改变了引用
console.log(array); //[]
//array并未改变,但渲染改变了
};

render: <p>This array is {JSON.stringify(array)}</p>; //[1]

或者触发展示组件的 re-render,这样即使不改变数组的引用,依然可以正确显示变动。

const handleChange = (newValue: number) => {
setValue(newValue);
setState(array.push(newValue)); //其他更新触发了组件的re-render,此时可以正常显示变动
console.log(array); //[1]
//array改变,且渲染改变
};

render: <p>This array is {JSON.stringify(array)}</p>; //[1]

再给一个直观的例子(感谢我的同事@ling) 直接尝试:https://codepen.io/ling-cao/pen/NWrMRrq

const { useRef, useEffect, useState } = React;

const useMemoryState = (init) => {
const [arr, setArr] = useState(init);
const lastArrRef = useRef(null);
const updateArr = (next) => {
lastArrRef.current = [...arr];
console.log(next);
setArr(next);
};
return [arr, updateArr, lastArrRef.current];
};

let i = 0;
const App = () => {
const [arr, setArr, lastArr] = useMemoryState([0]);
const [updateSign, setUpdateSign] = useState(false);

return (
<>
<div className="text">
<label>Current array :</label> {JSON.stringify(arr)}
</div>
<div className="box-container">
<div className="box">
<h1>Push a number to array</h1>
<pre>setArr(arr.push(i) && arr)</pre>
<br />
<button
onClick={() => {
i++;
setArr(arr.push(i) && arr);
}}
className="btn btn-2 btn-2c"
>
Try it
</button>
</div>
<div className="box">
<h1>Push a number to array and renew array</h1>
<pre>setArr(arr.push(i) && [...arr])</pre>
<br />
<button
onClick={() => {
i++;
setArr(arr.push(i) && [...arr]);
}}
className="btn btn-2 btn-2c"
>
Try it
</button>
</div>
<div className="box">
<h1>Push a number to array and update another state</h1>
<pre>setArr(arr.push(i) && arr); setUpdateSign(x => !x)</pre>
<br />
<button
onClick={() => {
i++;
{
setArr(arr.push(i) && arr);
setUpdateSign((x) => !x);
}
}}
className="btn btn-2 btn-2c"
>
Try it
</button>
</div>
</div>
</>
);
};

逐次点击第二个按钮或第三个按钮都可以正常更新渲染。

点击第一个按钮,通过 console 可以看出来,array 数组值有更新,但没有渲染(Current array 没变);再点其他两个按钮时,会把第一个按钮点击更新的结果一起渲染出来。 img

侧面展示并不是没有更新数组,而是更新后未渲染。

setState 不会立即改变数据

setState 某种意义上是类似于异步函数的。

// name is ""
this.setState({
name: "name",
});
console.log(`name is ${this.state.name}`);

这样写,name 是不能正常显示。 最常用的办法就是使用回调函数

this.setState(
{
name: "name",
},
() => {
console.log(`name is ${this.state.name}`);
}
);

多个 setState 的更新

setState 的“异步”是本身执行的过程和代码是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没办法立马拿到更新后的值,形成了所谓的异步。批量更新优化也是建立在“异步”之上的,如果对同一个值进行多次 setState,setState 的批量更新策略会对其进行覆盖,取最后一次执行;如果是同时 setState 多个不同的值,在更新时会对其合并批量更新。

setState 异步回调获取不到最新值

  useEffect(() => {
const newModel = {
name: props.name,
datasetId: props.datasetId,
modelId: null,
trainingStatus: TrainingStatus.Init,
modelStatus: Status.NotStarted,
} as TrainingModel;
setModels([...models, newModel]);
startTraining(newModel);
}, [props.datasetId]);

const startTraining = async (newModel: TrainingModel) => {
const dataset = await getDataset(newModel.datasetId);
let newModels = [...models];
let currModel = newModels.find(x => x.datasetId == newModels.datasetId);
currModel.trainingStatus = TrainingStatus.CreateDataset;
//此时可通过页面的渲染效果知道models中已有值,但此处断点models为空
setModels(newModels);
};

类似的,老生常谈的,在 useEffect 里面设置一个 Interval,过了 Interval time,也同样是 useEffect 更新时的 state 值,而得不到最新的 state 值。 为解决异步导致的获取不到最新 state 的问题,使用 setState 的回调函数获取 state 的当前最新值

const startTraining = async (newModel: TrainingModel) => {
const dataset = await getDataset(newModel.datasetId);
setModels((lastModels) => {
//此时的lastModels是models的最新值
const nextModels = [...lastModels];
let currModel = nextModels.find((x) => x.datasetId == newModel.datasetId);
currModel.trainingStatus = TrainingStatus.CreateDataset;
return nextModels;
});
};

原因是,组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被[看到],也就是说,组件内部的函数拿到的总是定义它的那次渲染中的 props 和 state。想要解决,一般两种方法,一种是上述的使用 setState 回调函数获取 state 最新值,一种是使用 ref保存修改并读取 state。