React Hooks 設(shè)計動機與工作模式
React-Hooks 設(shè)計動機初探
何謂類組件(Class Component)
所謂類組件,就是基于 ES6 Class 這種寫法,通過繼承 React.Component 得來的 React 組件。以下是一個典型的類組件:
class DemoClass extends React.Component {
// 初始化類組件的 state
state = {
text: ""
};
// 編寫生命周期方法 didMount
componentDidMount() {
// 省略業(yè)務(wù)邏輯
}
// 編寫自定義的實例方法
changeText = (newText) => {
// 更新 state
this.setState({
text: newText
});
};
// 編寫生命周期方法 render
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={this.changeText}>點我修改</button>
</div>
);
}
}
何謂函數(shù)組件/無狀態(tài)組件(Function Component/Stateless Component)
函數(shù)組件顧名思義,就是以函數(shù)的形態(tài)存在的 React 組件。早期并沒有 React-Hooks 的加持,函數(shù)組件內(nèi)部無法定義和維護 state,因此它還有一個別名叫“無狀態(tài)組件”。以下是一個典型的函數(shù)組件:
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`function 組件所接收到的來自外界的文本內(nèi)容是:[${text}]`}</p>
</div>
);
}
函數(shù)組件與類組件的對比:無關(guān)“優(yōu)劣”,只談“不同”
我們先基于上面的兩個 Demo,從形態(tài)上對兩種組件做區(qū)分。它們之間肉眼可見的區(qū)別就包括但不限于:
類組件需要繼承 class,函數(shù)組件不需要;類組件可以訪問生命周期方法,函數(shù)組件不能; 類組件中可以獲取到實例化后的 this,并基于這個this做各種各樣的事情,而函數(shù)組件不可以;類組件中可以定義并維護 state(狀態(tài)),而函數(shù)組件不可以;
單就我們列出的這幾點里面,頻繁出現(xiàn)了“類組件可以 xxx,函數(shù)組件不可以 xxx”,這是否就意味著類組件比函數(shù)組件更好呢?
答案當然是否定的。你可以說,在 React-Hooks 出現(xiàn)之前的世界里,類組件的能力邊界明顯強于函數(shù)組件,但要進一步推導(dǎo)“類組件強于函數(shù)組件”,未免顯得有些牽強。同理,一些文章中一味鼓吹函數(shù)組件輕量優(yōu)雅上手迅速,不久的將來一定會把類組件干沒(類組件:我做錯了什么?)之類的,更是不可偏聽偏信。
當我們討論這兩種組件形式時,不應(yīng)懷揣“孰優(yōu)孰劣”這樣的成見,而應(yīng)該更多地去關(guān)注兩者的不同,進而把不同的特性與不同的場景做連接,這樣才能求得一個全面的、辯證的認知。
重新理解類組件:包裹在面向?qū)ο笏枷胂碌摹爸匮b戰(zhàn)艦”
類組件是面向?qū)ο缶幊趟枷氲囊环N表征。面向?qū)ο笫且粋€老生常談的概念了,當我們應(yīng)用面向?qū)ο蟮臅r候,總是會有意或無意地做這樣兩件事情。
封裝:將一類屬性和方法,“聚攏”到一個 Class 里去。 繼承:新的 Class 可以通過繼承現(xiàn)有 Class,實現(xiàn)對某一類屬性和方法的復(fù)用。
React 類組件也不例外。我們再次審視一下這個典型的類組件 Case:
class DemoClass extends React.Component {
// 初始化類組件的 state
state = {
text: ""
};
// 編寫生命周期方法 didMount
componentDidMount() {
// 省略業(yè)務(wù)邏輯
}
// 編寫自定義的實例方法
changeText = (newText) => {
// 更新 state
this.setState({
text: newText
});
};
// 編寫生命周期方法 render
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={this.changeText}>點我修改</button>
</div>
);
}
}
不難看出,React 類組件內(nèi)部預(yù)置了相當多的“現(xiàn)成的東西”等著你去調(diào)度/定制,state 和生命周期就是這些“現(xiàn)成東西”中的典型。要想得到這些東西,難度也不大,你只需要輕輕地繼承一個 React.Component 即可。
這種感覺就好像是你不費吹灰之力,就擁有了一輛“重裝戰(zhàn)艦”,該有的槍炮導(dǎo)彈早已配備整齊,就等你操縱控制臺上的一堆開關(guān)了。
毋庸置疑,類組件給到開發(fā)者的東西是足夠多的,但“多”就是“好”嗎?其實未必。
把一個人塞進重裝戰(zhàn)艦里,他就一定能操縱這臺戰(zhàn)艦嗎?如果他沒有經(jīng)過嚴格的訓(xùn)練,不清楚每一個操作點的內(nèi)涵,那他極有可能會把炮彈打到友軍的營地里去。
React 類組件,也有同樣的問題——它提供了多少東西,你就需要學(xué)多少東西。假如背不住生命周期,你的組件邏輯順序大概率會變成一團糟?!按蠖钡谋澈?,是不可忽視的學(xué)習(xí)成本。
再想這樣一個場景:假如我現(xiàn)在只是需要打死一只蚊子,而不是打掉一個軍隊。這時候繼續(xù)開動重裝戰(zhàn)艦,是不是正應(yīng)了那句老話——“可以,但沒有必要”。這也是類組件的一個不便,它太重了,對于解決許多問題來說,編寫一個類組件實在是一個過于復(fù)雜的姿勢。復(fù)雜的姿勢必然帶來高昂的理解成本,這也是我們所不想看到的。
更要命的是,由于開發(fā)者編寫的邏輯在封裝后是和組件粘在一起的,這就使得類**組件內(nèi)部的邏輯難以實現(xiàn)拆分和復(fù)用。**如果你想要打破這個僵局,則需要進一步學(xué)習(xí)更加復(fù)雜的設(shè)計模式(比如高階組件、Render Props 等),用更高的學(xué)習(xí)成本來交換一點點編碼的靈活度。
這一切的一切,光是想想就讓人頭禿。所以說,類組件固然強大, 但它絕非萬能。
深入理解函數(shù)組件:呼應(yīng) React 設(shè)計思想的“輕巧快艇”
我們再來看這個函數(shù)組件的 case:
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`function 組件所接收到的來自外界的文本內(nèi)容是:[${text}]`}</p>
</div>
);
}
當然啦,要是你以為函數(shù)組件的簡單是因為它只能承擔(dān)渲染這一種任務(wù),那可就太小瞧它了。它同樣能夠承接相對復(fù)雜的交互邏輯,像這樣:
function DemoFunction(props) {
const { text } = props
const showAlert = ()=> {
alert(`我接收到的文本是${text}`)
}
return (
<div className="demoFunction">
<p>{`function 組件所接收到的來自外界的文本內(nèi)容是:[${text}]`}</p>
<button onClick={showAlert}>點擊彈窗</button>
</div>
);
}
相比于類組件,函數(shù)組件肉眼可見的特質(zhì)自然包括輕量、靈活、易于組織和維護、較低的學(xué)習(xí)成本等。
類組件和函數(shù)組件之間,縱有千差萬別,但最不能夠被我們忽視掉的,是心智模式層面的差異,是面向?qū)ο蠛秃瘮?shù)式編程這兩套不同的設(shè)計思想之間的差異。
說得更具體一點,函數(shù)組件更加契合 React 框架的設(shè)計理念。何出此言?不要忘了這個赫赫有名的 React 公式:

