概念
补充:下面的结论针对的是
React18之前。(最近看了React18版本,其更新机制发生了变化)
所谓的执行机制:就是看useState或者setState到底是同步函数的表现形式还是异步的表现形式。
先看结论: setState 和 useState 在react的合成事件和钩子函数中的异步的表现形式。在原生事件和setTimeout,Promise的then方法等是同步的表现形式。
这里的异步表现形式:并不是说它是由异步函数实现的,其实函数的本身是同步代码,但是呢,是因为合成事件 和 钩子函数的调用顺序在更新之前,导致在合成事件个钩子函数中不能立即拿到更新后的值,就表现出了异步的形式。
批量更新: 也是建立在"异步表现形式"上体现的,如果每一次修改state,就触发一次render函数,如果多次修改,就会触发多次,就会造成性能的浪费。所以,为了性能的优化,就先收集,然后一起更新,触发一次render函数。
useState的更新测试
const App: React.FC = () => {
const [name, setName] = useState<string>('james')
const [age, setAge] = useState<number>(23)
const btn1 = () => {
setName('kobe')
setAge(24)
}
const btn2 = () => {
Promise.resolve().then(() => {
setName('curry')
setName('30')
})
}
// 看打印了几次render
console.log('------render------')
return <div>
<button onClick={ btn1 }>同步函数</button>
<button onClick={ btn2 }>异步函数</button>
</div>
}
同步函数: 1次render触发
异步函数: 2次render触发
同步函数中,就使用批量更新,两次修改state,只触发了一次render。
类组件中的setState也是这么一回事,可以自己动手试一试。
批量更新的处理方式
先看类组件
export default class TestClass extends Component {
state = {
count: 1
}
btn1 = () => {
this.setState({count: this.state.count + 1})
this.setState({count: this.state.count + 1})
}
btn2 = () => {
this.setState(prev => ({count: prev.count + 1}))
this.setState(prev => ({count: prev.count + 1}))
}
render() {
return (
<div>
<h1>{ this.state.count }</h1>
<button onClick={this.btn1}>对象形式</button>
<button onClick={ this.btn2 }>函数形式</button>
</div>
)
}
}
对象形式: count值为 2
函数形式: count值为 3
类组件的批量更新的策略:
针对对象形式:对象合并(类似Object.assign),针对相同属性名,后面属性覆盖前面属性。
this.setState({count: this.state.count + 1})
this.setState({count: this.state.count + 1})
// 合并为
this.setState({count: this.state.count + 1})
// 所以count为2
针对函数形式: 函数收集,然后依次执行
this.setState(prev => ({count: prev.count + 1}))
this.setState(prev => ({count: prev.count + 1}))
// 执行两次 prev => ({count: prev.count + 1}),所以count为3
学习完了类组件的批量更新,就来看看函数组件。
函数组件分为直接传递参数和 函数形式
const App: React.FC = () => {
const [count, setCount] = useState(1)
const btn1 = () => {
setCount(1)
setCount(2)
}
const btn2 = () => {
setCount(prev => prev + 1)
setCount(prev => prev + 2)
}
return <div>
<h1>{ count }</h1>
<button onClick={ btn1 }>直接传参</button>
<button onClick={ btn2 }>函数形式</button>
</div>
}
直接传参: count 为 2
函数形式: count 为 4
函数批量更新策略:
针对直接传参形式: 简单理解就是覆盖,后面覆盖前面的
setCount(1)
setCount(2)
// 后面覆盖前面,count为2
针对函数形式:函数收集,依次执行
setCount(prev => prev + 1)
setCount(prev => prev + 2)
// 收集函数,依次执行
// prev => prev + 1
// prev => prev + 2
// 所以count = 1 + 1 + 2 = 4
批量更新策略总结
| 类组件 | 函数组件 |
|---|---|
| 对象形式:合并 | 传参形式:覆盖 |
| 函数形式:收集 | 函数形式:收集 |
强行批量更新
在setTimeout等中,useState或则setState是不存在批量更新的(批量更新只存在 '异步的表现形式'中)。但是有时候,在一些请求中,根据返回的数据,就是会多次修改state,那么就会多次出发render函数,造成性能浪费,那么这时候该怎么处理呢?
在react-dom库中,提供了一个函数,就专门用来进行批量更新的。 unstable_batchedUpdates
具体使用:
const btn2 = () => {
setTimeout(() => {
setCount(prev => prev + 1)
setCount(prev => prev + 2)
}, 0)
}
// 会触发两次render
const btn2 = () => {
setTimeout(() => {
unstable_batchedUpdates(() => {
setCount(prev => prev + 1)
setCount(prev => prev + 2)
})
}, 0)
}
// 只会触发一次render
所以,有的时候,我们可以使用这个函数,来进行一些性能优化,减少render的次数。
捕获值(Capture Value)
// 类组件的点击事件(count = 1)
btn1 = () => {
const { count } = this.state
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log('this.state.count', this.state.count)
console.log('count', count)
}, 3000)
}
有一个setTimeout,点击后,3秒后执行,在这3秒中,快速点击5次
// 打印
this.state.count 2
count 1
this.state.count 3
count 1
this.state.count 4
count 1
this.state.count 5
count 1
this.state.count 6
count 1
在上面,我们发现,react组件中state.count是已经发生了变化,但是在setTimeout函数中,count的值,是没有发生变化的。为什么呢?
由于闭包的关系, 在setTimeout函数中,使用了外部环境的变量(count),形成了闭包。那么setTimeout函数中就只会捕获,函数执行那一刻的外部变量的值,那时候count的值为1,所以count的值一直是1。
如果理解了上面的 值捕获,那么下面的函数组件的实例就非常的好懂了。
const btn1 = () => {
setTimeout(() => {
setCount(prev => prev + 1)
console.log('count', count)
}, 3000)
}
// 或则是
const btn1 = () => {
setCount(prev => prev + 1)
console.log('count', count)
}
点击了,打印的count值始终是1, 因为无论 btn1 还是 setTimeout,都是一个函数,使用了外部的变量,就形成了闭包,捕获的是执行那一刻的值。
面试题 (执行顺序)
const App: React.FC = () => {
const [value1, setValue1] = useState('a')
const [value2, setValue2] = useState('b')
const [value3, setValue3] = useState('c')
useEffect(() => {
setTimeout(() => {
console.log('------1------');
setValue1('new_james')
console.log('------2------');
setValue2('new_kobe')
console.log('------3------');
setValue3('new_curry')
}, 200);
}, [])
useEffect(() => {
console.log('------4------');
setValue1('new_james')
}, [value1])
useEffect(() => {
console.log('------5------');
setValue2('new_kobe')
}, [value2])
console.log('------render------')
return <div>测试</div>
}
给出你的打印结果。。。
补充React18
最近也了解了一些React18的新特性,其中之一:React18的更新策略发生了变化。
在 react18 中,都是采用的批量更新,无论是在同步的表现形式中还是异步的表现形式中。
在React17(上面的说到),在 合成事件 和 钩子函数 中是批量更新,在 异步函数 和 原生DOM事件 中,都不是采用的批量更新。
示例:
类组件
class App extends Component<any, IState> {
constructor(props: any) {
super(props);
this.state = {
count: 1,
};
}
// 同步的表现形式
btn1 = () => {
this.setState((prev) => ({ count: prev.count + 1 }));
this.setState((prev) => ({ count: prev.count + 1 }));
};
// 异步的表现形式
btn2 = () => {
setTimeout(() => {
this.setState((prev) => ({ count: prev.count + 1 }));
this.setState((prev) => ({ count: prev.count + 1 }));
}, 0);
};
render() {
console.log("render渲染的次数");
return (
<div>
<button onClick={this.btn1}>同步函数</button>
<button onClick={this.btn2}>异步函数</button>
</div>
);
}
}
函数组件
function App() {
const [count, setCount] = React.useState<number>(1);
const btn1 = () => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};
const btn2 = () => {
setTimeout(() => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}, 0);
};
console.log("render渲染的次数");
return (
<div className="App">
<button onClick={ btn1 }>同步函数</button>
<button onClick={ btn2 }>异步函数</button>
</div>
);
}
这里你就会发现,无论是函数组件还是类组件,在合成事件还是异步函数中,都是采用的批量更新。每次点击按钮的时候,只会打印一次render。
注意: 要关闭
React.StrictMode模式。React18中,当使用严格模式时,React 会对每个组件进行两次渲染,以便你观察一些意想不到的结果。
破坏批量更新
既然知道了React18都是采用的批量更新,那么怎么破坏react的批量更新呢?(当然这种操作,还是少操作为好)。
在react17中提供了 unstable_batchedUpdates 函数,用来合并批量操作。
在react18中提供了 flushSync 函数,用来取消批量操作。(当然 unstable_batchedUpdates也没被废弃掉,还是可以使用,可能在未来有可能被删除吧)
import { flushSync } from "react-dom";
const add = () => {
flushSync(() => {
setCount1(count1 + 1);
});
flushSync(() => {
setCount2(count2 + 1);
});
}
// 调用add(),flushSync包裹,render方法将会触发两次
总结
React17 和 React18 批量更新的策略变化。React17根据情况而采用不同的更新策略,React18就统一的采用更新策略,在学习中成本上减少负担,以及在开发上,可以不用考虑render渲染次数,带来的性能问题。(我总是在请求后台接口,返回数据时使用unstable_batchedUpdates函数来减少渲染次数,优化)。
在未来,全面拥抱react18吧!