React Hooks 設計思想

2021-02-20 前端大全

(給前端大全加星標,提升前端技能)

作者:繁星

https://zhuanlan.zhihu.com/p/103692400

聊聊 React 的 class 組件

組件是 React 應用的構建塊,自上而下的數據流結合組件可以將 UI 解構為獨立且可復用的單元。組件主要做的事情主要有以下三點:

假如現在有一個新聞列表頁面,列表的每一項都包含有標題、概要、詳情和縮略圖,如圖所示:

只是渲染內容。如果不考慮查看詳情這個交互,新聞列表的每一項是很純的,也就是 props 傳入什麼數據,就能渲染出一一對應的結果:

letNewsItem= (props) => {

return(

<li>

<img src={props.imgUrl} />

<div>

<h2>{props.title}</h2>

<p>{props.summary}</p>

<p style={{display: 'none'}}>{props.detail}</p>

<a>查看詳情</a>

</div>

</li>

)

}

要考慮查看詳情這個交互,就必須在 NewsItem 裡加入一個 isDetailShow 的 state 來表示新聞摘要與詳情的互斥顯示。到目前為止,NewsItem 還是很純的,並沒有和外部有交互。

要實現新聞圖片的懶加載,只有 NewsItem 進入可視區時才將 img 的 src 替換為真實的 url,這就要求 NewsItem 必須監聽瀏覽器事件,並在組件被卸載時移除這些監聽(防止內存洩漏)。此時,NewsItem 便不是一個純的組件了,因為與外部有了交互,這種與外部的交互被稱為副作用(函數式編程裡沒有任何副作用的函數被稱為純函數)。

組件的副作用是不可避免的,最常見的有 fetch data,訂閱事件,進行 DOM 操作,使用其他 JavaScript 庫(比如 jQuery,Map 等)。在這個例子中,NewsItem 並沒有 fetch data,相關職責由不純的父組件來承擔。

綜上,我們的組件需要 state 來存儲一定的邏輯狀態,並且需要可以訪問並更改 state 的方法函數。

class 就是一個很好的表現形式:要渲染的內容(props 或 state)放在類的屬性裡,那些處理用戶交互的回調函數和生命周期函數放在類的方法裡。方法與屬性通過 class 的形式建立了關聯,有能力訪問和更改屬性。回調函數通過更改對應屬性處理用戶操作,生命周期函數則給予開發者處理組件與外部的交互能力(處理副作用)。

這樣通過 class 組件,ReactDOM 就能做到渲染數據,綁定事件,並在不同的生命周期調用開發者所編寫的代碼,按需求將數據渲染成 HTML DOM,然後被瀏覽器渲染展示出來。

將組件渲染粗暴地分為若干個階段,通過生命周期函數處理副作用會帶來一些問題:

重複邏輯,被吐槽最多的例子如下:

async componentDidMount() {

const res = awaitget(`/users`);

this.setState({ users: res.data });

};

async componentDidUpdate(prevProps) {

if(prevProps.resource !== this.props.resource) {

const res = awaitget(`/users`);

this.setState({ users: res.data });

}

};

同一職責代碼有可能需要被強行分拆到不同的生命周期,例如同一個事件的訂閱與取消訂閱;

一部分代碼被分割到不同生命周期中,會導致組件沒有優雅的復用 state 邏輯代碼的能力,高階組件或 render props 等模式引入了嵌套,複雜且不靈活;

越來越多邏輯被放入不同生命周期函數中,這種組織方式導致代碼越來越複雜難懂;

除了這些,class 組件中的 this 也常被人們拿出來吐槽。那麼,是否有更優雅的設計呢?

閉包為什麼在某種程度上能取代 class?

我們的程序在執行的時候主要做了兩件事:

為了實現復用,我們將具有特定單一功能的邏輯放在函數裡,這樣既可以消滅掉重複代碼,又可以讓我們在思考問題時能夠進行合理的分解,降低代碼複雜度。

但是只有函數是不夠的,函數是一個標準的輸入-加工-輸出模型,輸入和輸出的都是變量裡所存儲的數據,當一個系統的複雜度高到一定程度的時候,將函數與其所操作的數據(環境)關聯起來就很有必要了。

