React Hook源碼解析(二)

本文首發於公衆號:符合預期的CoyPanjavascript

寫在前面

在上一篇文章中,java

juejin.im/post/5e7cc0…react

主要分析了Hook在React中是如何保存的,以及Hook的更新過程。本文中,咱們將經過下面兩個問題,繼續深刻研究Hook,以彌補上文中略過的一些細節。閉包

一、若是我連續屢次調用setState,Hook會怎麼處理呢?ide

二、Hook的useEffect 是如何工做的?函數

連續屢次setState

先看示例代碼:post

const App = () => {

    const [count, setCount] = useState(0);

    const handleClick = () => {
        setCount(count + 1);
        setCount(count + 2);
        setCount(count + 3);
    };

    return <React.Fragment> <span style={{ marginRight: '10px' }}>{count}</span> <button onClick={handleClick}>點擊</button> </React.Fragment> }; 複製代碼

咱們點擊一次button,最終頁面上會輸出多少呢?熟悉React的朋友們,很快就會獲得答案:3ui

在上一篇源碼解析中,這部份內容被忽略了。本文咱們來看看這裏的內部邏輯。spa

首先,先複習一下hook的結構:指針

var hook = {
    memoizedState: null, // 當前的state值
    baseState: null, 
    queue: null, // 存儲更新信息
    baseUpdate: null,
    next: null // 指向下一個hook對象的指針
  };
複製代碼

咱們先看一下組件掛載完成後的hook,注意queue字段的值:

以前講過,調用setCount的時候,實際上調用的是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)這個函數。這個函數經過閉包保存了對應的Fiber和hook對象的queue的引用。

首次setCount的時候,hook對queue的處理以下:

// 更新信息
var _update2 = {
    expirationTime: expirationTime, // Fiber調度相關
    suspenseConfig: suspenseConfig,
    action: action, // setCount函數接受的參數
    eagerReducer: null,
    eagerState: null,
    next: null
};

var last = queue.last;

// 首次更新時
if (last === null) {
  // This is the first update. Create a circular list.
  // 第一次更新,構建一個 環
  _update2.next = _update2;
} else {
  // 後續更新
  var first = last.next;

  if (first !== null) {
    // Still circular.
    _update2.next = first;
  }
  last.next = _update2;
}
// queue的最近一次更新指向_update2
queue.last = _update2;
複製代碼

第一次setCount後,會構造一個環形結構:

第二次、第三次setCount時,會繼續構造queue鏈:

var first = last.next;

if (first !== null) {
  // Still circular.
  _update2.next = first;
}
last.next = _update2;
複製代碼

最終會造成下圖的結構:

組件從新渲染時,react會從hook的queue鏈中,找到最新的值,賦值給hook的memoizedState,咱們就能夠拿到最新的state了:

// 代碼有省略

	...
  // 循環,直到拿到queue鏈上最新的值
	do {
    var updateExpirationTime = _update.expirationTime;

    if (updateExpirationTime < renderExpirationTime$1) {
      ...
    } else {
      ...
      if (_update.eagerReducer === reducer) {
        ...
      } else {
        var _action = _update.action;
        _newState = reducer(_newState, _action);
      }
    }
    prevUpdate = _update;
    _update = _update.next;
  } while (_update !== null && _update !== first);
      
  hook.memoizedState = _newState; // 最新的state值,本例中爲3
  hook.baseUpdate = newBaseUpdate; // 最新的基礎更新信息,action=3
  hook.baseState = newBaseState; // 最新的基礎state值,本例中爲3
  queue.lastRenderedState = _newState; // 最近渲染的state值,本例中爲3
  return [hook.memoizedState, dispatch];
複製代碼

這裏有一個注意事項,在上一篇文章中,咱們提到過,setState中是支持傳入函數的。假設咱們在setState中傳入的參數是一個函數,在本例中,若是咱們點擊按鈕後的代碼改爲:

const handleClick = () => {
    setCount(count => count + 1);
    setCount(count => count + 2);
    setCount(count => count + 3);
 };
複製代碼

最終的count值就不是3了,而是6。這是由於傳入reducer的是最新的state:

... 
do {
  var action = update.action; // 這裏的action是咱們傳入的回調函數
  newState = reducer(newState, action); // newState 是最新的 state
  update = update.next; // 取hook對象queue鏈上的下一次更新
} while (update !== null); 
...
複製代碼

useEffect是如何工做的

首先上示例代碼:

const fakeReq = function(input) {
    return new Promise( resolve => {
        setTimeout(() => {
            resolve(`${input} - ${Date.now()}`);
        }, 500);
    });
}

const App = () => {
    const [input, setInput] = useState('');
    const [res, setRes] = useState('');

    useEffect(() => {
        fakeReq(input).then(res => {
            setRes(res);
        });
    },[input]);

    return <React.Fragment>
        <input value={input} onChange={e => setInput(e.target.value)} />
        <div>
            返回結果爲:<span>{res}</span>
        </div>
    </React.Fragment>
};
複製代碼

