-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React - setState 学习笔记 #2
Comments
函数式 setState参考文章:
首先我们加深一下对 setState 工作原理的理解:
setState((state, props) => stateChange[, callback])函数式 setState 接收一个函数作为第一参数,该函数接受组件的"先前 state"(previous state)和"当前的 props"(current props),它用于计算并返回"下一个 state"(next state)。 React 为什么致力于推广函数式 setState ?React 一直致力于在 JavaScript 中推广函数式编程。在 setState 的使用上,也明确推荐传递一个函数作为第一参数,而不是直接传递对象。原因如下:
出于性能的考虑,React 会维护队列来批处理多个 setState。若直接传递对象作为 setState 的第一参数,在调用该 setState 时,React 会将传递给
setState 合并类似于 ES6 的 Object.assign(
// 按推入顺序排列
{},
{score: state.score + 1},
{score: state.score + 1},
{score: state.score + 1},
) 如上例所示,对象合并后,由于合并过程中键值对重复会覆盖的特点,最终结果实际上仍是执行了一次 如果我们传入一个函数作为 setState 的第一参数,最终结果与上例会有区别吗?答案是肯定的。 const updateQueue = [
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1})
]; 然后在某一时刻会发生对象合并,合并的顺序遵循 updater 推入队列的数据,可以理解为将 updater 逐一从队列(先进先出)弹入到合并函数内(此处用 Object.assign(
// 按推入顺序排列
{},
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1})
) 为了得到参与合并的源对象,程序会按照顺序调用执行回调函数,这就保证了下一个回调函数内使用的 state 是上一次回调更新后的 state。
函数式 setState 真正强大的功能在于,其可以在组件类之外声明状态更新逻辑。然后在组件类中调用它。 // outside your component class
function increaseScore (state, props) {
return {score : state.score + 1}
}
class User{
...
// inside your component class
handleIncreaseScore () {
// 将逻辑部分分离出 class 类
this.setState( increaseScore )
}
...
} 除此之外你还可以将相关的状态更改逻辑整合到一起,然后通过模块化导入的方式引用。 // 将逻辑通过模块化引入
import {increaseScore} from "../stateChanges";
class User{
...
// inside your component class
handleIncreaseScore () {
this.setState( increaseScore)
}
...
}\
function multiplyBy(number) {
return function increaseScore (state, props) {
return {score : state.score + number}
}
}
class User{
...
// inside your component class
handleIncreaseScore () {
// 现在可以传递不同的 number 来计算下一个 state 了
this.setState( multiplyBy(5) )
}
...
} 总结尽量使用函数式 setState:
|
setState 的合并时机参考文章: 前言看过很多关于 setState 执行机制的文章,它们都提及了 setState 的异步场景,我也从中学习到了 setState 状态批处理的一些思想。但这些文章也都不约而同地避开了 setState 究竟是何时完成对象合并的这一问题。跟随上述文章,实现一个简单的异步 setState,你将解开这一疑惑。 异步 setStatesetState 的批处理优化同步的 setState 有一个明显的问题在于:每次调用setState都会触发更新并马上触发一次渲染,即便每次更新的状态键值对是相同的值。
// 接收新的状态对象
setState( stateChange ) {
// 合并到目标对象
Object.assign( this.state, stateChange );
// 组件渲染
renderComponent( this );
} 因此,React 针对 setState 做了一些优化:React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会立即更新,而是通过维护队列,存储 updater ,然后再在”合适的时机“清空队列,实现对象的合并和组件的渲染。 异步 setState 实现要求
setState 队列为合并多个 stateChange,我们需要一个队列来保存每次 setState 接收的 stateChange,然后在一段时间后,清空这个队列并渲染组件。 // 声明队列
const queue = [];
function enqueueSetState( stateChange, component ) {
// 每一次 setState 执行实际是将接收的 stateChange 推入队列存放
// 此处还接收了 component,主要用于后续获取前一个 state
queue.push( {
stateChange,
component
} );
} 此时 setState 可以写为: setState( stateChange) {
enqueueSetState( stateChange, this );
}
清空队列我们已经实现了将 stateChange 推入队列,此外我们要需要实现一个函数,用于将队列内的 stateChange 逐一取出并最终合并成一个对象(即清空队列并返回新的 state 对象)。 function flush() {
let item;
// 遍历队列
while( item = setStateQueue.shift() ) {
const { stateChange, component } = item;
// 如果没有prevState,则将当前的state作为初始的prevState
// component 就是当前的组件实例,从组件实例上获取 state 属性
if ( !component.prevState ) {
component.prevState = Object.assign( {}, component.state );
}
// 如果 stateChange 是一个方法,也就是setState的第二种形式,则执行该方法,然后将返回结果与目标对象合并
if ( typeof stateChange === 'function' ) {
Object.assign( component.state, stateChange( component.prevState, component.props ) );
} else {
// 如果stateChange是一个对象,则直接合并到setState中
Object.assign( component.state, stateChange );
}
// 获取最新的 state
component.prevState = component.state;
}
} 组件渲染至此我们只实现了 state 的一套更新流程,setState 在 state 状态更新完成后还需要触发 render 渲染组件。渲染组件需要与遍历清空 stateChange 队列分开进行,因为同一个组件可能会多次添加到队列中,我们需要另一个队列保存所有组件,不同之处是,这个队列内不会有重复的组件。 // stateChange 队列
const queue = [];
// 组件队列
const renderQueue = [];
function enqueueSetState( stateChange, component ) {
queue.push( {
stateChange,
component
} );
// 如果 renderQueue 里没有当前组件,则添加到队列中,及避免重复添加相同组件
if ( !renderQueue.some( item => item === component ) ) {
renderQueue.push( component );
}
} 同时在 flush 清空队列的方法中,我们加入遍历渲染 renderQueue 队列的逻辑,保证 renderQueue 队列清空,所有组件都重新渲染。 function flush() {
let item, component;
while( item = queue.shift() ) {
// ...
}
// 渲染每一个组件
while( component = renderQueue.shift() ) {
renderComponent( component );
}
} ★ 重点:何时执行 flush 方法(合并对象并重新渲染组件)我们已经实现了 stateChange 和对应组件的存储,同时也实现了这两个存储队列的清空过程。在队列的存储过程触发时机比较明显,即当我们组件调用 setState 时触发。
当存储过程完成后,我们需要合并一段时间内所有的setState,也就是在一段时间后才执行flush方法来清空队列,关键是这个“一段时间“怎么由程序自行决定。
定义一个延迟执行函数的方法 function defer( fn ) {
return Promise.resolve().then( fn );
} 将 flush 函数通过该延迟方法调用,使其在队列 function enqueueSetState( stateChange, component ) {
// 空队列时调用 defer(flush),此时 flush 被推入事件循环的任务队列,而不是立即执行
if ( queue.length === 0 ) {
defer( flush );
}
// 同步代码被推入主线程并执行
queue.push( {
stateChange,
component
} );
if ( !renderQueue.some( item => item === component ) ) {
renderQueue.push( component );
}
// 所有同步代码执行完毕后,再从任务队列调用 flush 执行 state 更新以及组件渲染。
} 总结一个 React 组件调用 setState 后的一系列过程:
|
setState 下的正确 state 调用
setState((prevState, props) => stateChange[, callback]) 在 updater (第一个参数)为函数时,其接收上一个 state 状态以及当前 props 作为参数。
注意
|
React - setState 学习笔记
参考文章:
setState用法
setState(updater[, callback])
setState(stateChange[, callback])
setState((state, props) => stateChange[, callback])
setState执行流程
执行顺序:
setState -> render -> componentDidUpdate -> setState-callback
setState 不能保证同步。
setState异步执行
setState 在合成事件处理程序(React 内部对原生事件处理程序封装后的事件)以及生命周期中的更新是异步的。
updater 会被放入到队列中,这就导致 setState 执行完后不会立即更新 state,触发重新渲染。多次 setState 的调用只会产生一次批量更新,触发一次重新渲染。
setState 的”异步“与 JS 中常说的异步性质不同。setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是 setState 合并对象的操作被安排在了所有同步操作之后,导致在同步操作中没法立马拿到更新后的值,形式了所谓的“异步”。
setState同步执行
setState 在原生的事件处理函数以及异步回调函数中是同步执行的。
同步的 setState 执行后会立即执行 render()、componentDidUpdate(),再执行下一个 setState。即遵循 setState 的执行流程。
未来
摘录自React的核心成员 - Dan Abramov 的回答
Currently (React 16 and earlier), only updates inside React event handlers are batched by default. There is an unstable API to force batching outside of event handlers for rare cases when you need it.
In future versions (probably React 17 and later), React will batch all updates by default so you won't have to think about this. As always, we will announce any changes about this on the React blog and in the release notes.
The text was updated successfully, but these errors were encountered: