setState是同步还是异步
在react中,修改状态如果直接使用this.state,不会引起组件的重新渲染,需要通过 this.setState来对组件的属性进行修改。
1、this.setState的两种定义方式
定义初始状态
state = { count: 0 },
如果此时有一个按钮,点击按钮让计数加1,我们可以有两种写法
(1)传递对象
this.setState({ count: this.state.count + 1})
(2)传递函数
this.setState((state, props) => ({ count: count + 1}))
2、setState的两种方式有什么不同?
如果变更的state的值需要依赖于上一次的state的值,这种情况就需要用到函数的形式,比如以下这种情况
addCount(){
this.setState({ count: this.state.count + 1})
this.setState({ count: this.state.count + 1})
this.setState({ count: this.state.count + 1})
}
此时只会执行一次+1的操作,因为在React内部,会将多次setState合并操作,新的state由 Object.assgin({}, {count: 0}, { count: 1}) 合并所得,以上赋值会执行三次,但因为count的值没有更新,所以最终执行的结果只+1,如果setState的赋值是函数,那情况就不一样了
addCount(){
this.setState((state, props) => ({ count: count + 1}))
this.setState((state, props) => ({ count: count + 1}))
this.setState((state, props) => ({ count: count + 1}))
}
这样的操作会得到+3的效果,因为React会进行判断,如果传入的是函数,那么将执行此函数,此时count的值就已经被修改了
3、setState是同步还是异步的?
☆☆☆☆☆ 是异步的
(1) 即我们通过this.setState修改了状态之后,在它的下一行输出state的值并不会得到新的值
(2) 为什么是异步?
有两个原因,一是提高效率,每次修改state的值都会造成render的重新渲染,将多次修改state的值合并统一更新可以提高性能;二是render的更新会晚一些,如果render中有子组件,子组件的props依赖于父组件的state,props和state就不能保持一致
(3) 如何获取异步时的state值?
① 在setState的回调函数中
this.setState({
count: this.state.count + 1}},
()=>{ console.log(this.state.count)})
② 在componentDidUpdate中获取
componentDidUpdate(){
console.log(this.state.count)
}
☆☆☆☆☆ 是同步的
(1) 即我们通过this.setState修改状态之后,在它的下一行输出state是新的值
(2) 什么场景下是同步的?
① 原生js获取dom元素,并绑定事件
<button id="addBtn">点我+1</button>
componentDidMount(){
const addBtn = document.getElementById('addBtn')
changeBtn.addEventListener('click',()=>{
this.setState({ count: this.state.count + 1})
console.log(this.state.message)
})
}
② 计时器 setTimeout
<button onClick={ e => this.addOne() }>点我+1</button>
addOne(){
setTimeout(()=>{ this.setState({ count: this.state.count + 1 })
console.log(this.state.count ) },0)
}
如何实现useState改变值之后立刻获取最新的状态?
之前铺垫了一堆,终于到正题了,本想直奔主题,一不小心变成了介绍react-hooks的文章,不过多了解一点终究不会亏什么,react-hooks的思想其实是同步逻辑,但是在react的合成事件中状态更新是异步的,看一下下面这个场景,使用useState声明了一个状态state,又分别声明了两个函数setT和func,在setT中,调用setstate更新状态,然后调用func函数,在func中直接打印当前状态state
function App() {
const [state, setstate] = useState(0);
const setT = () => {
setstate(2);
func();
};
const func = () => {
console.log(state);
};
return (
<div className="App">
<header className="App-header">
<button onClick={setT}>set 2</button>
</header>
</div>
);
}
当我们点击set 2 按钮时,控制台打印的还是上一次的值0,而不是最新的2
因为在react合成事件中改变状态是异步的,出于减少render次数,react会收集所有状态变更,然后比对优化,最后做一次变更,在代码中可以看出,func的调用和setstate在同一个宏任务中,这是react还没有render,所以直接使用state获取的肯定是上一次闭包里的值0
有的人可能会说,直接将最新的值当作参数传递给func不就行了吗,对,这也是一种解决办法,但是有时不只是一个状态,可能要传递的参数很多,再有也是出于对react-hooks的深入研究,所以我选择通过自定义hooks实现在useState改变值之后立刻获取到最新的值,我们先看下实现的效果,代码变更如下:
function App() {
const [state, setstate] = useState(0);
const setT = () => {
setstate(2);
func();
};
/** 将func的方法传递给useSyncCallback然后返回一个新的函数 */
const func = useSyncCallback(() => {
console.log(state);
});
return (
<div className="App">
<header className="App-header">
<button onClick={setT}>set 2</button>
</header>
</div>
);
}
export default App;
效果如下:
可以看出当我们点击set 2按钮之后,控制台输出的直接是2,所以达到了在setstate之后立刻获取了最新状态
三、如何实现useSyncCallback?
先把代码放到这里,大家可以看一下
/*
* @lastTime: 2021-03-05 15:29:11
* @Description: 同步hooks
*/
import { useEffect, useState, useCallback } from 'react'
const useSyncCallback = callback => {
const [proxyState, setProxyState] = useState({ current: false })
const Func = useCallback(() => {
setProxyState({ current: true })
}, [proxyState])
useEffect(() => {
if (proxyState.current === true) setProxyState({ current: false })
}, [proxyState])
useEffect(() => {
proxyState.current && callback()
})
return Func
}
export default useSyncCallback
/*
* @lastTime: 2021-02-26 15:29:11
* @param: callback为回调函数
* @Description: 用法 const newFunc = useSyncCallback(yourCallback)
*/
在这里还需要介绍一个知识点useEffect,useEffect会在每次函数式组件render之后执行,可以通过传递第二个参数,配置依赖项,当依赖项变更useEffect才会更新,如果第二个参数传递一个空数组[],那么useEffect就会只执行一次,react-hooks正是使用useEffect来模拟componentDidMount和componentDidUpdate两个生命周期,也可以通过在useEffect中return一个函数模拟componentWillUnmount,那么useEffect是如何实现在render之后调用的呢,原理就是在useEffect hooks中会封装一个宏任务,然后把传进来的回调函数放到宏任务中去执行,下面实现了一个简版的useEffect:
/** @param hookStates 存储所有hooks */
const hookStates = [];
/** @param hookIndex 索引 */
let hookIndex = 0;
/**
*
* useReducer
* @param {*} callback 副作用函数
* @param {*} dependencies 依赖项
* @returns
*/
function useEffect(callback, dependencies) {
/** 判断当前索引下hookStates是否存在值 */
if (hookStates[hookIndex]) {
/** 如果存在就取出,并结构出destroyFunc销毁函数,和上一次依赖项 */
const [destroyFunc, lastDep] = hookStates[hookIndex];
/** 判断当前依赖项中的值和上次依赖项中的值有没有变化 */
const isSame =
dependencies &&
dependencies.every((dep, index) => dep === lastDep[index]);
if (isSame) {
/** 如果没有变化就把索引加一,hooks向后遍历 */
hookIndex++;
} else {
/** 如果有变化,并且存在销毁函数,就先调用销毁函数 */
destroyFunc && destroyFunc();
/** 创建一个新的宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
setTimeout(() => {
const destroyFunction = callback();
hookStates[hookIndex++] = [destroyFunction, dependencies];
});
}
} else {
/** 如果hookStates中不存在,证明是首次执行,直接创建一个宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
setTimeout(() => {
const destroyFunction = callback();
hookStates[hookIndex++] = [destroyFunction, dependencies];
});
}
}
我已经将useEffect的实现步骤写到了注释里,有兴趣的可以看一下,接下来我们就来使用useEffect实现我们想要的结果,因为useEffect是在react组件render之后才会执行,所以在useEffect获取的状态一定是最新的,所以利用这一点,把我们写的函数放到useEffect执行,函数里获取的状态就一定是最新的
useEffect(() => {
proxyState.current && callback()
})
首先,在useSyncCallback中创建一个标示proxyState,初始的时候会把proxyState的current值赋成false,在callback执行之前会先判断current是否为true,如果为true就允许callback执行,若果为false,就跳过不执行,因为useEffect在组件render之后,只要依赖项有变化就会执行,所以我们无法掌控我们的函数执行,在useSyncCallback中创建一个新的函数Func,并返回,通过这个Func来模拟函数调用,
const [proxyState, setProxyState] = useState({ current: false })
在这个函数中我们要做的就是变更prxoyState的current值为true,来使得让callback被调用的条件成立,同时触发react组件render这样内部的useEffect就会执行,随后调用callback实现我们想要的效果。