React Hooks 原始碼模擬與解讀

叫我小明呀發表於2019-08-24

useState 解析

useState 使用

通常我們這樣來使用 useState 方法

function App() {
  const [num, setNum] = useState(0);
  const add = () => {
    setNum(num + 1);
  };
  return (
    <div>
      <p>數字: {num}</p>
      <button onClick={add}> +1 </button>
    </div>
  );
}
複製程式碼

useState 的使用過程,我們先模擬一個大概的函式

function useState(initialValue) {
  var value = initialValue
  function setState(newVal) {	
    value = newVal
  }
  return [value, setState]
}
複製程式碼

這個程式碼有一個問題,在執行 useState 的時候每次都會 var _val = initialValue,初始化資料;

於是我們可以用閉包的形式來儲存狀態。

const MyReact = (function() {
   // 定義一個 value 儲存在該模組的全域性中
  let value
  return {
    useState(initialValue) {
      value = value || initialValue 
      function setState(newVal) {
        value = newVal
      }
      return [value, setState]
    }
  }
})()
複製程式碼

這樣在每次執行的時候,就能夠通過閉包的形式 來儲存 value。

不過這個還是不符合 react 中的 useState。因為在實際操作中會出現多次呼叫,如下。

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setNum('Dom');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
 	  <p>年齡: {age}</p>
      <button onClick={handleAge}> 加一歲 </button>
    </div>
  );
}
複製程式碼

因此我們需要在改變 useState 儲存狀態的方式

useState 模擬實現

const MyReact = (function() {
  // 開闢一個儲存 hooks 的空間
  let hooks = []; 
  // 指標從 0 開始
  let currentHook = 0 
  return {
    // 虛擬碼 解釋重新渲染的時候 會初始化 currentHook
    render(Component) {
      const Comp = Component()
      Comp.render()
      currentHook = 0 // 重新渲染時候改變 hooks 指標
      return Comp
    },      
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // 這裡我們暫且預設 setState 方式第一個引數不傳 函式,直接傳狀態
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
  }
})()
複製程式碼

因此當重新渲染 App 的時候,再次執行 useState 的時候傳入的引數 kevin , 0 也就不會去使用,而是直接拿之前 hooks 儲存好的值。

hooks 規則

官網 hoos 規則中明確的提出 hooks 不要再迴圈,條件或巢狀函式中使用。

React Hooks 原始碼模擬與解讀

為什麼不可以?

我們來看下

下面這樣一段程式碼。執行 useState 重新渲染,和初始化渲染 順序不一樣就會出現如下問題

如果瞭解了上面 useState 模擬寫法的儲存方式,那麼這個問題的原因就迎刃而解了。

React Hooks 原始碼模擬與解讀
React Hooks 原始碼模擬與解讀

useEffect 解析

useEffect 使用

初始化會 列印一次 ‘useEffect_execute’, 改變年齡重新render,會再列印, 改變名字重新 render, 不會列印。因為依賴陣列裡面就監聽了 age 的值

import React, { useState, useEffect } from 'react';

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setName('Don');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  useEffect(()=>{
    console.log('useEffect_execute')
  }, [age])
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
      <p>年齡: {age}</p>
      <button onClick={handleAge}> 加一歲 </button>
    </div>
  );
}
export default App;

複製程式碼

useEffect 的模擬實現

const MyReact = (function() {
  // 開闢一個儲存 hooks 的空間
  let hooks = []; 
  // 指標從 0 開始
  let currentHook = 0// 定義個模組全域性的 useEffect 依賴
  let deps;
  return {
    // 虛擬碼 解釋重新渲染的時候 會初始化 currentHook
    render(Component) {
      const Comp = Component()
      Comp.render()
      currentHook = 0 // 重新渲染時候改變 hooks 指標
      return Comp
    },      
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // 這裡我們暫且預設 setState 方式第一個引數不傳 函式,直接傳狀態
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      // 如果沒有依賴,說明是第一次渲染,或者是沒有傳入依賴引數,那麼就 為 true
      // 有依賴 使用 every 遍歷依賴的狀態是否變化, 變化就會 true
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      // 如果有 依賴, 並且依賴改變
      if (hasNoDeps || hasChangedDeps) {
        // 執行 
        callback()
        // 更新依賴
        deps = depArray
      }
    },
        
  }
})()
複製程式碼

useEffect 注意事項

依賴項要真實

依賴需要想清楚。

剛開始使用 useEffect 的時候,我只有想重新觸發 useEffect 的時候才會去設定依賴

那麼也就會出現如下的問題。

希望的效果是介面中一秒增加一歲

