准备
开发环境,在Chrome浏览器下,以及安装了React DevTools情况下,App组件总会render两次(原因不详,依稀记得之前有看过React新版在开发环境会主动调用组件两次,帮助排查生命周期一些不易发现的bug,以及为后面的大升级铺路),并且不光是进入页面,发生事件改变状态也类似。
App组件render两次
但不利于此文的调试,所以请打开Chrome无痕模式进行调试。
需求
money = price * num
使用useState
setXxx是异步的
function App() {
const [price, setPrice] = useState(0);
const [num, setNum] = useState(0);
const [money, setMoney] = useState(0);
function changePrice() {
setPrice(8);
setMoney(price * num);
}
function changeNum() {
setNum(2);
setMoney(price * num);
}
function changePriceAndNum() {
setPrice(6.6);
setNum(3);
setMoney(price * num);
}
return (
<div id="App" className="App">
<p>Price: {price}<button onClick={changePrice}>change price</button></p>
<p>Num: {num}<button onClick={changeNum}>change num</button></p>
<p>Money: {money}</p>
<button onClick={changePriceAndNum}>change price and num</button>
</div>
);
}
代码看起来很美好,但是很不幸,这段代码都不能工作,当点击“change price and num”时候,结果是这样:
结果并不正确
因为setPrice和setNum都是异步的,changePriceAndNum函数执行时候,取得的price和num还是当前状态,并不是setPrice和setNum之后的状态。
当再次点击按钮,结果就正确了。
进行改造
function App() {
const [price, setPrice] = useState(0);
const [num, setNum] = useState(0);
const [money, setMoney] = useState(0);
function changePrice() {
const newPrice = 8; // 设置价格变量
setPrice(newPrice);
setMoney(newPrice * num);
}
function changeNum() {
const newNum = 2; // 设置数量变量
setNum(newNum);
setMoney(price * newNum);
}
function changePriceAndNum() {
const newPrice = 6.6, newNum = 3; // 设置价格和数量变量
setPrice(newPrice);
setNum(newNum);
setMoney(newPrice * newNum);
}
return (
<div id="App" className="App">
<p>Price: {price}<button onClick={changePrice}>change price</button></p>
<p>Num: {num}<button onClick={changeNum}>change num</button></p>
<p>Money: {money}</p>
<button onClick={changePriceAndNum}>change price and num</button>
</div>
);
}
解决useState的bug
好了,这下正常工作了,因为把值和状态解耦了,不存在异步设置状态而获取不到的问题了,但是:
- 代码不够直观,比较复杂
- 上面代码,一个事件只是在一个函数体里执行,可以这么定义变量。这在真实业务中是不可能的,多层函数调用,想按这思路解决,就得当参数一层层传过去。
- 逻辑无法从组件拆分出来
使用useReducer
我们使用useReducer改写这个代码
const initialState = {
price: 0,
num: 0,
money: 0,
};
function myReducer(state, action) {
console.log(JSON.parse(JSON.stringify(state)))
switch(action.type) {
case 'changePrice':
return {
...state,
price: action.price,
money: action.price * state.num
}
case 'changeNum':
return {
...state,
num: action.num,
money: state.price * action.num
}
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(myReducer, initialState);
return (
<div id="App" className="App">
<p>Price: {state.price}<button onClick={() => dispatch({type:'changePrice', price: 8})}>change price</button></p>
<p>Num: {state.num}<button onClick={() => dispatch({type:'changeNum', num: 2})}>change num</button></p>
<p>Money: {state.money}</p>
<button onClick={() => {
dispatch({type:'changePrice', price: 6.6});
dispatch({type:'changeNum', num: 3});
}}>change price and num</button>
</div>
);
}
对于复杂逻辑,使用useReducer改造代码后,不用考虑状态异步的问题,性能也好。相关的业务逻辑也可以拆分到单独文件去了,使组件更干净。
奇怪的事情
1 useState 状态不改变,组件函数依然执行
function App() {
console.log("App render -->"+ performance.now())
const [num, setNum] = useState(0);
function changeNum() {
setNum(8);
}
return (
<div id="App" className="App">
num: {num}
<button onClick={changeNum}>change num</button>
</div>
);
}
- num初始为0
- 点击按钮,num变成8,打印了App render -->,这是肯定的
- 再次点击按钮,num还是8,没变,但仍然打印了App render-->,不好理解
- 再次点击按钮,就不会打印App render -->了
查询了一下,没有查到特别精确的解释,感觉跟下面这个issue可能类似。而且步骤2会真真切切会重新渲染,步骤3只是执行一下App函数,并不会重新渲染App组件。并且如果有子组件,子组件函数也不会在步骤3执行。所以不用太担心性能(所以一个相对解决办法是把复杂代码写子组件去)。
useState not bailing out when state does not change #14994
2 useState将状态改变写在setTimeout里,一组操作会触发多次组件函数执行
此情况是我在hashChange事件中发现的现象,然后发现setTimeout行为一致。可以猜想,异步调用譬如获取数据应该都是类似机制
function App() {
console.log("App render -->"+ performance.now())
const [a, setA] = useState('A');
const [b, setB] = useState('B');
const [c, setC] = useState('C');
function changeState() {
console.log('begin')
setA('AA');
console.log('a end')
setB('BB');
console.log('b end')
setC('CC');
console.log('c end')
}
return (
<div id="App" className="App">
a: {a}, b: {b}, c: {c}
<button onClick={changeState}>change state</button>
</div>
);
}
点击按钮的时候,改变了三个状态,只会触发一次App渲染:
普通调用
但是如果将代码包在setTimeout里,就会触发三次App渲染(真真切切的三次渲染,不是像上面那样执行一下App函数而已):
function App() {
console.log("App render -->"+ performance.now())
const [a, setA] = useState('A');
const [b, setB] = useState('B');
const [c, setC] = useState('C');
function changeState() {
setTimeout(() => {
console.log('begin')
setA('AA');
console.log('a end')
setB('BB');
console.log('b end')
setC('CC');
console.log('c end')
})
}
return (
<div id="App" className="App">
a: {a}, b: {b}, c: {c}
<button onClick={changeState}>change state</button>
</div>
);
}
setTimeout调用
不知道对于异步代码,做了什么样的处理,虽然是setTimeout异步执行,但是这三行代码是同步的啊。
网友评论