「前端發動機」深刻 React hooks — 原理 & 實現

前言

React Hooks的基本用法,官方文檔 已經很是詳細。這是系列的第三篇,探討一下 hooks 的實現機制。javascript

useState

前兩篇文章已經分析過 useState 和 useEffect 的執行機制,想要更加深刻的瞭解 hooks,能夠根據 hooks 的相關特色,本身模擬實現一下相關的函數,也能夠直接去看 源碼。不瞭解 hook 一些基本特性的同窗能夠去看文檔,或者個人前兩篇文章。html

useState 是 hooks 方案能夠實現的基礎,它讓咱們能夠在不依賴 class 的狀況下,使組件能夠有本身的狀態。它的使用很簡單:前端

const [count, setCount] = useState(0);
複製代碼

而咱們時刻要記住兩點:一、useState 就是一個函數;二、每次更新其實都是觸發了它的重執行(以前的文章說過,由於整個函數組件都重執行了)。java

簡單點就是下面這樣:react

function useState(initialState) {
      const state = initialState;
      const setState = newState => state = newState;
      return [state, setState];
}
複製代碼

固然如今是不完整的,由於它根本就不能更新數據。即使咱們 setState 的時候,觸發了組件更新,再到 useState 更新,最後也只是又一次初始化拿到的 initialState。由於每次 useState 的 state 都是局部變量,更新就等於從新建立了一次,上一次的值始終沒法拿到。因此,咱們須要把 state 保存起來。git

最簡單的就是全局變量了,固然 React 並非這麼實現的。對於 hooks,React 採用的是鏈表,感興趣的能夠本身去看看,這裏只是模擬實現一下。github

const HOOKS = []; // 全局的存儲 hook 的變量
let currentIndex = 0; // 全局的 依賴 hook 執行順序的下標
function useState(initialState) {
   HOOKS[currentIndex] = HOOKS[currentIndex] || initialState; // 判斷一下是否須要初始化
   const memoryCurrentIndex = currentIndex; // currentIndex 是全局可變的,須要保存本次的
   const setState = newState => HOOKS[memoryCurrentIndex] = newState;
   return [HOOKS[currentIndex++], setState]; // 爲了屢次調用 hook,每次執行 index 須要 +1
}
複製代碼

咱們將全部的 hooks,都保存在一個全局的數組變量中,每次更新時去找對應的下標就行了。這也是爲何要保證 hooks 的執行順序在更新先後一致的緣由。試想一下,第一次運行執行了四個 hooks,下標增長到了 3;而更新的時候由於 if 判斷等緣由,本來的第三個 hook 沒有執行,那第四個 hook 取到的值是否是就錯了?。數組

咱們再擴展一下,useState 初始化是能夠傳函數的,setState 也是同樣,因此能夠稍微加點判斷。微信

function useState(initialState) {
   HOOKS[currentIndex] = HOOKS[currentIndex]
       || (typeof initialState === 'function' ? initialState() : initialState);
   const memoryCurrentIndex = currentIndex; // currentIndex 是全局可變的,須要保存本次的
   const setState = p => {
       let newState = p;
       // setCount(count => count + 1) 判斷這種用法
       if (typeof p === 'function') newState = p(HOOKS[memoryCurrentIndex]);
       // 若是設置先後的值同樣,就不更新了
       if (newState === HOOKS[memoryCurrentIndex]) return;
       HOOKS[memoryCurrentIndex] = newState;
   };
   return [HOOKS[currentIndex++], setState];
}
複製代碼

Tick

固然,如今還只是簡單地更新了對應的 state,並無通知 render 更新函數,咱們能夠寫個簡單的工具。dom

const HOOKS = []; // 全局的存儲 hook 的變量
let currentIndex = 0; // 全局的 依賴 hook 執行順序的下標
const Tick = {
 render: null,
 queue: [],
 push: function(task) {
     this.queue.push(task);
 },
 nextTick: function(update) {
     this.push(update);
     Promise.resolve(() => {
         if (this.queue.length) { // 一次循環後,所有出棧,確保單次事件循環不會重複渲染
             this.queue.forEach(f => f()); // 依次執行隊列中全部任務
             currentIndex = 0; // 重置計數
             this.queue = []; // 清空隊列
             this.render && this.render(); // 更新dom
         }
     }).then(f => f());
 }
};
複製代碼

