一个关于React数据不变性的无聊问题
对于一个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}