乐趣区

关于javascript:一个关于React数据不可变的无聊问题

对于一个 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 分为两种 mountStateupdateState,因为 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 并依据 Fibertag类型判断进入的是 updateFunctionComponent 还是 updateClassComponent 然而最终都会在 bailoutOnAlreadyFinishedWork 函数中因为 childLanes 为 0 的缘故终止执行;也就是说在 mountState 阶段不会进入 render 阶段,然而在 updateState 阶段会进入 render 阶段并创立fiber,然而会被中断执行

退出移动版