import React, { useState, useEffect } from 'react';

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setName('Don');
  };
  const handleAge = () => {
    setAge(age + 1);
  };
  useEffect(() => {
    setInterval(() => {
      setAge(age + 1);
      console.log(age)
    }, 1000);
  }, []);
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
      <p>年齡: {age}</p>
      <button onClick={handleAge}> 加一歲 </button>
    </div>
  );
}
export default App;

複製程式碼

其實你會發現 這裡介面就增加了 一次 年齡。究其原因:

**在第一次渲染中,age0。因此,setAge(age+ 1)在第一次渲染中等價於setAge(0 + 1)。然而我設定了0依賴為空陣列,那麼之後的 useEffect 不會再重新執行,它後面每一秒都會呼叫setAge(0 + 1) **

也就是當我們需要 依賴 age 的時候我們 就必須再 依賴陣列中去記錄他的依賴。這樣useEffect 才會正常的給我們去執行。

所以我們想要每秒都遞增的話有兩種方法

方法一:

真真切切的把你所依賴的狀態填寫到 陣列中

  // 通過監聽 age 的變化。來重新執行 useEffect 內的函式
  // 因此這裡也就需要記錄定時器,當解除安裝的時候我們去清空定時器,防止多個定時器重新觸發
  useEffect(() => {
    const id = setInterval(() => {
      setAge(age + 1);
    }, 1000);
    return () => {
      clearInterval(id)
    };
  }, [age]);

複製程式碼

方法二

useState 的引數傳入 一個方法。

注:上面我們模擬的 useState 並沒有做這個處理 後面我會講解原始碼中去解析。

useEffect(() => {
    setInterval(() => {
      setAge(age => age + 1);
    }, 1000);
  }, []);
複製程式碼

useEffect 只執行了一次,通過 useState 傳入函式的方式它不再需要知道當前的age值。因為 React render 的時候它會幫我們處理

這正是setAge(age => age + 1)做的事情。再重新渲染的時候他會幫我們執行這個方法,並且傳入最新的狀態。

所以我們做到了去時刻改變狀態,但是依賴中卻不用寫這個依賴,因為我們將原本的使用到的依賴移除了。(這句話表達感覺不到位)

介面無限請求問題

剛開始使用 useEffect 的我,在介面請求的時候常常會這樣去寫程式碼。

props 裡面有 頁碼,通過切換頁碼,希望監聽頁碼的變化來重新去請求資料

// 以下是虛擬碼 
// 這裡用 dva 傳送請求來模擬

import React, { useState, useEffect } from 'react';
import { connect } from 'dva';

function App(props) {
  const { goods, dispatch, page } = props;
  useEffect(() => {
    // 頁面完成去發情請求
   dispatch({
      type: '/goods/list',
      payload: {page, pageSize:10},
    });
    // xxxx 
  }, [props]);
  return (
    <div>
      <p>商品: {goods}</p>
	 <button>點選切下一頁</button>
    </div>
  );
}
export default connect(({ goods }) => ({
  goods,
}))(App);
複製程式碼

然後得意洋洋的重新整理介面,發現 Network 中瘋狂迴圈的請求介面,導致頁面的卡死。

究其原因是因為在依賴中,我們通過介面改變了狀態 props 的更新, 導致重新渲染元件,導致會重新執行 useEffect 裡面的方法,方法執行完成之後 props 的更新, 導致重新渲染元件,依賴專案是物件,引用型別發現不相等,又去執行 useEffect 裡面的方法,又重新渲染,然後又對比,又不相等, 又執行。因此產生了無限迴圈。

Hooks 原始碼解析

該原始碼位置: react/packages/react-reconciler/src/ReactFiberHooks.js

const Dispatcher={
  useReducer: mountReducer,
  useState: mountState,
  // xxx 省略其他的方法
}
複製程式碼

mountState 原始碼

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
    /*
    mountWorkInProgressHook 方法 返回初始化物件
    {
        memoizedState: null,
        baseState: null, 
        queue: null,
        baseUpdate: null,
        next: null,
  	}
    */
  const hook = mountWorkInProgressHook();
 // 如果傳入的是函式 直接執行,所以第一次這個引數是 undefined
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });

	/*
	定義 dispatch 相當於
	const dispatch = queue.dispatch =
	dispatchAction.bind(null,currentlyRenderingFiber,queue);
	*/ 
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));

 // 可以看到這個dispatch就是dispatchAction繫結了對應的 currentlyRenderingFiber 和 queue。最後return:
  return [hook.memoizedState, dispatch];
}
複製程式碼

dispatchAction 原始碼

