对于一个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 ( <div className="App"> <button onClick={handleClick}>click me--conventionality</button> {list.map(item=><div key={item}>{item}</div>)} </div> );}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 ( <div className="App"> <button onClick={handleClick}>click 1</button> <button onClick={handleClick2}>click 2</button> {list.map(item=><div key={item}>{item}</div>)} </div> );}
咱们先点击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,然而会被中断执行