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

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

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

前言

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

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

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

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

函數式強調在邏輯處理中不變性。面向對象經過消息傳遞改變每一個Object的內部狀態。二者是大相徑庭的編程思想,都具備本身的優點,也由於如此,才使得咱們從 class 組件 轉化到 函數組件式,有一些費解。編程

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

  • 代碼重用:在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>
  );
}
複製代碼

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

以上,無論 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;
  }
}
複製代碼

演示地址: codesandbox.io/s/ecstatic-…

至此,咱們也實現了單個 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;
}
複製代碼

演示地址: codesandbox.io/s/festive-p…

useCallback

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

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

能夠看到,以上咱們也輕鬆地實現了 useMemo 。可是有一個問題,以上只是單個函數使用方式,因此接下來咱們還須要處理一下多個函數的狀況。

完整版

咱們能夠按照 preact 的方法來實現。即用數組來實現多個函數的處理邏輯。

核心邏輯就是

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

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

也能夠經過如下圖來理解

第一次渲染,將每一個狀態都緩存到數組中。

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。

完整渲染過程

咱們再經過一次總體的流程圖來說解完整版的實現。

codesandbox.io/s/loving-bl…

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值。

1-事件觸發.png

第二次渲染

將全部狀態依次取出,進行渲染。

1-第二次渲染.png

後記

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

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

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

參考

github.com/brickspert/…

segmentfault.com/a/119000001…

www.zhihu.com/question/30…

zh-hans.reactjs.org/docs/hooks-…

medium.com/@ryardley/r…

github.com/preactjs/

zh-hans.reactjs.org/docs/hooks-…

更多請關注

相關文章
相關標籤/搜索