註:函數式編程要求把I/O限制到最小,幹掉所有不必要的讀寫行為,保持計算過程的單純性。

最常見的將變量與函數關聯起來方式有:

函數對於其詞法環境(lexical environment)的引用共同構成閉包(closure),簡單說,一個函數內部能夠訪問到函數外的變量,如果這 個函數內部引用了其外部的變量,且自身又被別處引用,那這個不會被銷毀的函數就和它所引用的外部變量一起構成閉包。例如:

// 模塊化下可以將 makeCounter 內部代碼放在 makeCounter.js 中,並將 return 改為 export

const makeCounter = () => {

let privateCounter = 0;

function changeBy(val) {

privateCounter += val;

}

return{

increment: function() {

changeBy(1);

},

decrement: function() {

changeBy(-1);

},

value: function() {

return privateCounter;

}

}

};

// 使用 makeCounter

const counter = makeCounter();

console.log(counter.value()); /* logs 0 */

counter.increment();

counter.increment();

console.log(counter.value()); /* logs 2 */

counter.decrement();

console.log(counter.value()); /* logs 1 */

看,我們使用閉包將變量 privateCounter 與幾個函數關聯了起來,從這點來講能力與面向對象編程相同。

組件的 API 設計

API 的核心在於表達能力,對於 React 組件來說,就是如何讓開發者將需求良好地表達出來,然後被 ReactDOM 識別並渲染。

class 組件和 functional 組件所要表達的內容是是一樣的,只是表現形式不同。它們都努力做到了一點:將存儲組件狀態的 state 與處理這些 state 的方法關聯起來。具體一點說就是一下三點:

state 存儲組件的狀態,並可被渲染成 HTML DOM;

用來處理用戶操作事件的回調函數可以訪問並變更 state,觸發組件重新渲染;

用來處理與組件外部交互(副作用)的函數可以訪問並變更 state,觸發組件重新渲染;

2 中函數的執行是確定的,用戶的操作觸發某個事件後就會執行相應的回調函數,更改 state,觸發新的渲染。開發者需要有能力控制 3 中的函數執行,確定要不要執行以及在什麼時候執行。在 class 組件中,生命周期函數給開發者提供了這種控制能力。

那麼,如果我們通過一套 API 設計實現以上三點且避開 class 組件的缺陷,提供更好的分離關注點能力,讓代碼復用更加簡易,是不是一件很值得期待的事情呢?React Hooks 就是滿足這些要求的新設計。

React Hooks 原理

先來看一個使用 React Hooks 的例子:

functionCounter() {

const[counter, setCounter] = useState(0);

function increment() {

setCounter(counter+1);

}

function decrement() {

setCounter(counter-1);

}

return(

<div className="content">

<h1>MyAwesomeCounter</h1>

<hr/>

<h2 className="count">{counter}</h2>

<div className="buttons">

<button onClick={increment}>+</button>

<button onClick={decrement}>-</button>

</div>

</div>

);

}

是的,你看到了這個例子與閉包例子中的 makeCounter 十分相似。makerCounter 使用程序控制並通過 console 出結果,Counter 通過用戶點擊控制,輸出包含結果且可以被渲染的組件。除了這點不同,其他部分代碼原理是完全一致的,只是 Hook 進行了一些封裝,讓開發者編寫代碼體驗更好。

我們來看下 useState 的簡化實現:

// React useState hooks

constReact= (function() {

let hooks = [];

let idx = 0;

return{

render(Component) {

const C = Component();

C.render();

idx = 0; // reset for next render

return C;

},

useState(initVal) {

const state = hooks[idx] || initVal;

const _idx = idx;

const setState = newVal => {

hooks[_idx] = newVal;

};

idx++;

return[state, setState];

}

};

})();

// Component which use useState

const{ useState, render } = React;

functionCounter() {

const[count, setCount] = useState(0);

const[text, setText] = useState('apple');

return{

render() {

console.log(`text: ${text}, count: ${count}`);

},

click() {

setCount(count + 1);

},

type(type) {

setText(type)

}

};

}

