React Hooks 源碼模擬與解讀

useState 解析

useState 使用

一般咱們這樣來使用 useState 方法javascript

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

useState 的使用過程,咱們先模擬一個大概的函數html

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

這個代碼有一個問題,在執行 useState 的時候每次都會 var _val = initialValue,初始化數據;java

因而咱們能夠用閉包的形式來保存狀態。react

const MyReact = (function() {
   // 定義一個 value 保存在該模塊的全局中
  let value
  return {
    useState(initialValue) {
      value = value || initialValue 
      function setState(newVal) {
        value = newVal
      }
      return [value, setState]
    }
  }
})()
複製代碼

這樣在每次執行的時候,就可以經過閉包的形式 來保存 value。ios

不過這個仍是不符合 react 中的 useState。由於在實際操做中會出現屢次調用,以下。axios

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 儲存狀態的方式api

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 不要再循環,條件或嵌套函數中使用。閉包

爲何不能夠?

咱們來看下dom

下面這樣一段代碼。執行 useState 從新渲染,和初始化渲染 順序不同就會出現以下問題

若是瞭解了上面 useState 模擬寫法的存儲方式,那麼這個問題的緣由就迎刃而解了。

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,提升效率。

最後

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

參考:

相關文章
相關標籤/搜索