不夸張地說,React 組件本身的定位就是函數(shù),一個吃進數(shù)據(jù)、吐出 UI 的函數(shù)。作為開發(fā)者,我們編寫的是聲明式的代碼,而 React 框架的主要工作,就是及時地把聲明式的代碼轉(zhuǎn)換為命令式的 DOM 操作,把數(shù)據(jù)層面的描述映射到用戶可見的 UI 變化中去。這就意味著從原則上來講,React 的數(shù)據(jù)應(yīng)該總是緊緊地和渲染綁定在一起的,而類組件做不到這一點。
首先我們來看這樣一個類組件:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
這個組件返回的是一個按鈕,交互內(nèi)容也很簡單:點擊按鈕后,過 3s,界面上會彈出“Followed xxx”的文案。類似于我們在微博上點擊“關(guān)注某人”之后彈出的“已關(guān)注”這樣的提醒。
看起來好像沒啥毛病,但是如果你在這個在線 Demo中嘗試點擊基于類組件形式編寫的 ProfilePage 按鈕后 3s 內(nèi)把用戶切換為 Sophie,你就會看到如下圖所示的效果:

明明我們是在 Dan 的主頁點擊的關(guān)注,結(jié)果卻提示了“Followed Sophie”!
這個現(xiàn)象必然讓許多人感到困惑:user 的內(nèi)容是通過 props 下發(fā)的,props 作為不可變值,為什么會從 Dan 變成 Sophie 呢?
因為雖然 props 本身是不可變的,但 this 卻是可變的,this 上的數(shù)據(jù)是可以被修改的,this.props 的調(diào)用每次都會獲取最新的 props,而這正是 React 確保數(shù)據(jù)實時性的一個重要手段。
多數(shù)情況下,在 React 生命周期對執(zhí)行順序的調(diào)控下,this.props 和 this.state 的變化都能夠和預(yù)期中的渲染動作保持一致。但在這個案例中,我們通過 setTimeout 將預(yù)期中的渲染推遲了 3s,打破了 this.props 和渲染動作之間的這種時機上的關(guān)聯(lián),進而導(dǎo)致渲染時捕獲到的是一個錯誤的、修改后的 this.props。這就是問題的所在。
但如果我們把 ProfilePage 改造為一個像這樣的函數(shù)組件:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
事情就會大不一樣。
props 會在 ProfilePage 函數(shù)執(zhí)行的一瞬間就被捕獲,而 props 本身又是一個不可變值,因此我們可以充分確保從現(xiàn)在開始,在任何時機下讀取到的 props,都是最初捕獲到的那個 props。當父組件傳入新的 props 來嘗試重新渲染 ProfilePage 時,本質(zhì)上是基于新的 props 入?yún)l(fā)起了一次全新的函數(shù)調(diào)用,并不會影響上一次調(diào)用對上一個 props 的捕獲。這樣一來,我們便確保了渲染結(jié)果確實能夠符合預(yù)期。
如果你認真閱讀了我前面說過的那些話,相信你現(xiàn)在一定也**不僅僅能夠充分理解 Dan 所想要表達的“函數(shù)組件會捕獲 render 內(nèi)部的狀態(tài)”**這個結(jié)論,而是能夠更進一步地意識到這樣一件事情:函數(shù)組件真正地把數(shù)據(jù)和渲染綁定到了一起。
經(jīng)過歲月的洗禮,React 團隊顯然也認識到了,函數(shù)組件是一個更加匹配其設(shè)計理念、也更有利于邏輯拆分與重用的組件表達形式,接下來便開始“用腳投票”,用實際行動支持開發(fā)者編寫函數(shù)式組件。于是,React-Hooks 便應(yīng)運而生。
Hooks 的本質(zhì):一套能夠使函數(shù)組件更強大、更靈活的“鉤子”
React-Hooks 是什么?它是一套能夠使函數(shù)組件更強大、更靈活的“鉤子”。
前面我們已經(jīng)說過,函數(shù)組件比起類組件“少”了很多東西,比如生命周期、對 state 的管理等。這就給函數(shù)組件的使用帶來了非常多的局限性,導(dǎo)致我們并不能使用函數(shù)這種形式,寫出一個真正的全功能的組件。
React-Hooks 的出現(xiàn),就是為了幫助函數(shù)組件補齊這些(相對于類組件來說)缺失的能力。
如果說函數(shù)組件是一臺輕巧的快艇,那么 React-Hooks 就是一個內(nèi)容豐富的零部件箱?!爸匮b戰(zhàn)艦”所預(yù)置的那些設(shè)備,這個箱子里基本全都有,同時它還不強制你全都要,而是允許你自由地選擇和使用你需要的那些能力,然后將這些能力以 Hook(鉤子)的形式“鉤”進你的組件里,從而定制出一個最適合你的“專屬戰(zhàn)艦”。
從核心 API 看 Hooks 的基本形態(tài)
useState():為函數(shù)組件引入狀態(tài)
早期的函數(shù)組件相比于類組件,其一大劣勢是缺乏定義和維護 state 的能力,而 state(狀態(tài))作為 React 組件的靈魂,必然是不可省略的。因此 React-Hooks 在誕生之初,就優(yōu)先考慮了對 state 的支持。useState 正是這樣一個能夠為函數(shù)組件引入狀態(tài)的 API。
函數(shù)組件,真的很輕
在過去,你可能會為了使用 state,不得不去編寫一個類組件(這里我給出一個 Demo,編碼如下所示):
import React, { Component } from "react";
export default class TextButton extends Component {
constructor() {
super();
this.state = {
text: "初始文本"
};
}
changeText = () => {
this.setState(() => {
return {
text: "修改后的文本"
};
});
};
render() {
const { text } = this.state;
return (
<div className="textButton">
<p>{text}</p>
<button onClick={this.changeText}>點擊修改文本</button>
</div>
);
}
}
有了 useState 后,我們就可以直接在函數(shù)組件里引入 state。以下是使用 useState 改造過后的 TextButton 組件:
import React, { useState } from "react";
export default function Button() {
const [text, setText] = useState("初始文本");
function changeText() {
return setText("修改后的文本");
}
return (
<div className="textButton">
<p>{text}</p>
<button onClick={changeText}>點擊修改文本</button>
</div>
);
}
useState 快速上手
從用法上看,useState 返回的是一個數(shù)組,數(shù)組的第一個元素對應(yīng)的是我們想要的那個 state 變量,第二個元素對應(yīng)的是能夠修改這個變量的 API。我們可以通過數(shù)組解構(gòu)的語法,將這兩個元素取出來,并且按照我們自己的想法命名。一個典型的調(diào)用示例如下:
const [state, setState] = useState(initialState);
在這個示例中,我們給自己期望的那個狀態(tài)變量命名為 state,給修改 state 的 API 命名為 setState。useState 中傳入的 initialState 正是 state 的初始值。后續(xù)我們可以通過調(diào)用 setState,來修改 state 的值,像這樣:
setState(newState)
狀態(tài)更新后會觸發(fā)渲染層面的更新,這點和類組件是一致的。
這里需要向初學(xué)者強調(diào)的一點是:狀態(tài)和修改狀態(tài)的 API 名都是可以自定義的。比如在上文的 Demo 中,就分別將其自定義為 text 和 setText:
const [text, setText] = useState("初始文本");
“set + 具體變量名”這種命名形式,可以幫助我們快速地將 API 和它對應(yīng)的狀態(tài)建立邏輯聯(lián)系。
當我們在函數(shù)組件中調(diào)用 React.useState 的時候,實際上是給這個組件關(guān)聯(lián)了一個狀態(tài)——注意,是“一個狀態(tài)”而不是“一批狀態(tài)”。這一點是相對于類組件中的 state 來說的。在類組件中,我們定義的 state 通常是一個這樣的對象,如下所示:
this.state {
text: "初始文本",
length: 10000,
author: ["xiuyan", "cuicui", "yisi"]
}
這個對象是“包容萬物”的:整個組件的狀態(tài)都在 state 對象內(nèi)部做收斂,當我們需要某個具體狀態(tài)的時候,會通過 this.state.xxx 這樣的訪問對象屬性的形式來讀取它。
而在 useState 這個鉤子的使用背景下,state 就是單獨的一個狀態(tài),它可以是任何你需要的 JS 類型。像這樣:
// 定義為數(shù)組
const [author, setAuthor] = useState(["xiuyan", "cuicui", "yisi"]);
// 定義為數(shù)值
const [length, setLength] = useState(100);
// 定義為字符串
const [text, setText] = useState("初始文本")
你還可以定義為布爾值、對象等,都是沒問題的。它就像類組件中 state 對象的某一個屬性一樣,對應(yīng)著一個單獨的狀態(tài),允許你存儲任意類型的值。
useEffect():允許函數(shù)組件執(zhí)行副作用操作
函數(shù)組件相比于類組件來說,最顯著的差異就是 state 和生命周期的缺失。useState 為函數(shù)組件引入了 state,而 useEffect 則在一定程度上彌補了生命周期的缺席。
useEffect 能夠為函數(shù)組件引入副作用。過去我們習(xí)慣放在 componentDidMount、componentDidUpdate 和 componentWillUnmount 三個生命周期里來做的事,現(xiàn)在可以放在 useEffect 里來做,比如操作 DOM、訂閱事件、調(diào)用外部 API 獲取數(shù)據(jù)等。
useEffect 和生命周期函數(shù)之間的“替換”關(guān)系
我們可以通過下面這個例子來理解 useEffect 和生命周期函數(shù)之間的替換關(guān)系。這里我先給到你一個用 useEffect 編寫的函數(shù)組件示例:
// 注意 hook 在使用之前需要引入
import React, { useState, useEffect } from 'react';
// 定義函數(shù)組件
function IncreasingTodoList() {
// 創(chuàng)建 count 狀態(tài)及其對應(yīng)的狀態(tài)修改函數(shù)
const [count, setCount] = useState(0);
// 此處的定位與 componentDidMount 和 componentDidUpdate 相似
useEffect(() => {
// 每次 count 增加時,都增加對應(yīng)的待辦項
const todoList = document.getElementById("todoList");
const newItem = document.createElement("li");
newItem.innerHTML = `我是第${count}個待辦項`;
todoList.append(newItem);
});
// 編寫 UI 邏輯
return (
<div>
<p>當前共計 {count} 個todo Item</p>
<ul id="todoList"></ul>
<button onClick={() => setCount(count + 1)}>點我增加一個待辦項</button>
</div>
);
}
通過上面這段代碼構(gòu)造出來的界面在剛剛掛載完畢時,就是如下圖所示的樣子:

