当前位置: 首页 > Web前端 > JavaScript

一个关于React数据不变性的无聊问题

时间:2023-03-27 01:47:37 JavaScript

对于一个React开发者,不知道你有没有想过,为什么React追求不可变数据的范式;一个月前,我想到了一个问题,如果我在使用useState钩子时传入一个更改的引用,类型对象会发生什么情况?例如:import{useState}from"react"functionApp(){const[list,setList]=useState([0,1,2])consthandleClick=()=>{list.push(list.length)setList(list)}return(clickme--conventionality{list.map(item=>{item}

)}
);}导出默认应用;那么当我们点击按钮时会发生什么?答案是我们的视觉没有任何反应!列表数据一直是012;这个结果相信99%的react开发者都可以期待!也可以肯定,80%以上的人会说,因为你的新数据和旧数据一样(newState===oldState)===true,所以这道题的答案确实是这道。那么newState和oldState的对比在哪里,拦截又在哪里呢?之前想的是在render阶段的update阶段在reconcileChildFibers中打上effectTag标记再做判断。但是,今天给beginWork的时候,发现上面的代码根本就到不了beginWork(挂载阶段)。出于好奇,我决定从源码开始探究(答案可能有点无聊);我们知道useStatehooks生成const[list,setList]=useState([0,1,2])是dispatchAction方法和useSta的mountState阶段te分为mountState和updateState两种,因为setList是在mount时创建的,所以我们先看看它是如何创建的if(typeofinitialState==='function'){//$FlowFixMe:Flow不喜欢混合类型initialState=initialState();}hook.memoizedState=hook.baseState=initialState;varqueue={pending:null,interleaved:null,lanes:NoLanes,dispatch:null,lastRenderedReducer:basicStateReducer,lastRenderedState:initialState};hook.queue=队列;//创建dispatch方法并保存在链中//dispatch由dispatchSetState方法创建vardispatch=queue.dispatch=dispatchSetState.bind(null,currentlyRenderingFiber$1,queue);//这一步返回链表中的list和setList/这里进入控制台,可以正常输出,程序可以进行到这一步console.log('dispatchSetState',fiber,queue,action){if(typeofarguments[3]==='function'){error("来自useState()和useReducer()Hooks的状态更新不支持"+'第二个回调参数。要在'+'渲染后执行副作用,使用useEffect()在组件主体中声明它。');}}varlane=requestUpdateLane(fiber);varupdate={lane:lane,action:action,hasEagerState:false,eagerState:null,next:null};//首页更新走到这里}else{enqueueUpdate$1(fiber,queue,update);varalternate=fiber.alternate;//是否是第一次更新判定(mount之后还未进入update)if(fiber.lanes===NoLanes&&(alternate===null||alternate.lanes===NoLanes)){//队列当前为空,这意味着我们可以在进入渲染阶段之前急切地计算//下一个状态。如果新的state与当前状态//相同,我们可以完全摆脱困境。varlastRenderedReducer=queue.lastRenderedReducer;if(lastRenderedReducer!==null){varprevDispatcher;{prevDispatcher=ReactCurrentDispatcher$1.current;ReactCurrentDispatcher$1。当前=InvalidNestedHooksDispatcherOnUpdateInDEV;}try{//这一步我们可以看到传入的值发生了变化//当前传入的状态(保存在链中)varcurrentState=queue.lastRenderedState;//第一次[0,1,2,3]//状态计算数据vareagerState=lastRenderedReducer(currentState,action);//firsttime[0,1,2,3]//将急切计算的状态和用于计算//它的缩减器存储在更新对象上。如果在我们进入渲染阶段时//reducer没有更改,则可以使用eager状态//而无需再次调用reducer。update.hasEagerState=true;向上date.eagerState=eagerState;//判断newState和oldState进行比较,第一次点击在这里终止if(objectIs(eagerState,currentState)){//快速路径。我们可以在不安排React重新渲染的情况下摆脱困境。//如果组件出于不同的原因重新渲染并且到那时reducer已更改,我们仍然有可能需要稍后对此更新进行变基。//console.log(222222,queue)返回;}}catch(error){//抑制错误。它会在渲染阶段再次抛出。}最后{{ReactCurrentDispatcher$1.current=prevDispatcher;}}}}vareventTime=requestEventTime();varroot=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)){//快速路径。我们可以在不安排React重新渲染的情况下摆脱困境。//稍后我们仍然有可能需要重新设置此更新的基础,//如果组件出于不同的原因重新呈现,并且到那时//reducer已更改。//console.log(222222,queue)return;}数据比较函数is(x,y){returnx===y&&(x!==0||1/x===1/y)||x!==x&&y!==y//eslint-disable-lineno-self-compare;}varobjectIs=typeofObject.is==='function'?对象是:是;最后的mount阶段在dispatchSetState方法中被拦截了,那么update阶段会发生什么呢?出于好奇,我重写了demoupdateStatefunctionApp(){const[list,setList]=useState([0,1,2])//consthandleClick=()=>{list.push(3)setList(list)}consthandleClick2=()=>{setList([...list,list.length])}return(点击1点击2{list.map(item=>{item}
)}
);}我们先点击click2进入update状态,再点击click1,你会发现进入了beginWork方法,因为是Function组件,所以会在updateFunctionComponent中执行,但是这一步它停止了;原因是这里判断进入bailoutOnAlreadyFinishedWork//这里进入bailoutOnAlreadyFinishedWork//bailoutOnAlreadyFinishedWork判断节点是否可以重用//当前处于更新阶段,所以current不能为空//!didReceiveUpdate代表更新阶段if(current!==null&&!didReceiveUpdate){bailoutHooks(current,workInProgress,renderLanes);console.log('bailoutOnAlreadyFinishedWork')returnbailoutOnAlreadyFinishedWork(current,workInProgress,renderLanes);}然后我们看bailoutOnAlreadyFinishedWork方法Onfunctionbailout(AlreadyFinishedWorkworkInProgress,renderLanes){if(current!==null){//重用之前的依赖workInProgress.依赖项=当前。依赖关系;}{//不要更新救助的“基本”渲染时间。stopProfilerTimerIfRunning();}markSkippedUpdateLanes(workInProgress.lanes);//检查孩子们是否有任何待处理的工作。console.log(renderLanes,workInProgress.childLanes)if(!includesSomeLane(renderLanes,workInProgress.childLanes)){console.log("stop")//孩子们也没有任何工作。我们可以跳过它们。//TODO:一旦我们添加回恢复,我们应该检查孩子是否是//正在进行的工作集。如果是这样,我们需要转移它们的影响。{返回空值;}}//这个fiber没有工作,但是它的子树有。克隆子//fibers并继续。最后这个render阶段会在这里被强行中断//判断子节点是否有task操作要执行//这里停止的原因是workInProgress.childLanes为0,等式成立if(!includesSomeLane(renderLanes,workInProgress.childLanes)){console.log("stop")//孩子们也没有任何工作。我们可以跳过它们。//TODO:一旦我们添加回简历ing,我们应该检查孩子是否是//正在进行的工作集。如果是这样,我们需要转移它们的影响。{返回空值;}}//这个fiber没有工作,但是它的子树有。克隆子//纤维并继续。总结无论是在mountState阶段,变量数据在dispatchSetState时都会因为数据比较而中断,所以不会进入beginWork。在updateState阶段,变量数据会进入beginWork,根据Fiber标签类型判断是进入updateFunctionComponent还是updateClassComponent,但最终会在bailoutOnAlreadyFinishedWork函数中终止执行,因为childLanes为0;也就是说在mountState阶段不会进入render阶段,而在updateState阶段会进入render阶段并创建fiber。但会被打断