函數式編程看React Hooks(一)簡單React Hooks實現

函數式編程看React Hooks(一)簡單React Hooks實現

函數式編程看React Hooks(二)事件綁定副作用深度剖析

前言

函數式編程介紹(摘自基維百科)

函數式編程(英語:functional programming)或稱函數程序設計、泛函編程,是一種編程範式,它將計算機運算視爲函數運算,並且避免使用程序狀態以及易變對象。其中,λ演算(lambda calculus)爲該語言最重要的基礎。而且,λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。

面向對象編程介紹(摘自基維百科)

面向對象程序設計(英語:Object-oriented programming,縮寫:OOP)是種具有對象概念的程序編程典範,同時也是一種程序開發的抽象方針。它可能包含數據、屬性、代碼與方法。對象則指的是類的實例。它將對象作爲程序的基本單元,將程序和數據封裝其中,以提高軟件的重用性、靈活性和擴展性,對象裏的程序可以訪問及經常修改對象相關連的數據。在面向對象程序編程裏,計算機程序會被設計成彼此相關的對象

函數式強調在邏輯處理中不變性。面向對象通過消息傳遞改變每個Object的內部狀態。兩者是截然不同的編程思想,都具有自己的優勢,也因爲如此,才使得我們從 class 組件 轉化到 函數組件式,有一些費解。

從 react 的變化可以看出,react 走的道路越來越接近於函數式編程,輸入輸出一致性。當然也不是憑空地去往這個方面,而是爲了能夠解決更多的問題。以下 三點是 react 官網所提到的 hooks 的動機 https://zh-hans.reactjs.org/docs/hooks-intro.html#motivation

  • 代碼重用:在hooks出來之前,常見的代碼重用方式是 HOC 和render props,這兩種方式帶來的問題是:你需要解構自己的組件,同時會帶來很深的組件嵌套
  • 複雜的組件邏輯:在class組件中,有許多的lifecycle 函數,你需要在各個函數的裏面去做對應的事情。這種方式帶來的痛點是:邏輯分散在各處,開發者去維護這些代碼會分散自己的精力,理解代碼邏輯也很喫力
  • class組件的困惑:對於初學者來說,需要理解class組件裏面的this是比較喫力的,同時,基於class的組件難以優化。

本文是爲了給後面一篇文章作爲鋪墊,因爲在之後文章的講解過程中,你如果了理解了 React Hooks 的原理,再加上一步一步地講解,你可能會對 React Hooks 中各種情況會恍然大悟。

一開始的時候覺得 hooks 非常地神祕,寫慣了 class 式的組件後,我們的思維就會定格在那裏,生命週期,state,this等的使用。 因此會以 class 編寫的模式去寫函數式組件,導致我們一次又一次地爬坑,接下來我們就開始我們的實現方式講解。(提示:以下是都只是一種簡單的模擬方法,與實際有一些差別,但是核心思想是一致的)

開始

我們先寫一個簡單的 react 函數式組件。

function Counter(count) {
  return (
    <div>
      <div>{count}</div>
      <button>
        點擊
      </button>
    </div>
  );
}

在 React Hooks 還未出現的時候,我們的組件大多用來直接渲染,不含有狀態存儲,Function組件沒有state,所以也叫SFC(stateless functional component),現在更新叫做FC(functional component)。

爲了使得一個函數內有狀態,react 使用了一個特別的方法就是 hooks, 其實這是利用閉包實現的一個類似作用域的東西去存儲狀態,我第一想到的就是利用對象引用存儲數據,就像是面向對象一樣的方式,存在一個對象中中,通過引用的方式來進行獲取。但是 react 爲了能夠儘可能地分離狀態,精妙地採用了閉包。

讓我們看看他是如何實現的。(爲了儘可能簡化,我進行了改編)

useState