function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A) {
  //... 省略驗證的程式碼
  const alternate = fiber.alternate;
    /*
    這其實就是判斷這個更新是否是在渲染過程中產生的,currentlyRenderingFiber只有在FunctionalComponent更新的過程中才會被設定,在離開更新的時候設定為null,所以只要存在並更產生更新的Fiber相等,說明這個更新是在當前渲染中產生的,則這是一次reRender。
所有更新過程中產生的更新記錄在renderPhaseUpdates這個Map上,以每個Hook的queue為key。
對於不是更新過程中產生的更新,則直接在queue上執行操作就行了,注意在最後會發起一次scheduleWork的排程。
    */
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    didScheduleRenderPhaseUpdate = true;
    const update: Update<A> = {
      expirationTime: renderExpirationTime,
      action,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);
    const update: Update<A> = {
      expirationTime,
      action,
      next: null,
    };
    flushPassiveEffects();
    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;
    scheduleWork(fiber, expirationTime);
  }
}
複製程式碼

mountReducer 原始碼

多勒第三個引數,是函式執行,預設初始狀態 undefined

其他的和 上面的 mountState 大同小異

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
	// 其他和 useState 一樣
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
複製程式碼

通過 react 原始碼中,可以看出 useState 是特殊的 useReducer

  • 可見useState不過就是個語法糖,本質其實就是useReducer
  • updateState 複用了 updateReducer(區別只是 updateState 將 reducer 設定為 updateReducer)
  • mountState 雖沒直接呼叫 mountReducer,但是幾乎大同小異(區別只是 mountState 將 reducer 設定為basicStateReducer)

注:這裡僅是 react 原始碼,至於重新渲染這塊 react-dom 還沒有去深入瞭解。

更新:

分兩種情況,是否是 reRender,所謂reRender就是說在當前更新週期中又產生了新的更新,就繼續執行這些更新知道當前渲染週期中沒有更新為止

他們基本的操作是一致的,就是根據 reducerupdate.action 來建立新的 state,並賦值給Hook.memoizedState 以及 Hook.baseState

注意這裡,對於非reRender得情況,我們會對每個更新判斷其優先順序,如果不是當前整體更新優先順序內得更新會跳過,第一個跳過得Update會變成新的baseUpdate他記錄了在之後所有得Update,即便是優先順序比他高得,因為在他被執行得時候,需要保證後續的更新要在他更新之後的基礎上再次執行,因為結果可能會不一樣。

來源

preact 中的 hooks

Preact 最優質的開源 React 替代品!(輕量級 3kb)

注意:這裡的替代是指如果不用 react 的話,可以使用這個。而不是取代。

useState 原始碼解析

呼叫了 useReducer 原始碼


export function useState(initialState) {
	return useReducer(invokeOrReturn, initialState);
}
複製程式碼

useReducer 原始碼解析

// 模組全域性定義
/** @type {number} */
let currentIndex; // 狀態的索引,也就是前面模擬實現 useState 時候所說的指標

let currentComponent; // 當前的元件

export function useReducer(reducer, initialState, init) {
	/** @type {import('./internal').ReducerHookState} */
    // 通過 getHookState 方法來獲取 hooks 
	const hookState = getHookState(currentIndex++);

	// 如果沒有元件 也就是初始渲染
	if (!hookState._component) {
		hookState._component = currentComponent;
		hookState._value = [
			// 沒有 init 執行 invokeOrReturn
				// invokeOrReturn 方法判斷 initialState 是否是函式
				// 是函式 initialState(null) 因為初始化沒有值預設為null
				// 不是函式 直接返回 initialState
			!init ? invokeOrReturn(null, initialState) : init(initialState),

			action => {
				// reducer == invokeOrReturn
				const nextValue = reducer(hookState._value[0], action);
				// 如果當前的值,不等於 下一個值
				// 也就是更新的狀態的值,不等於之前的狀態的值
				if (hookState._value[0]!==nextValue) {
					// 儲存最新的狀態
					hookState._value[0] = nextValue;
					// 渲染元件
					hookState._component.setState({});
				}
			}
		];
	}
    // hookState._value 資料格式也就是 [satea:any, action:Function] 的資料格式拉
	return hookState._value;
}

複製程式碼

getHookState 方法

function getHookState(index) {
	if (options._hook) options._hook(currentComponent);
	const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [], _pendingLayoutEffects: [] });

	if (index >= hooks._list.length) {
		hooks._list.push({});
	}
	return hooks._list[index];
}
複製程式碼

invokeOrReturn 方法

function invokeOrReturn(arg, f) {
	return typeof f === 'function' ? f(arg) : f;
}
複製程式碼

總結

使用 hooks 幾個月了。基本上所有類元件我都使用函式式元件來寫。現在 react 社群的很多元件,都也開始支援hooks。大概瞭解了點重要的原始碼,做到知其然也知其所以然,那麼在實際工作中使用他可以減少不必要的 bug,提高效率。

最後

全文章,如有錯誤或不嚴謹的地方,請務必給予指正,謝謝!

參考:

相關文章