Stories

Detail Return Return

一個關於React數據不可變的無聊問題 - Stories Detail

對於一個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,但是會被中斷執行

Add a new Comments

Some HTML is okay.