引言
咱们在一些面试题中常常会遇到一个问题,那就是state的扭转是同步还是异步的。当初本人只是一味的记住事件触发导致state的扭转是异步的,其它状况下都是同步的,并不了解其中的原因。接下来就为大家梳理下其中的原因,知其然也要知其所以然。
这篇文章我会别离会对类组件和函数组件中的state的应用和更新机制进行简略的介绍。
类组件中的state
根本用法
在书写类组件时,通过在类中申明state
变量来保护状态,通过setState
办法来更新存储的状态信息
this.setState(updater[, callback])
obj
这个参数能够传递两个类型:
- 对象类型:当传入的参数是对象类型时,那么会将这个对象合并到
state
外面去,更新state的状态。 - 函数类型:会将以后的state和props当做参数,返回值用于与原来的
state
进行合并,更新state的状态。
callback
回调函数中能够获取到最新的state
状态信息,能够作为依赖state
变动执行一些副作用操作。
state更新机制
class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0, hello: "hello world", }; } handleClick = () => { this.setState({ count: this.state.count + 1 }, () => { console.log("callback1", this.state.count); }); console.log("log1", this.state.count); this.setState({ count: this.state.count + 1 }, () => { console.log("callback2", this.state.count); }); console.log("log2", this.state.count); this.setState({ count: this.state.count + 1 }, () => { console.log("callback3", this.state.count); }); console.log("log3", this.state.count); }; render() { console.log("render this", this); return ( <button onClick={this.handleClick}> {this.state.hello}: {this.state.count} </button> ); }}
打印后果:log1 0 ,log2 0,log3 0;cb1 1,cb2 1,cb3 1
依照原先的想法执行一次点击操作会执行3次加1的操作,然而从打印后果上来看丝毫没有失效,一起来看下执行流程。
其实每次在执行setState的时候,state的状态信息还没有被更新过,每次都是应用的上次的state状态,所以3次打印的后果都是0。这种景象背地的起因是React会期待所有事件处理函数都解决函数调用的setState调用实现之后,再进行对立更新状态,防止反复渲染的问题。
那么能不能更新state而不进行从新渲染呢?答案是不能,起因有以下两点,这个在React官网中有提到。
- 这样会毁坏掉props和state之间的一致性,造成一些难以debug的问题。
- 一些新性能变得无奈实现。
在React的issue外面有具体的解释:https://github.com/facebook/react/issues/11527
那么在事件处理函数中为何应用setTimeout
和Promise
会突破这种批量更新导致的异步操作?
handleClick = () => { setTimeout(() => { this.setState({ count: this.state.count + 1 }, () => { console.log("callback1", this.state.count); }); console.log("log1", this.state.count); this.setState({ count: this.state.count + 1 }, () => { console.log("callback2", this.state.count); }); console.log("log2", this.state.count); this.setState({ count: this.state.count + 1 }, () => { console.log("callback3", this.state.count); }); console.log("log3", this.state.count); });};
在下面的代码出对handleClick
内的代码应用setTimeout
来进行包裹,当初来看看打印后果
打印后果:callbakc1 1, log1 1,callbakc2 2, log2 2, callback 3,log3 3。那么当初程序执行的流程图就变为上面的这种模式了。
每次调用setState函数后会更新state,而后进行从新进行渲染,随后调用setState提供的回调函数,回调函数执行实现后,代码会继续执行前面的代码。
留神: 如果React版本是v18的话,setTimeout和Promise也是应用批量更新的形式如果须要及时更新,有上面两个方法
- 对
React
做降级解决 - 应用
ReactDOM.render(<MyApp />, document.getElementById("root"));
,应用这种形式相当于降级解决。
那如何在上述的代码中开启批量更新呢?
应用ReactDom提供的unstable_batchedUpdates
开启批量更新。不要被前缀unstable
所影响,现在这个Api曾经内置React v18中去了,无需放心稳定性问题。
在Redux官网文档中,提供了batch函数中,其实就是unstable_batchedUpdates
的别名。上面提供redux文档的局部内容:
React's unstable_batchedUpdates() API allows any React updates in an event loop tick to be batched together into a single render pass. React already uses this internally for its own event handler callbacks. This API is actually part of the renderer packages like ReactDOM and React Native, not the React core itself.
Since React-Redux needs to work in both ReactDOM and React Native environments, we've taken care of importing this API from the correct renderer at build time for our own use. We also now re-export this function publicly ourselves, renamed to batch(). You can use it to ensure that multiple actions dispatched outside of React only result in a single render update, like this:
handleClick = () => { setTimeout(() => { ReactDom.unstable_batchedUpdates(() => { this.setState({ count: this.state.count + 1 }, () => { console.log("callback1", this.state.count); }); console.log("log1", this.state.count); this.setState({ count: this.state.count + 1 }, () => { console.log("callback2", this.state.count); }); console.log("log2", this.state.count); this.setState({ count: this.state.count + 1 }, () => { console.log("callback3", this.state.count); }); console.log("log3", this.state.count); } });};
函数组件中的state
在React v16.8
中,Hooks
的呈现让函 数组件焕发新生,让过后的React
开发者感到大为惊叹,从此函数组件成为编写React
组件的首选形式。
根本用法
const [state, dipatchState] = useState(initialData)
state
:保护状态信息的变量dipatchState
:用于扭转state
的函数,间接对state
进行批改是不会被容许的,接管的参数类型如下:
- 非函数:参数会间接替换原有的
state
,而不是进行合并。 - 函数:会接管
state
和props
两个参数,这两个参数都是以后最新的状态,返回的后果会更新到state
外面。
函数组件中state更新机制
function Counter(props) { const [count, setCount] = useState(0); console.log("outerCount", count); const handleClick = () => { setCount(count + 1); console.log("count1: ", count); setCount((count) => { console.log('funcCount', count); return count + 1; }); console.log("count2: ", count) }; return <button onClick={handleClick}>count:{count}</button>;}
打印后果:count1 0,count2 0,funcCount 1,outerCount 1。
前三次打印后果都是0,背地的起因很简略,所有扭转的state
只有在下一次函数组件被执行的时候才会更新,在以后函数的执行上下文里的count都是本次函数组件渲染时的初始值。
留神:通过给dispathState传递的函数参数可能拿到最新的state,但不会影响以后函数执行上下文中state的值
在类组件中,能够通过给setState传递回调函数来监测state的变动,那么在函数组件中如何来检测state的变动呢?
答案是能够通过向useEffect的依赖中退出state,当state变动后,useEffect外面的函数会被从新执行。
把下面的代码稍稍精简一下
function Counter(props) { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; return <button onClick={handleClick}>count:{count}</button>;}
假如点击两次按钮,那么流程就是
每次执行函数组件都会造成一次快照,上图每个块状图就是一次快照,每次快照都有本人的props
、state
、事件处理函数等其它在函数组件外面申明的变量和函数。
// 第一次渲染function Counter(props) { count = 0 const handleClick = () => { setCount(count + 1); }; <button onClick={handleClick}>count:{count}</button>;}// 第二次渲染function Counter(props) { count = 1 const handleClick = () => { setCount(count + 1); }; <button onClick={handleClick}>count:{count}</button>;}// 第三次渲染function Counter(props) { count = 2 const handleClick = () => { setCount(count + 1); }; <button onClick={handleClick}>count:{count}</button>;}
如何了解每次渲染都有本人的事件处理函数?借助Dan提供的例子来阐明问题。
function Counter() { const [count, setCount] = useState(0); function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); } return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={handleAlertClick}> Show alert </button> </div> );}
在3s的工夫内,间断点击2次以上,发现在3s过后,仍然可能正确的展现数字。每次渲染都产生了新的handleAlertClick
事件处理函数,这个函数捕捉了当次渲染的count
。这里其实借助了闭包的个性,顺利捕捉了正确的count。
留神:应用useState提供的dipatch函数更新state,dispatch函数会浅比拟前后两次state,如果比拟相等,那么会造成视图不更新。
function App(){ const [ state, dispatchState ] = useState({ name:'ztq' }) const handleClick = ()=>{ // 点击按钮,视图没有更新。 state.name = 'raysuner' dispatchState(state) // 间接扭转 `state`,在内存中指向的地址雷同。 } return ( <div> <span> { state.name }</span> <button onClick={ handleClick }>changeName</button> </div> )}