setState
setState是同步更新的,但是表现为异步,为什么设计成异步的呢?
- 如果每次调用
setState
都进行一次更新, 那么意味着render
函数会被频繁的调用界面重新渲染, 这样的效率是很低的
最好的方法是获取到多个更新, 之后进行批量更新
- 如果同步更新了
state
, 但还没有执行render
函数, 那么state
和props
不能保持同步
state
和props
不能保持一致性, 会在开发中产生很多的问题
总结:setState
设计为异步, 可以显著的提高性能
如何获取异步更新后的值?
-
方式一:
setState
的回调setState
接收两个参数: 第二个参数是回调函数(callback
), 这个回调函数会在state
更新后执行
import React from "react"; class App extends React.Component { constructor(props) { super(props); this.state = { name: "nihao", }; } shouldComponentUpdate(nextProps, nextState, nextContext) { console.log("hello", this.state.name);//方式二 return true; }
render() { return (
{this.state.name}
<button onClick={() => { this.setState({ name: "liweike" }, () => { console.log(this.state.name);//方式一 }); }} > 点我更新值 -
方式二:
componentDidUpdate
生命周期函数
setState表现出来一定是异步的吗?
1、在组件生命周期或者React合成事件中,setState是异步的
2、在setTimeout或者原生DOM事件中,setState是同步的
验证一:在setTimeout的更新——>同步更新
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "nihao",
};
}
render() {
return (
<div>
<h1>{this.state.name}</h1>
<button
onClick={() => {
setTimeout(() => {
this.setState({ name: "liweike" });
console.log(this.state.name);//打印liweike
});
}}
>
点我更新值
</button>
</div>
);
}
}
export default App;
验证二:在原生DOM事件——>同步更新
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "nihao",
};
}
componentDidMount() {
document.getElementById("btn").addEventListener("click", (e) => {
this.setState({ name: "liweile" });
console.log(this.state.name);//打印liweile
});
}
render() {
return (
<div>
<h1>{this.state.name}</h1>
<button id="btn">点我更新值</button>
</div>
);
}
}
export default App;
需要注意的是在hook写法中,这两种都没有用
import { useState } from "react";
const App = function () {
const [name, setName] = useState("nihao");
return (
<div>
<h1>{name}</h1>
<button
onClick={() => {
setTimeout(() => {
setName("liweile");
console.log(name);//打印nihao,没有获取到更新后的值
});
}}
>
点我更新
</button>
</div>
);
};
export default App;
import { useState, useEffect } from "react";
const App = function () {
const [name, setName] = useState("nihao");
useEffect(() => {
document.getElementById("btn").addEventListener("click", (e) => {
setName("liweile");
console.log(name);//打印nihao,没有获取到更新后的值
});
}, []);
return (
<div>
<h1>{name}</h1>
<button id="btn">点我更新</button>
</div>
);
};
export default App;
对于Hook写法的setState注意点
1、当state所定义的state类型为Object或者Array时,在回调中直接setState是无法成功的
function App() {
const [obj,setObj] = useState({
num:1
});
const clickMe = () => {
setObj(v => {
let newObj = v
newObj.num = v.num + 1 // 直接修改num的值不成功
return newObj
})
}
return (
<button onClick={clickMe}>{obj.num}</button>
);
}
此时num的值一直为1。
原因
由于Object为引用类型,setState通过回调函数的形式赋值,其参数v存的是obj的地址,此时let newObj = v操作将newObj指向obj的地址,由于react中state是只读的,因此newObj.num = v.num + 1这个操作相当于obj.num = obj.num +1,因此无法成功。
解决方案
通过浅拷贝或者深拷贝(相关资料网上很多)可解决此问题,将代码修改如下:
function App() {
const [obj,setObj] = useState({
num:1
});
const clickMe = () => {
setObj(v => {
let newObj = Object.assign({},v) // 对v进行浅拷贝
newObj.num = v.num + 1
return newObj
})
}
return (
<button onClick={clickMe}>{obj.num}</button>
);
}
修改state的同时需要使用state的值时,建议使用函数的方式修改并进行相关的使用操作
setState中数据的合并
当我们多次调用了setState,只会生效最后一次state
import { useState, useEffect } from "react";
const App = function () {
const [num, setNum] = useState(0);
return (
<div>
<h1>{num}</h1>
<button
onClick={() => {
setNum(num + 1);
setNum(num + 1);
setNum(num + 1);
setNum(num + 1);//此时页面呈现的是1
}}
>
点我更新
</button>
</div>
);
};
export default App;
-
setState
合并时进行累加: 给setState传递函数, 使用前一次state
中的值import { useState, useEffect } from "react"; const App = function () { const [num, setNum] = useState(0); return (
{num}
<button onClick={() => { setNum((num) => num + 1); setNum((num) => num + 1); setNum((num) => num + 1); setNum((num) => num + 1);//此时打印的是4 }} > 点我更新
React 的更新流程
-
React
在props
或state
发生改变时,会调用React
的render
方法,会创建一颗不同的树 -
React
需要基于这两颗不同的树之间的差别来判断如何有效的更新UI
:- 同层节点之间相互比较,不会跨节点比较
-
不同类型的节点,产生不同的树结构
-
开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定
-
比如下面的代码更改:
React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用
- 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅对比更新有改变的属性
memo
为什么需要memo?
shouldComponentUpdate
React
给我们提供了一个生命周期方法shouldComponentUpdate
(很多时候,我们简称为SCU
),这个方法接受参数,并且需要有返回值;主要作用是:**控制当前类组件对象是否调用render
**方法
-
该方法有两个参数:
-
参数一:
nextProps
修改之后, 最新的porps
属性 -
参数二:
nextState
修改之后, 最新的state
属性 -
该方法返回值是一个 booolan 类型
-
返回值为
true
, 那么就需要调用render
方法 -
返回值为
false
, 那么不需要调用render
方法 -
比如我们在App中增加一个
message
属性: -
JSX
中并没有依赖这个message
, 那么它的改变不应该引起重新渲染 -
但是通过
setState
修改state
中的值, 所以最后render
方法还是被重新调用了
PureComponent
-
如果所有的类, 我们都需要手动来实现
shouldComponentUpdate
, 那么会给我们开发者增加非常多的工作量- 我们设想一下在
shouldComponentUpdate
中的各种判断目的是什么?
- 我们设想一下在
-
props
或者state
中数据是否发生了改变, 来决定shouldComponentUpdate
返回true
或false
-
事实上
React
已经考虑到了这一点, 所以React
已经默认帮我们实现好了, 如何实现呢?- 将 class 继承自 PureComponent
-
内部会进行浅层对比最新的
state
和porps
, 如果组件内没有依赖porps
或state
将不会调用render
-
解决的问题: 比如某些子组件没有依赖父组件的
state
或props
, 但却调用了render
函数
那么函数式组件如何解决这些问题呢?
- 函数式组件如何解决
render
: 在没有依赖state
或props
但却重新渲染render
问题- 我们需要使用一个高阶组件
memo
- 我们需要使用一个高阶组件
使用memo函数
*
使用方式很简单,在 Function Component 之外,在声明一个 areEqual 方法来判断两次 props 有什么不同,如果第二个参数不传递,则默认只会进行 props 的浅比较。
上面 React.memo() 的使用我们可以发现,最终都是在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。
而在某些场景下,我们只是希望 component 的部分不要进行 re-render,而不是整个 component 不要 re-render,也就是要实现 局部 Pure
功能,这时需要使用useMemo();
使用 useMemo() 进行细粒度性能优化
useMemo()
基本用法如下:
*
useMemo() 返回的是一个 memoized 值,只有当依赖项(比如上面的 a,b 发生变化的时候,才会重新计算这个 memoized 值)memoized 值不变的情况下,不会重新触发渲染逻辑。
说起渲染逻辑,需要记住的是 useMemo() 是在 render 期间执行的,所以不能进行一些额外的副操作,比如网络请求等。
useMemo与useCallback
useMemo和useCallback都是reactHook提供的两个API,用于缓存数据,优化性能;两者接收的参数都是一样的,第一个参数表示一个回调函数,第二个表示依赖的数据。
共同作用
在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用
两者的区别
useMemo 缓存的结果是回调函数中return回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态
useCallback 缓存的结果是函数,主要用于缓存函数,应用场景如需要缓存的函数,因为函数式组件每次任何一个state发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback应该和React.memo配套使用,缺了一个都可能导致性能不升反而下降。