上面的代碼中,咱們在輸入框進入輸入的同時,會發起一個請求,而且將返回的結果顯示在頁面上。首先,咱們來看看React是怎麼保存useEffect的。

在代碼中,調用useEffect後,一樣會生成一個hook對象,只是這個hook對象的memoizedState字段不太同樣:

...
// fiberEffectTag 和 hookEffectTag 是兩個標識
// create、deps是咱們傳入useEffect的兩個參數
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  sideEffectTag |= fiberEffectTag;
  // useEffect生成的hook對象的memoizedState是一個特殊的對象
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
...
複製代碼

咱們來看看pushEffect幹了什麼:

function pushEffect(tag, create, destroy, deps) {
	
	// effect對象
  var effect = {
    tag: tag,
    create: create,
    destroy: destroy,
    deps: deps,
    // Circular
    next: null
  };

  // componentUpdateQueue是一個全局變量,用來保存組件的最新的反作用
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}
複製代碼

構造一個帶環的鏈:

在本例中,初始化完成後,最終Fiber對象的hook鏈爲:

當咱們在輸入框進行輸入時,來看看useEffect是如何起做用的。

輸入時,會觸發組件的從新渲染,假設咱們輸入了3,此時傳入useEffect的依賴變成了:

useEffect(() => {
    fakeReq(input).then(res => {
        setRes(res);
    });
},['3']);
複製代碼

useEffect的更新代碼爲:

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  // 當前處理的hook
  var hook = updateWorkInProgressHook();
  // 最新傳入的依賴
  var nextDeps = deps === undefined ? null : deps;
  var destroy = undefined;

  if (currentHook !== null) {
    // 上一次的effect
    var prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;

    if (nextDeps !== null) {
      var prevDeps = prevEffect.deps;
			// 對比兩次依賴是否相同。若是相同,則在componentUpdateQueue上增長一個 tag = NoEffect$1 的 effect。這裏的 NoEffect$1 是一個常量, 值爲 0。 這裏很重要
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(NoEffect$1, create, destroy, nextDeps);
        return;
      }
    }
  }
	
  sideEffectTag |= fiberEffectTag;
  // 若是兩次依賴不一樣,在 componentUpdateQueue 上增長一個 effect,而且更新hook的memorizedState
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
複製代碼

通過React的調度,會在 commitHookEffectList 這個函數中,判斷是否須要執行 useEffect 中傳入的函數:

function commitHookEffectList(unmountTag, mountTag, finishedWork) {

  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;

    do {
      if ((effect.tag & unmountTag) !== NoEffect$1) {
        // Unmount
        var destroy = effect.destroy;
        effect.destroy = undefined;

        if (destroy !== undefined) {
          destroy();
        }
      }
			
      if ((effect.tag & mountTag) !== NoEffect$1) {
        // Mount
        var create = effect.create;
        effect.destroy = create();

        {
          var _destroy = effect.destroy;

          if (_destroy !== undefined && typeof _destroy !== 'function') {
            var addendum = void 0;

            if (_destroy === null) {
              addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
            } else if (typeof _destroy.then === 'function') {
              ...
            } else {
              addendum = ' You returned: ' + _destroy;
            }
            ...
          }
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
複製代碼

NoEffect$1是一個等於0的全局常量,從上面代碼的do...while...部分能夠看到,當一個 effect 的 tag 爲 0時,和任何變量作與運算,值都爲0,不會進行任何操做。而上面的分析也提到了,useEffect的dep沒有變時,會聲明一個 tag = NoEffect$1 的effect。所以,useEffect的dep沒有變化時,useEffect的函數不會被執行。

咱們再來看看,react是怎麼比較兩次的deps是否相同的:

// useEffect中傳入的Deps是否相同
function areHookInputsEqual(nextDeps, prevDeps) {
 
	...
  for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // 這裏的 is$1 ,就是 Object.is 這個方法
    if (is$1(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}
複製代碼

總結

回到本文開頭的兩個問題:

一、若是我連續屢次調用setState,Hook會怎麼處理呢?

二、Hook的useEffect 是如何工做的?

對於每個hook,react會在hook對象的queue字段上,以有環鏈的形式,存儲更新信息。連續屢次更新,會沿着queue鏈計算出最新該hook最新的值。

使用useEffect,也會生成一個hook對象。只是該hook對象與useState生成的hook對象有區別。組件從新渲染時,會判斷傳入useEffect的dep依賴是否與上一次相同,相同的話,則會爲這次更新打上特殊的tag,保證不會執行useEffect中傳入的函數。

寫在後面

本文在前一篇文章的基礎上,進一步分析了hook中state的更新機制。另外,大體分析了useEffect是如何存儲,如何工做的。因爲本文不涉及react的調度更新過程,看起來不太連貫,請多包涵。關於react hook的更多解析,請關注我後續的文章。

相關文章
相關標籤/搜索