IncreasingTodoList 是一個只允許增加 item 的 ToDoList(待辦事項列表)。按照 useEffect 的設(shè)定,每當我們點擊“點我增加一個待辦項”這個按鈕,驅(qū)動 count+1 的同時,DOM 結(jié)構(gòu)里也會被追加一個 li 元素。以下是連擊按鈕三次之后的效果圖:

同樣的效果,按照注釋里的提示,我們也可以通過編寫 class 組件來實現(xiàn):
import React from 'react';
// 定義類組件
class IncreasingTodoList extends React.Component {
// 初始化 state
state = { count: 0 }
// 此處調(diào)用上個 demo 中 useEffect 中傳入的函數(shù)
componentDidMount() {
this.addTodoItem()
}
// 此處調(diào)用上個 demo 中 useEffect 中傳入的函數(shù)
componentDidUpdate() {
this.addTodoItem()
}
// 每次 count 增加時,都增加對應(yīng)的待辦項
addTodoItem = () => {
const { count } = this.state
const todoList = document.getElementById("todoList")
const newItem = document.createElement("li")
newItem.innerHTML = `我是第${count}個待辦項`
todoList.append(newItem)
}
// 定義渲染內(nèi)容
render() {
const { count } = this.state
return (
<div>
<p>當前共計 {count} 個todo Item</p>
<ul id="todoList"></ul>
<button
onClick={() =>
this.setState({
count: this.state.count + 1,
})
}
>
點我增加一個待辦項
</button>
</div>
)
}
}
通過這樣一個對比,類組件生命周期和函數(shù)組件 useEffect 之間的轉(zhuǎn)換關(guān)系可以說是躍然紙上了。
在這里,我提個醒:初學(xué) useEffect 時,我們難免習(xí)慣于借助對生命周期的理解來推導(dǎo)對 useEffect 的理解。但長期來看,若是執(zhí)著于這個學(xué)習(xí)路徑,無疑將阻礙你真正從心智模式的層面擁抱 React-Hooks。
有時候,我們必須學(xué)會忘記舊的知識,才能夠更好地擁抱新的知識。對于每一個學(xué)習(xí) useEffect 的人來說,生命周期到 useEffect 之間的轉(zhuǎn)換關(guān)系都不是最重要的,最重要的是在腦海中構(gòu)建一個“組件有副作用 → 引入 useEffect”這樣的條件反射——當你真正拋卻類組件帶給你的刻板印象、擁抱函數(shù)式編程之后,想必你會更加認同“useEffect 是用于為函數(shù)組件引入副作用的鉤子”這個定義。
useEffect 快速上手
useEffect 可以接收兩個參數(shù),分別是回調(diào)函數(shù)與依賴數(shù)組,如下面代碼所示:
useEffect(callBack, [])
useEffect 用什么姿勢來調(diào)用,本質(zhì)上取決于你想用它來達成什么樣的效果。下面我們就以效果為線索,簡單介紹 useEffect 的調(diào)用規(guī)則。
每一次渲染后都執(zhí)行的副作用:
傳入回調(diào)函數(shù),不傳依賴數(shù)組。調(diào)用形式如下所示
useEffect(callBack)
僅在掛載階段執(zhí)行一次的副作用:
傳入回調(diào)函數(shù),且這個函數(shù)的返回值不是一個函數(shù),同時傳入一個空數(shù)組。調(diào)用形式如下所示:
useEffect(()=>{
// 這里是業(yè)務(wù)邏輯
}, [])
僅在掛載階段和卸載階段執(zhí)行的副作用:傳入回調(diào)函數(shù),且這個函數(shù)的返回值是一個函數(shù),同時傳入一個空數(shù)組。假如回調(diào)函數(shù)本身記為 A, 返回的函數(shù)記為 B,那么將在掛載階段執(zhí)行 A,卸載階段執(zhí)行 B。調(diào)用形式如下所示:
useEffect(()=>{
// 這里是 A 的業(yè)務(wù)邏輯
// 返回一個函數(shù)記為 B
return ()=>{
}
}, [])
這里需要注意,這種調(diào)用方式之所以會在卸載階段去觸發(fā) B 函數(shù)的邏輯,是由 useEffect 的執(zhí)行規(guī)則決定的:useEffect 回調(diào)中返回的函數(shù)被稱為“清除函數(shù)”,當 React 識別到清除函數(shù)時,會在卸載時執(zhí)行清除函數(shù)內(nèi)部的邏輯。這個規(guī)律不會受第二個參數(shù)或者其他因素的影響,只要你在 useEffect 回調(diào)中返回了一個函數(shù),它就會被作為清除函數(shù)來處理。
每一次渲染都觸發(fā),且卸載階段也會被觸發(fā)的副作用:傳入回調(diào)函數(shù),且這個函數(shù)的返回值是一個函數(shù),同時不傳第二個參數(shù)。如下所示:
useEffect(()=>{
// 這里是 A 的業(yè)務(wù)邏輯
// 返回一個函數(shù)記為 B
return ()=>{
}
})
上面這段代碼就會使得 React 在每一次渲染都去觸發(fā) A 邏輯,并且在卸載階段去觸發(fā) B 邏輯。
其實你只要記住,如果你有一段副作用邏輯需要在卸載階段執(zhí)行,那么把它寫進 useEffect 回調(diào)的返回函數(shù)(上面示例中的 B 函數(shù))里就行了。也可以認為,這個 B 函數(shù)的角色定位就類似于生命周期里 componentWillUnmount 方法里的邏輯
根據(jù)一定的依賴條件來觸發(fā)的副作用:傳入回調(diào)函數(shù)(若返回值是一個函數(shù),仍然僅影響卸載階段對副作用的處理,此處不再贅述),同時傳入一個非空的數(shù)組,如下所示:
useEffect(()=>{
// 這是回調(diào)函數(shù)的業(yè)務(wù)邏輯
// 若 xxx 是一個函數(shù),則 xxx 會在組件卸載時被觸發(fā)
return xxx
}, [num1, num2, num3])
這里我給出的一個示意數(shù)組是 [num1, num2, num3]。首先需要說明,數(shù)組中的變量一般都是來源于組件本身的數(shù)據(jù)(props 或者 state)。若數(shù)組不為空,那么 React 就會在新的一次渲染后去對比前后兩次的渲染,查看數(shù)組內(nèi)是否有變量發(fā)生了更新(只要有一個數(shù)組元素變了,就會被認為更新發(fā)生了),并在有更新的前提下去觸發(fā) useEffect 中定義的副作用邏輯。
Hooks 是如何幫助我們升級工作模式的
函數(shù)組件相比類組件來說,有著不少能夠利好 React 組件開發(fā)的特性,而 React-Hooks 的出現(xiàn)正是為了強化函數(shù)組件的能力?,F(xiàn)在,基于對 React-Hooks 編碼層面的具體認知,想必你對“動機”的理解也已經(jīng)上了一個臺階。這里我們就趁熱打鐵,針對“Why React-Hooks”這個問題,做一個加強版的總結(jié)。
相信有不少嗅覺敏銳的同學(xué)已經(jīng)感覺到了——沒錯,這個環(huán)節(jié)就是手把手教你做“為什么需要 React-Hooks”這道面試題。以“Why xxx”開頭的這種面試題,往往都沒有標準答案,但會有一些關(guān)鍵的“點”,只要能答出關(guān)鍵的點,就足以證明你思考的方向是正確的,也就意味著這道題能給你加分。這里,我梳理了以下 4 條答題思路:
告別難以理解的 Class; 解決業(yè)務(wù)邏輯難以拆分的問題; 使狀態(tài)邏輯復(fù)用變得簡單可行; 函數(shù)組件從設(shè)計思想上來看,更加契合 React 的理念。
關(guān)于思路 4,我在上個課時已經(jīng)講得透透的了,這里我主要是借著代碼的東風(fēng),把 1、2、3 攤開來給你看一下。
1. 告別難以理解的 Class:把握 Class 的兩大“痛點”
坊間總有傳言說 Class 是“難以理解”的,這個說法的背后是 this 和生命周期這兩個痛點。
先來說說 this,在上個課時,你已經(jīng)初步感受了一把 this 有多么難以捉摸。但那畢竟是個相對特殊的場景,更為我們所熟悉的,可能還是 React 自定義組件方法中的 this??纯聪旅孢@段代碼:
class Example extends Component {
state = {
name: 'test',
age: '99';
};
changeAge() {
// 這里會報錯
this.setState({
age: '100'
});
}
render() {
return <button onClick={this.changeAge}>{this.state.name}的年齡是{this.state.age}</button>
}
}
你先不用關(guān)心組件具體的邏輯,就看 changeAge 這個方法:它是 button 按鈕的事件監(jiān)聽函數(shù)。當我點擊 button 按鈕時,希望它能夠幫我修改狀態(tài),但事實是,點擊發(fā)生后,程序會報錯。原因很簡單,changeAge 里并不能拿到組件實例的 this
為了解決 this 不符合預(yù)期的問題,各路前端也是各顯神通,之前用 bind、現(xiàn)在推崇箭頭函數(shù)。但不管什么招數(shù),本質(zhì)上都是在用實踐層面的約束來解決設(shè)計層面的問題。好在現(xiàn)在有了 Hooks,一切都不一樣了,我們可以在函數(shù)組件里放飛自我(畢竟函數(shù)組件是不用關(guān)心 this 的)哈哈,解放啦
至于生命周期,它帶來的麻煩主要有以下兩個方面:
學(xué)習(xí)成本 不合理的邏輯規(guī)劃方式
對于第一點,大家都學(xué)過生命周期,都懂。下面著重說說這“不合理的邏輯規(guī)劃方式”是如何被 Hooks 解決掉的。
2. Hooks 如何實現(xiàn)更好的邏輯拆分
在過去,你是怎么組織自己的業(yè)務(wù)邏輯的呢?我想多數(shù)情況下應(yīng)該都是先想清楚業(yè)務(wù)的需要是什么樣的,然后將對應(yīng)的業(yè)務(wù)邏輯拆到不同的生命周期函數(shù)里去——沒錯,邏輯曾經(jīng)一度與生命周期耦合在一起。
在這樣的前提下,生命周期函數(shù)常常做一些奇奇怪怪的事情:比如在 componentDidMount 里獲取數(shù)據(jù),在 componentDidUpdate 里根據(jù)數(shù)據(jù)的變化去更新 DOM 等。如果說你只用一個生命周期做一件事,那好像也還可以接受,但是往往在一個稍微成規(guī)模的 React 項目中,一個生命周期不止做一件事情。下面這段偽代碼就很好地詮釋了這一點:
componentDidMount() {
// 1. 這里發(fā)起異步調(diào)用
// 2. 這里從 props 里獲取某個數(shù)據(jù),根據(jù)這個數(shù)據(jù)更新 DOM
// 3. 這里設(shè)置一個訂閱
// 4. 這里隨便干點別的什么
// ...
}
componentWillUnMount() {
// 在這里卸載訂閱
}
componentDidUpdate() {
// 1. 在這里根據(jù) DidMount 獲取到的異步數(shù)據(jù)更新 DOM
// 2. 這里從 props 里獲取某個數(shù)據(jù),根據(jù)這個數(shù)據(jù)更新 DOM(和 DidMount 的第2步一樣)
}
像這樣的生命周期函數(shù),它的體積過于龐大,做的事情過于復(fù)雜,會給閱讀和維護者帶來很多麻煩。最重要的是,這些事情之間看上去毫無關(guān)聯(lián),邏輯就像是被“打散”進生命周期里了一樣。比如,設(shè)置訂閱和卸載訂閱的邏輯,雖然它們在邏輯上是有強關(guān)聯(lián)的,但是卻只能被分散到不同的生命周期函數(shù)里去處理,這無論如何也不能算作是一個非常合理的設(shè)計。
而在 Hooks 的幫助下,我們完全可以把這些繁雜的操作按照邏輯上的關(guān)聯(lián)拆分進不同的函數(shù)組件里:我們可以有專門管理訂閱的函數(shù)組件、專門處理 DOM 的函數(shù)組件、專門獲取數(shù)據(jù)的函數(shù)組件等。Hooks 能夠幫助我們實現(xiàn)業(yè)務(wù)邏輯的聚合,避免復(fù)雜的組件和冗余的代碼。
3. 狀態(tài)復(fù)用:Hooks 將復(fù)雜的問題變簡單
過去我們復(fù)用狀態(tài)邏輯,靠的是 HOC(高階組件)和 Render Props 這些組件設(shè)計模式,這是因為 React 在原生層面并沒有為我們提供相關(guān)的途徑。但這些設(shè)計模式并非萬能,它們在實現(xiàn)邏輯復(fù)用的同時,也破壞著組件的結(jié)構(gòu),其中一個最常見的問題就是“嵌套地獄”現(xiàn)象。
Hooks 可以視作是 React 為解決狀態(tài)邏輯復(fù)用這個問題所提供的一個原生途徑?,F(xiàn)在我們可以通過自定義 Hook,達到既不破壞組件結(jié)構(gòu)、又能夠?qū)崿F(xiàn)邏輯復(fù)用的效果。
保持清醒:Hooks 并非萬能
盡管我們已經(jīng)說了這么多 Hooks 的“好話”,盡管 React 團隊已經(jīng)用腳投票表明了對函數(shù)組件的積極態(tài)度,但我們還是要謹記這樣一個基本的認知常識:事事無絕對,凡事皆有兩面性。更何況 React 僅僅是推崇函數(shù)組件,并沒有“拉踩”類組件,甚至還官宣了“類組件和函數(shù)組件將繼續(xù)共存”這件事情。這些都在提醒我們,在認識到 Hooks 帶來的利好的同時,還需要認識到它的局限性。
關(guān)于 Hooks 的局限性,目前社區(qū)鮮少有人討論。這里我想結(jié)合團隊開發(fā)過程當中遇到的一些瓶頸,和你分享實踐中的幾點感受:
Hooks 暫時還不能完全地為函數(shù)組件補齊類組件的能力:比如 getSnapshotBeforeUpdate、componentDidCatch 這些生命周期,目前都還是強依賴類組件的 “輕量”幾乎是函數(shù)組件的基因,這可能會使它不能夠很好地消化“復(fù)雜”:我們有時會在類組件中見到一些方法非常繁多的實例,如果用函數(shù)組件來解決相同的問題,業(yè)務(wù)邏輯的拆分和組織會是一個很大的挑戰(zhàn)。我個人的感覺是,從頭到尾都在“過于復(fù)雜”和“過度拆分”之間搖擺不定,哈哈。耦合和內(nèi)聚的邊界,有時候真的很難把握,函數(shù)組件給了我們一定程度的自由,卻也對開發(fā)者的水平提出了更高的要求。 Hooks 在使用層面有著嚴格的規(guī)則約束:對于如今的 React 開發(fā)者來說,如果不能牢記并踐行 Hooks 的使用原則,如果對 Hooks 的關(guān)鍵原理沒有扎實的把握,很容易把自己的 React 項目搞成大型車禍現(xiàn)場。
最后


“分享、點贊、在看” 支持一波