在 React 中,setState 的更新雖然是同步的,可是咱們感知不到,至少看起來它異步了。這是由於 React 本身實現了一套事務管理。能力有限,這裏就用 Promise 來替代一下,相似於 Vue 中的 nextTick。

const setState = p => {
   // ···
   Tick.nextTick(() => {
       HOOKS[memoryCurrentIndex] = newState;
   });
 };
複製代碼

useEffect

useEffect 充當了 class 中的生命週期的角色,咱們更多地用來通知更新,清理反作用。有了對 useState 的瞭解,大體原理是同樣的。只不過多了一個依賴數組,若是依賴數組中的每一項都和上一次的同樣,就不須要更新,反之更新。

function useEffect(fn, deps) {
   const hook = HOOKS[currentIndex];
   const _deps = hook && hook._deps;
   // 判斷是否傳了依賴,沒傳默認每次更新
   // 判斷本次依賴和上次的是否所有同樣
   const hasChange = _deps ? !deps.every((v, i) => _deps[i] === v) : true;
   const memoryCurrentIndex = currentIndex; // currentIndex 是全局可變的
   if (hasChange) {
       const _effect = hook && hook._effect;
       setTimeout(() => {
           // 每次先判斷一下有沒有上一次的反作用須要卸載
           typeof _effect === 'function' && _effect();
           // 執行本次的
           const ef = fn();
           // 更新effects
           HOOKS[memoryCurrentIndex] = {...HOOKS[memoryCurrentIndex], _effect: ef};
       })
   }
   // 更新依賴
   HOOKS[currentIndex++] = {_deps: deps, _effect: null};
}
複製代碼

能夠看到 useEffect 在 HOOKS 中保存了兩個值,一個是依賴,一個是反作用,保存的時機和它的執行順序一致。由於 useEffect 須要在 dom 掛載後再執行,因此用了 setTimeout 簡單模擬,React 中不是這樣。

useReducer

useReducer 很好理解,能夠看作是對 useState 作了一層包裝,讓咱們能夠更直觀的去管理狀態。先回憶一下它的使用方法:

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return {total: state.total + 1};
        case 'decrement':
            return {total: state.total - 1};
        default:
            throw new Error();
    }
}
const [state, dispatch] = useReducer(reducer, { count: 0});
// state.count ...
複製代碼

也就是,咱們其實只須要結合 useState 和 reducer 就行了。

function useReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);
    const update = (state, action) => {
      const result = reducer(state, action);
      setState(result);
    }
    const dispatch = update.bind(null, state);
    return [state, dispatch];
}
複製代碼

useMemo & useCallback

這兩個都用經過依賴用來優化提升 React 性能的,針對處理一些消耗大的計算。useMemo 會返回一個值,而 useCallback 會返回一個函數。

useMemo

function useMemo(fn, deps) {
    const hook = HOOKS[currentIndex];
    const _deps = hook && hook._deps;
    const hasChange = _deps ? !deps.every((v, i) => _deps[i] === v) : true;
    const memo = hasChange ? fn() : hook.memo;
    HOOKS[currentIndex++] = {_deps: deps, memo};
    return memo;
}
複製代碼

useCallback

function useCallback(fn, deps) {
    return useMemo(() => fn, deps);
}
複製代碼

小結

此次簡單的模擬實現了部分的 React hook,你們有興趣的也能夠本身去完善,若是發現有問題的地方,能夠在評論指出,我會及時更新。

  • 完整代碼在這裏 hook.js
  • 使用方法以下:
function render() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        const time = setInterval(() => {
            setCount(count => count + 1);
        }, 1000)
        // 清除反作用
        return () => {
            clearInterval(time);
        }
    }, [count]);
    document.querySelector('.add').onclick = () => {
        setCount(count + 1);
    };
    document.querySelector('#count').innerHTML = count;
}
// 綁定 render
Tick.render = render;
render();
複製代碼

參考文章

交流羣

微信羣:掃碼回覆加羣。

mmqrcode1566432627920.png

後記

若是你看到了這裏,且本文對你有一點幫助的話,但願你能夠動動小手支持一下做者,感謝🍻。文中若有不對之處,也歡迎你們指出,共勉。好了,又耽誤你們的時間了,感謝閱讀,下次再見!

感興趣的同窗能夠關注下個人公衆號 前端發動機,好玩又有料。

相關文章
相關標籤/搜索