对于一个React的开发者来说不知道你有没有想过为什么React追求数据不可变这个范式;
一个月前我想过一个问题如果我在使用useState这个hooks的时候传入的是一个改变后的引用类型对象会发生什么?
例如:
import {useState} from "react"
function App() {
const [list,setList] = useState([0,1,2])
const handleClick = ()=>{
list.push(list.length)
setList(list)
}
return (
{list.map(item=>{item})}
);
}
export default App;
然后当我们点击按钮的时候会发生什么呢?答案是从我们的视觉感官来讲什么也没有发生!列表数据一直是012;
关于这个结果我相信百分之99的react开发者都是可以预料的!也肯定有百分之80以上的人会说因为你的新数据和老数据是同一个(newState===oldState)===true在这个问题上答案也确实是这个一个。那么newState与oldState是在哪里做的比较,又是在哪里做的拦截呢?我之前想的是会在render阶段update时的reconcileChildFibers中打上effectTag标记判断前做的判断,然而当我今天在给beginWork后我发现以上这个代码压根走不到beginWork (mount阶段),带着好奇我决定从源码出发去探索一下(答案可能会有点无聊);
我们知道useState这个hooks生成
const [list,setList] = useState([0,1,2])
是dispatchAction这个方法
mountState阶段
而useState分为两种mountState与updateState,因为setList是在mount时被创建的所以我们先去查看他是如何被创建的
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
hook.queue = queue;
//创建dispatch方法并保存到链式当中
//dispatch是通过dispatchSetState这个方法创建的
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
//这一步return出链式当中的list与setList
return [hook.memoizedState, dispatch];
}
dispatch是通过dispatchSetState这个方法创建的,然后我们去dispatchSetState中去查看
function dispatchSetState(fiber, queue, action) {
//此处打上console,可以正常输出,程序可以进行到此步
console.log('dispatchSetState',fiber,queue,action)
{
if (typeof arguments[3] === 'function') {
error("State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().');
}
}
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
//首屏更新走这里
console.log(currentlyRenderingFiber$1===null)
console.log(fiber.alternate===null)//true
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
enqueueUpdate$1(fiber, queue, update);
var alternate = fiber.alternate;
//是否是首次更新判断(mount之后还未进入update)
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
var lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
var prevDispatcher;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
//在这一步我们可以看到传入的值是已经改变的的
//当前传入state(保存在链中)
var currentState = queue.lastRenderedState;//第一次 [0,1,2,3]
//state计算数据
var eagerState = lastRenderedReducer(currentState, action); //第一次 [0,1,2,3]
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
//判断newState与oldState做比较,第一次点击在这里终止
if (objectIs(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// console.log(222222,queue)
return;
}
} catch (error) {// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
var eventTime = requestEventTime();
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
console.log('root',root)
if (root !== null) {
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane);
}
我们通过调试可以看到因为已经经过首屏更新所以走的是else内的部分,最终在else内进行当前值与计算值比较因为是同一个引用类型对象所以返回的是true
//判断newState与oldState做比较,第一次点击在这里终止
if (objectIs(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// console.log(222222,queue)
return;
}
数据比较
function is(x, y) {
return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y // eslint-disable-line no-self-compare
;
}
var objectIs = typeof Object.is === 'function' ? Object.is : is;
最终mount阶段在dispatchSetState方法中就被拦截了,那么在update阶段又会怎么样呢?带着好奇我改写了一下demo
updateState
function App() {
const [list,setList] = useState([0,1,2])
//
const handleClick = ()=>{
list.push(3)
setList(list)
}
const handleClick2 = ()=>{
setList([...list,list.length])
}
return (
{list.map(item=>{item})}
);
}
我们先点击click2使其进入update状态,然后再点击click1,你会发现它进入了beginWork方法因为是Function组件,所以会在updateFunctionComponent 中执行,但是这这一步它停止了;原因是它在这里判断进入了bailoutOnAlreadyFinishedWork
//在这里进入bailoutOnAlreadyFinishedWork
//bailoutOnAlreadyFinishedWork 判断节点是否可复用
//当前为update阶段所以current不可能为空
//!didReceiveUpdate代表为update阶段
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
console.log('bailoutOnAlreadyFinishedWork')
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
然后再让我们看看bailoutOnAlreadyFinishedWork 方法
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
if (current !== null) {
// Reuse previous dependencies
workInProgress.dependencies = current.dependencies;
}
{
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning();
}
markSkippedUpdateLanes(workInProgress.lanes); // Check if the children have any pending work.
console.log(renderLanes, workInProgress.childLanes)
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
console.log("stop")
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
{
return null;
}
} // This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
最终本次render阶段会在这里被强制中断
//判断子节点有无需要进行的任务操作
//在这里停止原因是workInProgress.childLanes为0导致等式成立
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
console.log("stop")
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
{
return null;
}
} // This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
总结
不管是在mountState阶段可变数据会在dispatchSetState时就会因为数据比对而中断,因此进入不到beginWork,在updateState阶段,可变数据会进入beginWork并根据Fiber的tag类型判断进入的是updateFunctionComponent还是updateClassComponent但是最终都会在bailoutOnAlreadyFinishedWork函数中因为childLanes为0的缘故终止执行;也就是说在mountState阶段不会进入render阶段,但是在updateState阶段会进入render阶段并创建fiber,但是会被中断执行