// simulate render

const counter = render(Counter); // text: apple, count: 0

counter.click();

render(Counter); // text: apple, count: 1

counter.type("pear");

render(Counter); //text: pear, count: 1

代碼很簡單,這裡不做解讀,這裡重點說幾點:

React 的 API 設計能力確實不錯,用解構賦值將 state 和對應的 setState 放在一起,簡潔明了;

useState 的第一次執行可以取代 class 的構造函數初始化過程,值為 useState 的參數 initVal,運行後存儲在閉包中所對應的 hooks[index] 變量裡。從第二次 render 時開始訪問 hooks[index] 而不是 initVal;

初始化時每調用一次 useState ,閉包裡 hooks 便會遞增分配對應的 index key 來存儲對應的值。render 結束後 index 會重置為 0,下一次 render 執行 useState 時會按照相同順序訪問 hooks[index];

正是因為 hooks 是這樣實現的,我們在調用 hooks 的時候必須要嚴格保證每一次 render 都能獲得一致的執行順序,所以必須要做到:

到目前為止,我們已經可以通過 hooks 的形式管理 state,並通過調用包含 setState 的回調函數處理用戶操作。剩下要解決的便是副作用的問題,useEffect 是 hooks 所提供的方案,下面來看一下 useEffect 的簡化實現原理(並不完整):

useEffect(cb, depArray) {

const hasNoDeps = !depArray;

hooks[idx] = hooks[idx] || {};

const{deps, cleanup} = hooks[idx]; // undefined when first render

const hasChanged = deps

? !depArray.every((el, i) => el === deps[i])

: true;

if(hasNoDeps || hasChanged) {

cleanup && cleanup();

hooks[idx].cleanup = cb();

hooks[idx].deps = depArray;

}

idx++;

}

完整簡化代碼地址:https://stackblitz.com/edit/behind-react-hook

useEffect 提供了一個函數(上面代碼中的 cb)運行的容器,這個容器有以下幾個特點:

useEffect 容器在每次 render 後運行;

不區分 Mounting 和 Updating ,每次 render 後都會執行容器 useEffect;

cb 運行時可以訪問到 Functional 組件的內部變量(包含通過 useState 生成的任何 state 和 setState);

cb 是否執行取決於依賴數組裡的依賴項是否發生變化。如果沒有依賴數組,每次 render 後都會調用 cb。如果依賴數組為[],僅在第一次 render 後調用;

容器中的 cb 執行後可以返回一個函數 cleanup,在下一次執行 cb 之前會調用 cleanup;

在 Unmounting 時如果有返回的 cleanup,也會調用(簡化代碼沒有實現);

通過將副作用相關代碼放在 useEffect 的 cb 中,並在 cb 返回的函數裡移除副作用,我們可以在一個 useEffect 中實現任何想要的生命周期控制:

依賴數組為 [] 可以實現僅在 Mouting 時執行;

不寫依賴數組可以實現 Mouting 和 Updating 時執行;

cb 返回的 cleanup 函數可以執行 Unmounting 時執行的代碼;

可以通過依賴數組裡的內容是否變更來控制 cb 是否執行;

這種設計最大的好處就是我們可以將單一職責的代碼放在一個獨立的 useEffect 容器裡,而不是粗暴地將它們拆分在各個生命周期函數中。同時也要注意的是,useEffect 的 cb 必須要返回一個 cleanup 函數或者 undefined,所以不可以是 async 函數;

React Hooks 的優點

通過 Hooks 我們可以對 state 邏輯進行良好的封裝,輕鬆做到隔離和復用,優點主要體現在:

復用代碼更容易:hooks 是普通的 JavaScript 函數,所以開發者可以將內置的 hooks 組合到處理 state 邏輯的自定義 hooks中,這樣複雜的問題可以轉化一個單一職責的函數,並可以被整個應用或者 React 社區所使用;

使用組合方式更優雅:不同於 render props 或高階組件等的模式,hooks 不會在組件樹中引入不必要的嵌套,也不會受到 mixins 的負面影響;