let _state;
function useState(initialState) {
	_state = _state || initialState; // 如果存在舊值則返回, 使得多次渲染後的依然能保持狀態。
  function setState(newState) {
    _state = newState;
    render();  // 重新渲染,將會重新執行 Counter
  }
  return [_state, setState];
}
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>
        點擊
      </button>
    </div>
  );
}

演示地址: https://codesandbox.io/s/dawn-bash-rqqoh

以上,不管 Counter 重新渲染多少次,通過閉包,依然能夠訪問到最新的 state,從而達到了存儲狀態的效果。

useEffect

再看看 useEffect, 先來看看使用方法。 useEffect(callback, dep?), 以下是一個非常簡單的使用例子。

  useEffect(() => {
      console.log(count);
  }, [count]);
function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(count);
  }, [count]);
  return (
    <div>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>
        點擊
      </button>
    </div>
  );
}

因爲函數式不像 class 那樣有複雜的生命週期,已經對 hooks 已經熟悉使用的你,可能會知道 useEffect 可以當做,componentdidmount 來使用。但是在這裏你直接將他按照順序執行。在 return 前他會執行。

let _deps = {
  args: []
}; // _deps 記錄 useEffect 上一次的 依賴
function useEffect(callback, args) {
  const hasChangedDeps = args.some((arg, index) => arg !== _deps.args[index]); // 兩次的 dependencies 是否完全相等
  // 如果 dependencies 不存在,或者 dependencies 有變化
  if (!_deps.args || hasChangedDeps) {
    callback();
    _deps.args = args;
  }
}

演示地址: https://codesandbox.io/s/ecstatic-glitter-w9kq7

至此,我們也實現了單個 useEffect。

useMemo

我們再來看看, useMemo,其實他也以上實現的方式一樣,也是通過閉包來進行存儲數據, 從而達到緩存提高性能的作用。

function Counter() {
  const [count, setCount] = useState(0);
  const computed = () => {
    console.log('我執行了');
    return count * 10 - 2;
  }
  const sum = useMemo(computed, [count]);
  return (
    <div>
      <div>{count} * 10 - 2 = {sum}</div>
      <button onClick={() => setCount(count + 1)}>
        點擊
      </button>
    </div>
  );
}

接下來我們來進行實現

let _deps = {
  args: []
}; // _deps 記錄 useMemo 上一次的 依賴
function useMemo(callback, args) {
  const hasChangedDeps = args.some((arg, index) => arg !== _deps.args[index]); // 兩次的 dependencies 是否完全相等
  // 如果 dependencies 不存在,或者 dependencies 有變化
  if (!_deps.args || hasChangedDeps) {
    _deps.args = args;
    _deps._callback = callback;
    _deps.value = callback();
    return _deps.value;
  }

  return _deps.value;
}

演示地址: https://codesandbox.io/s/festive-platform-v04xw

useCallback

那麼 useCallback 呢? 其實就是 useMemo 的一個包裝,畢竟你緩存函數的返回值,那麼我我讓返回值爲一個函數不就行了?

function useCallback(callback, args) {
	return useMemo(() => callback, args);
}

可以看到,以上我們也輕鬆地實現了 useMemo 。但是有一個問題,以上只是單個函數使用方式,所以接下來我們還需要處理一下多個函數的情況。

完整版

我們可以按照 preact 的方法來實現。即用數組來實現多個函數的處理邏輯。

核心邏輯就是

  • 第一次聲明的時候將 useState, useEffect, useMemo, useCallback 等鉤子函數的狀態依次存入數組。

  • 更新的時候,將前一次的函數狀態值依次取出。

也可以通過以下圖來理解

