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

2021-03-02 React

函數式編程看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 的方法來實現。即用數組來實現多個函數的處理邏輯。

核心邏輯就是

也可以通過以下圖來理解

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

每次重新渲染,獲取數組中每個的緩存狀態。

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

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>

);

}

初始化

第一次渲染

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

事件觸發

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

第二次渲染

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

後記

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

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

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

參考

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

更多請關注

友情連結:https://huayifeng.top/

相關焦點

  • React Hooks使用小結
    但是從掘金,知乎,博客以及面試等地方可以發現,hooks是無法避免的一個話題,好像不用hooks你就是上個世紀的人了(流下了前端卑微的淚水)  為了不當原始人,今天就來和大家一起探討下hooks大法。為什麼要用hooks?
  • 你可能不知道的 React Hooks
    本文是譯文,原文地址是:https://medium.com/@sdolidze/the-iceberg-of-react-hooks-af0b588f43fbReact Hooks 與類組件不同,它提供了用於優化和組合應用程式的簡單方式,並且使用了最少的樣板文件。
  • 寫React Hooks前必讀
    英文文檔:https://reactjs.org/docs/hooks-intro.html中文文檔:https://zh-hans.reactjs.org/docs/hooks-intro.html其中重點必看hooks:useState、useReducer、useEffect、useCallback、useMemo
  • React 16.8發布:hooks終於來了!
    從 16.8.0 開始,React 包含了穩定版本的 React hooks 實現:React DOMReact DOM ServerReact Test RendererReact Shallow Renderer請注意,要使用 hooks,所有 React 包都需要升級到 16.8.0 或更高版本。
  • 30 分鐘精通 React 新特性——React Hooks
    我們可以稍微跑下題簡單看一下這兩種模式。渲染屬性指的是使用一個值為函數的prop來傳遞需要動態渲染的nodes或組件。如下面的代碼可以看到我們的 DataProvider組件包含了所有跟狀態相關的代碼,而 Cat組件則可以是一個單純的展示型組件,這樣一來 DataProvider就可以單獨復用了。
  • React Hooks 原理與最佳實踐
    然後我寫了一篇文章,利用 Object.defineProperty 簡單實現了 Composition API,可以參考:用 React Hooks 簡單實現 Vue3 Composition API當然這個實現還有很多問題,也比較簡單,可以參考工業聚寫的完整實現:react-use-setup
  • 超性感的React Hooks(四):useEffect
    而在hooks中的思維則不同:創造一個變量,來作為變化值,實現目的的同時防止循環執行代碼如下:import React, { useState, useEffect } from 'react';import '.
  • 寫React Hooks前需要注意什麼?
    必須完整閱讀一次React Hooks官方文檔英文文檔:https://reactjs.org/docs/hooks-intro.html中文文檔:https://zh-hans.reactjs.org/docs/hooks-intro.html其中重點必看hooks: useState、useReducer、useEffect、useCallback
  • React Hooks 還不如類?
    你必須遵循一些嚴格而怪異的規則:https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level需要注意代碼放置的位置,並且這裡面存在許多陷阱。我不能將一個 hook 放在一個 if 語句中,因為 hooks 的內部機制是基於調用順序的,這簡直太瘋狂了!
  • React Hooks 設計思想
    要實現新聞圖片的懶加載,只有 NewsItem 進入可視區時才將 img 的 src 替換為真實的 url,這就要求 NewsItem 必須監聽瀏覽器事件,並在組件被卸載時移除這些監聽(防止內存洩漏)。此時,NewsItem 便不是一個純的組件了,因為與外部有了交互,這種與外部的交互被稱為副作用(函數式編程裡沒有任何副作用的函數被稱為純函數)。
  • 30 分鐘精通 React 今年最勁爆的新特性 —— React Hooks
    我們可以稍微跑下題簡單看一下這兩種模式。渲染屬性指的是使用一個值為函數的prop來傳遞需要動態渲染的nodes或組件。如下面的代碼可以看到我們的DataProvider組件包含了所有跟狀態相關的代碼,而Cat組件則可以是一個單純的展示型組件,這樣一來DataProvider就可以單獨復用了。
  • 【實戰總結篇】寫React Hooks前必讀
    必須完整閱讀一次React Hooks官方文檔英文文檔:https://reactjs.org/docs/hooks-intro.html中文文檔:https://zh-hans.reactjs.org/docs/hooks-intro.html其中重點必看hooks: useState、useReducer、useEffect、useCallback
  • ahooks 正式發布:值得擁抱的 React Hooks 工具庫
    圖片來源:網絡從 React Hooks 正式發布到現在,越來越多的項目正在使用 Function Component 替代 Class Component,Hooks 這一新特性也逐漸被廣泛的使用方案,這便也是 ahooks 的由來。
  • React hooks 最佳實踐【更新中】
    1.儘量設計簡單的hookshooks 設計的初衷就是為了使開發更加快捷簡便,因此在使用hooks 的時候,我們不應該吝嗇使用較多的hooks,例如我們處理不同狀態對應不同邏輯的時候,按照寫class的邏輯,我們經常會在一個生命周期函數裡寫下多個邏輯,並用if區分;在寫hooks的時候,因為沒有shouldComponentUpdate這類的生命周期函數,我們應該將他們分離開,將他們寫在不同的useEffect
  • 在函數式編程中使用自定義React Hooks
    在本文中我決定走技術路線,分享我編寫自定義 hooks 和集成某些函數式編程策略的經驗。本文介紹了一個自定義 hook:useRecorder()。 本文最初發布於 Orizens 博客,經原作者 Oren Farhi 授權,由 InfoQ 中文站翻譯並分享。
  • React v16.8 發布:帶來穩定版的 Hooks 功能
    hooks 可以讓你在不編寫類的情況下使用 state 和 React 的其他功能。你還可以構建自己的 hooks,在組件之間共享可重用的有狀態邏輯。從 16.8.0 開始,React 包含穩定的 React Hooks 實現:React DOMReact DOM ServerReact Test RendererReact Shallow Renderer要注意的是,如需使用
  • 使用React Hooks代替類的6大理由
    擴展函數式組件時,不必將其重構為類組件經常會有這種情況,那就是一個 React 組件從一個函數式組件開始開發,一開始這個函數式組件只依賴 props,後來演變為具有狀態的類組件。從函數式組件更改為類組件需要一些重構工作,具體取決於組件的複雜程度。使用 React Hooks 時,由於函數式組件具有進入狀態的能力,因此重構工作會非常少。
  • 超性感的React Hooks(十一)useCallback、useMemo
    1react hooks提供的api,大多都有記憶功能。例如•useState•useEffect•useLayoutEffect•useReducer•useRef•useMemo 記憶計算結果•useCallback 記憶函數體其他幾個api的使用方法,我們在前面已經一一跟大家分析過。這裡主要關注useMemo與useCallback。
  • 「不容錯過」手摸手帶你實現 React Hooks
    例如,useState 是允許你在 React 函數組件中添加 state 的 Hook。為什麼使用 Hooks 引用官網描述可能要用到 render props (渲染屬性)或者 HOC(高階組件),但無論是渲染屬性,還是高階組件,都會在原先的組件外包裹一層父容器(一般都是 div 元素).如果你在 React DevTools 中觀察過 React 應用,你會發現由 providers
  • React:useHooks小竅門
    這篇文章提供了簡單易懂的案例,幫助你去了解hooks如何使用,並且鼓勵你在接下來的項目中去使用它。但是在此之前,請確保你已經看了hook的官方文檔useEventListener如果你發現自己使用useEffect添加了許多事件監聽,那你可能需要考慮將這些邏輯封裝成一個通用的hook。