更少的代碼量:一個 useEffect 執行單一職責,可以幹掉生命周期函數中的重複代碼。避免將同一職責代碼分拆在幾個生命周期函數中,更好的復用能力可以幫助優秀的開發者最大限度降低代碼量;

代碼邏輯更清晰:hooks 幫助開發者將組件拆分為功能獨立的函數單元,輕鬆做到「分離關注點」,代碼邏輯更加清晰易懂;

單元測試:處理 state 邏輯的自定義 hooks 可以被獨立進行單元測試,更加可靠;

本文主要介紹了 React Hooks 設計思想和優點,但 hooks 也是有不少」坑點「的,我們在使用的時候要利用好優點,努力避開」坑點「。後面我會單獨寫一篇文章來介紹 React Hooks 的實踐。

覺得本文對你有幫助?請分享給更多人

關注「前端大全」加星標,提升前端技能

好文章,我在看❤️

相關焦點

  • 函數式編程看React Hooks(一)簡單React Hooks實現
    在面向對象程序編程裡,電腦程式會被設計成彼此相關的對象函數式強調在邏輯處理中不變性。面向對象通過消息傳遞改變每個Object的內部狀態。兩者是截然不同的編程思想,都具有自己的優勢,也因為如此,才使得我們從 class組件 轉化到 函數組件式,有一些費解。從 react 的變化可以看出,react 走的道路越來越接近於函數式編程,輸入輸出一致性。
  • React Hooks使用小結
    但是從掘金,知乎,博客以及面試等地方可以發現,hooks是無法避免的一個話題,好像不用hooks你就是上個世紀的人了(流下了前端卑微的淚水)  為了不當原始人,今天就來和大家一起探討下hooks大法。為什麼要用hooks?
  • React 16.8發布:hooks終於來了!
    如果你之前從未聽說過 hooks,可以參考以下這些資源:「Introducing hookss」解釋了我們為 React 添加 hooks 功能:https://reactjs.org/docs/hooks-intro.html「hookss at a Glance」對內置的 hooks 進行了快速的介紹:https://reactjs.org
  • 你可能不知道的 React Hooks
    參考資料[1] Hooks API Reference: https://reactjs.org/docs/hooks-reference.html[2] useEffect: https://reactjs.org/docs/hooks-reference.html#useeffect[3] Hooks API Reference: https://reactjs.org
  • 寫React Hooks前必讀
    來源:相學長 https://zhuanlan.zhihu.com/p/113216415最近團隊內有同學,由於寫react hooks引發了一些bug,甚至有1例是線上問題。團隊內也因此發起了一些爭執,到底要不要寫hooks?到底要不要加lint?到底要不要加autofix?
  • ahooks 正式發布:值得擁抱的 React Hooks 工具庫
    方案,這便也是 ahooks 的由來。那麼好奇的你肯定會問, ice/hooks 與標題的 ahooks 的關係是什麼?待我細細道來 😆在 ice/hooks RFC 期間,我們也對比參考了社區的同類方案諸如 react-use 等,但最終因為 react-use 提供的 Hooks 過於冗餘
  • 30 分鐘精通 React 新特性——React Hooks
    難道是Mixins要在react中死灰復燃了嗎?當然不會了,等會我們再來談兩者的區別。總而言之,這些hooks的目標就是讓你不再寫class,讓function一統江湖。React為什麼要搞一個Hooks?想要復用一個有狀態的組件太麻煩了!我們都知道react都核心思想就是,將一個頁面拆成一堆獨立的,可復用的組件,並且用自上而下的單向數據流的形式將這些組件串聯起來。
  • 寫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(四):useEffect
    hooks的設計中,每一次DOM渲染完成,都會有當次渲染的副作用可以執行。而useEffect,是一種提供我們能夠自定義副作用邏輯的方式3一個簡單的案例。現在有一個counter表示數字,我希望在DOM渲染完成之後的一秒鐘,counter數字加1。
  • React v16.8 發布:帶來穩定版的 Hooks 功能
    hooks 可以讓你在不編寫類的情況下使用 state 和 React 的其他功能。你還可以構建自己的 hooks,在組件之間共享可重用的有狀態邏輯。>從 16.8.0 開始,React 包含穩定的 React Hooks 實現:React DOMReact DOM ServerReact Test RendererReact Shallow Renderer要注意的是,如需使用 hooks
  • 【實戰總結篇】寫React Hooks前必讀
    「最近團隊內有同學,由於寫react hooks引發了一些bug,甚至有1例是線上問題。
  • 30 分鐘精通 React 今年最勁爆的新特性 —— React Hooks
    難道是Mixins要在react中死灰復燃了嗎?當然不會了,等會我們再來談兩者的區別。總而言之,這些hooks的目標就是讓你不再寫class,讓function一統江湖。React為什麼要搞一個Hooks?想要復用一個有狀態的組件太麻煩了!我們都知道react都核心思想就是,將一個頁面拆成一堆獨立的,可復用的組件,並且用自上而下的單向數據流的形式將這些組件串聯起來。
  • React Hooks 原理與最佳實踐
    Custom Hooks對於 react 來說,在函數組件中使用 state 固然有一些價值,但最有價值的還是可以編寫通用 custom hooks 的能力。想像一下,一個單純不依賴 UI 的業務邏輯 hook,我們開箱即用。不僅可以在不同的項目中復用,甚至還可以跨平臺使用,react、react native、react vr 等等。
  • React hooks 最佳實踐【更新中】
    首先對於原先的類組件而言,最好的思想是封裝,我們使用的constructor、componentDidMount都是繼承自React的方法,這樣做相對於hooks來說的好處是,我們的每一個組件對於我們來說都是可預見的,這樣我們在寫每個組件的時候也都是在這個思路上進行開發的,很顯然,這樣一種方式帶來的不便就是我們每個組件的開發成本太高,組件其中如果有涉及到某個生命周期的邏輯,我們也不便將它抽離出來復用
  • React Hooks 還不如類?
    你必須遵循一些嚴格而怪異的規則:https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level需要注意代碼放置的位置,並且這裡面存在許多陷阱。我不能將一個 hook 放在一個 if 語句中,因為 hooks 的內部機制是基於調用順序的,這簡直太瘋狂了!
  • 使用React Hooks代替類的6大理由
    React hooks 已經出來有一段時間了,但是許多 React 開發人員對它們的態度並不是很積極。我發現這背後主要有兩個原因。第一個原因是許多 React 開發人員已經參與了一個大型項目,需要付出巨大的努力才能遷移整個代碼庫。另一個原因是大家對 React 類已經很熟悉了。有足夠經驗的話,繼續使用類會感到更自在。
  • React-hooks入坑指南
    前言如果你是在使用React >= 16.8的版本,那麼你可以使用hooks在編寫你的組件,它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。由於微信中無法插入非微信域名的超連結,所以文章的超連結無法進行跳轉,可能會影響閱讀體驗。
  • React:useHooks小竅門
    這篇文章提供了簡單易懂的案例,幫助你去了解hooks如何使用,並且鼓勵你在接下來的項目中去使用它。但是在此之前,請確保你已經看了hook的官方文檔useEventListener如果你發現自己使用useEffect添加了許多事件監聽,那你可能需要考慮將這些邏輯封裝成一個通用的hook。
  • 超性感的React Hooks(十一)useCallback、useMemo
    1react hooks提供的api,大多都有記憶功能。例如•useState•useEffect•useLayoutEffect•useReducer•useRef•useMemo 記憶計算結果•useCallback 記憶函數體其他幾個api的使用方法,我們在前面已經一一跟大家分析過。這裡主要關注useMemo與useCallback。
  • 「不容錯過」手摸手帶你實現 React Hooks
    如此很容易產生 bugthis 指向問題:父組件給子組件傳遞函數時,必須綁定 thisHook 規則 只能在函數內部的最外層調用 Hook,不要在循環、條件判斷或者子函數中調用只在 React 函數中調用 Hook 在 React 的函數組件中調用 Hook 在自定義 Hook 中調用其他 Hook利用 eslint 做 hooks