第一次渲染,將每個狀態都緩存到數組中。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Bl5mDlhK-1570871852164)(https://s3.qiufengh.com/blog/first-render.png)]

每次重新渲染,獲取數組中每個的緩存狀態。
re-render.png

以下爲了能夠清晰地讓大家明白原理,進行了一些刪減。但是核心邏輯不變。

let currentIndex = 0;
let currentComponent = {
  __hooks: []
};
function getHookState(index) {
  const hooks = currentComponent.__hooks;
  if (index >= hooks.length) {
    hooks.push({});
  }
  return hooks[index];
}

function argsChanged(oldArgs, newArgs) {
  return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}

function useState(initialState) {
  const hookState = getHookState(currentIndex++);
  hookState._value = [
    hookState._value ? hookState._value[0] : initialState,
    function setState(newState) {
      hookState._value[0] = newState;
      render(); // 重新渲染,將會重新執行 Counter
    }
  ];

  return hookState._value;
}

function useEffect(callback, args) {
  const state = getHookState(currentIndex++);
  if (argsChanged(state._args, args)) {
    callback();
    state._args = args;
    render();
  }
}

function useMemo(callback, args) {
  const state = getHookState(currentIndex++);
  if (argsChanged(state._args, args)) {
    state._args = args;
    state._callback = callback;
    state.value = callback();
    return state.value;
  }

  return state.value;
}

現在用以上 43 行代碼實現了一個簡易的 React Hooks。

完整渲染過程

我們再通過一次整體的流程圖來講解完整版的實現。

https://codesandbox.io/s/loving-blackburn-o7g8g

function Counter() {
  const [count, setCount] = useState(0);
  const [firstName, setFirstName] = useState("Rudi");

  const computed = () => {
    return count * 10 - 2;
  };
  const sum = useMemo(computed, [count]);

  useEffect(() => {
    console.log("init");
  }, []);
  return (
    <div>
      <div>
        {count} * 10 - 2 = {sum}
      </div>
      <button onClick={() => setCount(count + 1)}>點擊</button>
      <div>{firstName}</div>
      <button onClick={() => setFirstName("Fred")}>Fred</button>
    </div>
  );
}

初始化

1-初始化.png

第一次渲染

將所有的狀態都存進閉包中。

1-第一次渲染.png

事件觸發

改變了第二個狀態的value值。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-W1JedfMK-1570871852168)(https://s3.qiufengh.com/blog/1-事件觸發.png)]

第二次渲染

將所有狀態依次取出,進行渲染。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-P4SagYxs-1570871852169)(https://s3.qiufengh.com/blog/1-第二次渲染.png)]

後記

通過以上的實現,我們也可以明白一些 React Hooks 中看似有點奇怪的規定了。例如爲什麼不要在循環、條件判斷或者子函數中調用? 因爲順序很重要,我們將緩存(狀態)按一定地順序壓入數組,所以取出上一次狀態,也必須以同樣的順序去獲取。否則的話,會導致獲取不一致的情況。。。當然我們可以試想一下,如果每個狀態單元,可以有唯一的名字,那麼將不會受到這些規定的約束。但是這樣會使得開發帶來額外的傳入參數,就是唯一的名字。也會帶來名字衝突等問題,因此採用這樣的方式來約定,一定程度上簡化了開發者的開發成本,並且也能夠消除不一致性。(ps: 如果有人有興趣,可以實現一版不依賴於順序,只依賴於名字的,當做小玩具~)

當然真實中的 react 是利用了單鏈表來代替數組的。略微有些不一樣,但是本質的思路是一致的,以及 useEffect 是每次渲染完成後運行的。

以上都是站在巨人的肩膀上(有很多優秀的文章,看參考),再加上查看一些源碼得出的整個過程。最後,留出一個小問題給大家,那麼每次 useEffectreturn 函數 的邏輯又是怎麼樣的呢?歡迎評論區說出實現方式~ 如果文章有任何問題,也歡迎在評論區指出~

參考

https://github.com/brickspert/blog/issues/26

https://segmentfault.com/a/1190000019824818

https://www.zhihu.com/question/306916142

https://zh-hans.reactjs.org/docs/hooks-faq.html#can-i-skip-an-effect-on-updates

https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

https://github.com/preactjs/

https://zh-hans.reactjs.org/docs/hooks-intro.html#motivation

更多請關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章