React的setState详解
我们使用React
开发的时候,需要修改state
中的数据,而我们并不能通过直接修改state
中的数据来 实现界面的更新。当然用过Vue
的都知道,Vue
有自己的响应式系统,能够知道哪些数据发生了改变并自动更新视图。
setState的基本使用
React
需要通过setState
这个API来进行视图的重新渲染。这个函数是挂载在Component
的原型上供我们使用。
另外我们还要遵循一个原则就是永远都不要直接修改state
中的属性值,通过setState
来修改也是。**
**如:
//这样修改是不会触发组件的更新
this.state.counter += 1;
//正确的方式如下
this.setState({
counter:this.state.counter+1
})
setState的更新
setState是异步更新的
我们通过setState
修改数据之后是不能立马获取改变后的值的。
//counter初值为1
this.setState({
counter:this.state.counter+1
})
console.log(this.state.counter);//1
关于为什么setState
是异步更新的在官方的github
上有解释 链接
我们每次调用setState
来更新时,视图同步更新,但是多次调用就可能使得视图更新频繁,影响性能,所以设计为异步更新方式是为了批处理更新状态,将多次的更新合并成一次更新。
另外如果同步更新state
而没有及时执行render
,那么state
中的数据就和props
数据不同步。
当然我们也可以获取异步更新后的记结果,setState
第二个参数可以传入一个回调函数,在这个回调函数中就可以获取异步更新后的结果。
this.setState({
counter:this.state.counter+1
}, () => {
console.log(this.state.counter)
})
setState是同步更新的
在某些情况下我们可以让setState
同步更新。
把函数放在计数器中,设置时间为0那就是同步执行, 另外放置在原生的事件执行的回调函数也是同步执行的。
setTimeout(() => {
//初始为1
this.setState({
counter:this.state.counter+1
})
console.log(this.state.counter) //2
}, 0)
当然整体上执行还是异步执行。
所以我们这里可以做个总结:
- 当我们在组件的生命周期或者React的合成事件中执行
setState
,其是异步执行的。 - 当我们在计数器中执行或者原生的回调函数执行
setState
,其是同步执行的。
setState是如何执行的
找到定义方法的地方:
Component.prototype.setState = function(partialState, callback) {
//下面主要是校验和警告
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
//这里是关键,这个updater是每个类组件都拥有的方法
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
点击进入updater:
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
找到如下对象:**
**
主要是调用了enqueueSetState
这个方法,注意eventTime
和lane
这两个常量,点进后面的执行函数,这个函数主要是获取setState
的执行上下文,来判断执行方式---同步或者异步。
export function requestEventTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return now();
}
// We're not inside React, so we may be in the middle of a browser event.
if (currentEventTime !== NoTimestamp) {
// Use the same start time for all updates until we enter React again.
return currentEventTime;
}
// This is the first update since React yielded. Compute a new start time.
currentEventTime = now();
return currentEventTime;
}
export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
//根据mode来判断同步还是批处理
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// old behavior is to give this the same "thread" (lanes) as
// whatever is currently rendering. So if you call `setState` on a component
// that happens later in the same render, it will flush. Ideally, we want to
// remove the special case and treat them as if they came from an
// interleaved event. Regardless, this pattern is not officially supported.
// This behavior is only a fallback. The flag only exists until we can roll
// out the setState warning, since existing code might accidentally rely on
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}
const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
if (
__DEV__ &&
warnOnSubscriptionInsideStartTransition &&
ReactCurrentBatchConfig._updatedFibers
) {
ReactCurrentBatchConfig._updatedFibers.add(fiber);
}
// The algorithm for assigning an update to a lane should be stable for all
// updates at the same priority within the same event. To do this, the
// inputs to the algorithm must be the same.
//
// The trick we use is to cache the first of each of these inputs within an
// event. Then reset the cached values once we can be sure the event is
// over. Our heuristic for that is whenever we enter a concurrent work loop.
if (currentEventTransitionLane === NoLane) {
// All transitions within the same event are assigned the same lane.
currentEventTransitionLane = claimNextTransitionLane();
}
return currentEventTransitionLane;
}
// Updates originating inside certain React methods, like flushSync, have
// their priority set by tracking it with a context variable.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
return updateLane;
}
// This update originated outside React. Ask the host environment for an
// appropriate priority, based on the type of event.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}
最后会调用enqueueUpdate
export function enqueueUpdate<State>(
fiber: Fiber,
//更新链表
update: Update<State>,
lane: Lane,
) {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
if (isInterleavedUpdate(fiber, lane)) {
const interleaved = sharedQueue.interleaved;
if (interleaved === null) {
// This is the first update. Create a circular list.
update.next = update;
// At the end of the current render, this queue's interleaved updates will
// be transferred to the pending queue.
pushInterleavedQueue(sharedQueue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}
sharedQueue.interleaved = update;
} else {
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
if (__DEV__) {
if (
currentlyProcessingQueue === sharedQueue &&
!didWarnUpdateInsideUpdate
) {
console.error(
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
'from inside an update function. Update functions should be pure, ' +
'with zero side-effects. Consider using componentDidUpdate or a ' +
'callback.',
);
didWarnUpdateInsideUpdate = true;
}
}
}
setState的合并更新
数据的合并
我们通过setState
修改state
中的一个数据时,并不会把其他的数据覆盖掉而是会合并。
源码如下:
function getStateFromUpdate<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
update: Update<State>,
prevState: State,
nextProps: any,
instance: any,
): any {
switch (update.tag) {
case ReplaceState: {
const payload = update.payload;
if (typeof payload === 'function') {
// Updater function
if (__DEV__) {
enterDisallowedContextReadInDEV();
}
const nextState = payload.call(instance, prevState, nextProps);
if (__DEV__) {
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
) {
setIsStrictModeForDevtools(true);
try {
payload.call(instance, prevState, nextProps);
} finally {
setIsStrictModeForDevtools(false);
}
}
exitDisallowedContextReadInDEV();
}
return nextState;
}
// State object
return payload;
}
case CaptureUpdate: {
workInProgress.flags =
(workInProgress.flags & ~ShouldCapture) | DidCapture;
}
// Intentional fallthrough
case UpdateState: {
const payload = update.payload;
let partialState;
//如果传入的参数为函数就不会合并执行
if (typeof payload === 'function') {
// Updater function
if (__DEV__) {
enterDisallowedContextReadInDEV();
}
partialState = payload.call(instance, prevState, nextProps);
if (__DEV__) {
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
) {
setIsStrictModeForDevtools(true);
try {
payload.call(instance, prevState, nextProps);
} finally {
setIsStrictModeForDevtools(false);
}
}
exitDisallowedContextReadInDEV();
}
} else {
// Partial state object
partialState = payload;
}
if (partialState === null || partialState === undefined) {
// Null and undefined are treated as no-ops.
return prevState;
}
// Merge the partial state and the previous state.
//合并部分状态和前一个状态。
return Object.assign({}, prevState, partialState);
}
case ForceUpdate: {
hasForceUpdate = true;
return prevState;
}
}
return prevState;
}
可以看到内部是合并处理的。
本身的合并
如果我们多次调用setState
修改state
的值
//初始值为1
this.setState({
counter:this.state.counter+1
})
this.setState({
counter:this.state.counter+1
})
this.setState({
counter:this.state.counter+1
})
实际上内部会将上述的setState
合并为一次调用。内部会判断执行队列中所有setState
的前一次的值和更新后的值是否相等,如果相等就会合并执行(这里发现都是从1变为2)。并不会执行后在之前的基础再次更新。
但是我们有时并不希望合并。
setState
第一个参数可以传入一个函数。同时接收参数
this.setState((preState, props) => {
return {
counter: preState.counter + 1,
};
});
this.setState((preState, props) => {
return {
counter: preState.counter + 1,
};
});
this.setState((preState, props) => {
return {
counter: preState.counter + 1,
};
});
上述操作就是可以累加。内部会判断setState
传入的是对象还是函数,从而是否合并执行。