看完這篇文章,希望你可以從整體上對 Hooks 有個認識,并對其設計哲學有一些理解
React組件設計理論
React以一種全新的編程范式定義了前端開發約束,它為視圖開發帶來了一種全新的心智模型:
- React認為,UI視圖是數據的一種視覺映射,即UI = F(DATA),這里的F需要負責對輸入數據進行加工、并對數據的變更做出響應
- 公式里的F在React里抽象成組件,React是以組件(Component-Based)為粒度編排應用的,組件是代碼復用的最小單元
- 在設計上,React采用props屬性來接收外部的數據,使用state屬性來管理組件自身產生的數據(狀態),而為了實現(運行時)對數據變更做出響應需要,React采用基于類(Class)的組件設計!
除此之外,React認為組件是有生命周期的,因此開創性地將生命周期的概念引入到了組件設計,從組件的create到destory提供了一系列的API供開發者使用
這就是React組件設計的理論基礎,我們最熟悉的React組件一般長這樣:
// React基于Class設計組件
class MyConponent extends React.Component {
// 組件自身產生的數據
state = {
counts: 0
}
// 響應數據變更
clickHandle = () => {
this.setState({ counts: this.state.counts++ });
if (this.props.onClick) this.props.onClick();
}
// lifecycle API
componentWillUnmount() {
console.log('Will mouned!');
}
// lifecycle API
componentDidMount() {
console.log('Did mouned!');
}
// 接收外來數據(或加工處理),并編排數據在視覺上的呈現
render(props) {
return (
<>
<div>Input content: {props.content}, btn click counts: {this.state.counts}</div>
<button onClick={this.clickHandle}>Add</button>
</>
);
}
}
Class Component的問題
組件復用困局
組件并不是單純的信息孤島,組件之間是可能會產生聯系的,一方面是數據的共享,另一個是功能的復用:
- 對于組件之間的數據共享問題,React官方采用單向數據流(Flux)來解決
- 對于(有狀態)組件的復用,React團隊給出過許多的方案,早期使用CreateClass + Mixins,在使用Class Component取代CreateClass之后又設計了Render Props和Higher Order Component,直到再后來的Function Component+ Hooks設計,React團隊對于組件復用的探索一直沒有停止
HOC使用(老生常談)的問題:
- 嵌套地獄,每一次HOC調用都會產生一個組件實例
- 可以使用類裝飾器緩解組件嵌套帶來的可維護性問題,但裝飾器本質上還是HOC
- 包裹太多層級之后,可能會帶來props屬性的覆蓋問題
Render Props:
- 數據流向更直觀了,子孫組件可以很明確地看到數據來源
- 但本質上Render Props是基于閉包實現的,大量地用于組件的復用將不可避免地引入了callback hell問題
- 丟失了組件的上下文,因此沒有this.props屬性,不能像HOC那樣訪問this.props.children
Javascript Class的缺陷
this的指向(語言缺陷)
class People extends Component {
state = {
name: 'dm',
age: 18,
}
handleClick(e) {
// 報錯!
console.log(this.state);
}
render() {
const { name, age } = this.state;
return (<div onClick={this.handleClick}>My name is {name}, i am {age} years old.</div>);
}
}
createClass不需要處理this的指向,到了Class Component稍微不慎就會出現因this的指向報錯。
編譯大小(還有性能)問題
// Class Component
class App extends Component {
state = {
count: 0
}
componentDidMount() {
console.log('Did mount!');
}
increaseCount = () => {
this.setState({ count: this.state.count + 1 });
}
decreaseCount = () => {
this.setState({ count: this.state.count - 1 });
}
render() {
return (
<>
<h1>Counter</h1>
<div>Current count: {this.state.count}</div>
<p>
<button onClick={this.increaseCount}>Increase</button>
<button onClick={this.decreaseCount}>Decrease</button>
</p>
</>
);
}
}
// Function Component
function App() {
const [ count, setCount ] = useState(0);
const increaseCount = () => setCount(count + 1);
const decreaseCount = () => setCount(count - 1);
useEffect(() => {
console.log('Did mount!');
}, []);
return (
<>
<h1>Counter</h1>
<div>Current count: {count}</div>
<p>
<button onClick={increaseCount}>Increase</button>
<button onClick={decreaseCount}>Decrease</button>
</p>
</>
);
}
Class Component編譯結果(Webpack):
var App_App = function (_Component) {
Object(inherits["a"])(App, _Component);
function App() {
var _getPrototypeOf2;
var _this;
Object(classCallCheck["a"])(this, App);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = Object(possibleConstructorReturn["a"])(this, (_getPrototypeOf2 = Object(getPrototypeOf["a"])(App)).call.apply(_getPrototypeOf2, [this].concat(args)));
_this.state = {
count: 0
};
_this.increaseCount = function () {
_this.setState({
count: _this.state.count + 1
});
};
_this.decreaseCount = function () {
_this.setState({
count: _this.state.count - 1
});
};
return _this;
}
Object(createClass["a"])(App, [{
key: "componentDidMount",
value: function componentDidMount() {
console.log('Did mount!');
}
}, {
key: "render",
value: function render() {
return react_default.a.createElement(/*...*/);
}
}]);
return App;
}(react["Component"]);
Function Component編譯結果(Webpack):
function App() {
var _useState = Object(react["useState"])(0),
_useState2 = Object(slicedToArray["a" /* default */ ])(_useState, 2),
count = _useState2[0],
setCount = _useState2[1];
var increaseCount = function increaseCount() {
return setCount(count + 1);
};
var decreaseCount = function decreaseCount() {
return setCount(count - 1);
};
Object(react["useEffect"])(function () {
console.log('Did mount!');
}, []);
return react_default.a.createElement();
}
- Javascript實現的類本身比較雞肋,沒有類似Java/C++多繼承的概念,類的邏輯復用是個問題
- Class Component在React內部是當做Javascript Function類來處理的
- Function Component編譯后就是一個普通的function,function對js引擎是友好的
Function Component缺失的功能
不是所有組件都需要處理生命周期,在React發布之初Function Component被設計了出來,用于簡化只有render時Class Component的寫法。
Function Component是純函數,利于組件復用和測試
Function Component的問題是只是單純地接收props、綁定事件、返回jsx,本身是無狀態的組件,依賴props傳入的handle來響應數據(狀態)的變更,所以Function Component不能脫離Class Comnent來存在!
function Child(props) {
const handleClick = () => {
this.props.setCounts(this.props.counts);
};
// UI的變更只能通過Parent Component更新props來做到!!
return (
<>
<div>{this.props.counts}</div>
<button onClick={handleClick}>increase counts</button>
</>
);
}
class Parent extends Component() {
// 狀態管理還是得依賴Class Component
counts = 0
render () {
const counts = this.state.counts;
return (
<>
<div>sth...</div>
<Child counts={counts} setCounts={(x) => this.setState({counts: counts++})} />
</>
);
}
}
所以,Function Comonent是否能脫離Class Component獨立存在,關鍵在于讓Function Comonent自身具備狀態處理能力,即在組件首次render之后,“組件自身能夠通過某種機制再觸發狀態的變更并且引起re-render”,而這種“機制”就是Hooks!
Hooks的出現彌補了Function Component相對于Class Component的不足,讓Function Component取代Class Component成為可能。
Function Component + Hooks組合
1、功能相對獨立、和render無關的部分,可以直接抽離到hook實現,比如請求庫、登錄態、用戶核身、埋點等等,理論上裝飾器都可以改用hook實現(如react-use,提供了大量從UI、動畫、事件等常用功能的hook實現)。
case:Popup組件依賴視窗寬度適配自身顯示寬度、相冊組件依賴視窗寬度做單/多欄布局適配
function useWinSize() {
const html = document.documentElement;
const [ size, setSize ] = useState({ width: html.clientWidth, height: html.clientHeight });
useEffect(() => {
const onSize = e => {
setSize({ width: html.clientWidth, height: html.clientHeight });
};
window.addEventListener('resize', onSize);
return () => {
window.removeEventListener('resize', onSize);
};
}, [ html ]);
return size;
}
// 依賴win寬度,適配圖片布局
function Article(props) {
const { width } = useWinSize();
const cls = `layout-${width >= 540 ? 'muti' : 'single'}`;
return (
<>
<article>{props.content}<article>
<div className={cls}>recommended thumb list</div>
</>
);
}
// 彈層寬度根據win寬高做適配
function Popup(props) {
const { width, height } = useWinSize();
const style = {
width: width - 200,
height: height - 300,
};
return (<div style={style}>{props.content}</div>);
}
2、有render相關的也可以對UI和功能(狀態)做分離,將功能放到hook實現,將狀態和UI分離
case:表單驗證
function App() {
const { waiting, errText, name, onChange } = useName();
const handleSubmit = e => {
console.log(`current name: ${name}`);
};
return (
<form onSubmit={handleSubmit}>
<>
Name: <input onChange={onChange} />
<span>{waiting ? "waiting..." : errText || ""}</span>
</>
<p>
<button>submit</button>
</p>
</form>
);
}
React Hooks 的本質
稍微復雜點的項目肯定是充斥著大量的 React 生命周期函數(注意,即使你使用了狀態管理庫也避免不了這個),每個生命周期里幾乎都承擔著某個業務邏輯的一部分,或者說某個業務邏輯是分散在各個生命周期里的。
而 Hooks 的出現本質是把這種面向生命周期編程變成了面向業務邏輯編程,你不用再去關心本不該關心的生命周期。
一個 Hooks 演變
我們先假想一個常見的需求,一個 Modal 里需要展示一些信息,這些信息需要通過 API 獲取且跟 Modal 強業務相關,要求我們:
- 因為業務簡單,沒有引入額外狀態管理庫
- 因為業務強相關,并不想把數據跟組件分開放
- API 數據會隨機變動,因此需要每次打開 Modal 才獲取最新數據
- 為了后期優化,不可以有額外的組件創建和銷毀
我們可能的實現如下:
class RandomUserModal extends React.Component {
constructor(props) {
super(props);
this.state = {
user: {},
loading: false,
};
this.fetchData = this.fetchData.bind(this);
}
componentDidMount() {
if (this.props.visible) {
this.fetchData();
}
}
componentDidUpdate(prevProps) {
if (!prevProps.visible && this.props.visible) {
this.fetchData();
}
}
fetchData() {
this.setState({ loading: true });
fetch('https://randomuser.me/api/')
.then(res => res.json())
.then(json => this.setState({
user: json.results[0],
loading: false,
}));
}
render() {
const user = this.state.user;
return (
<ReactModal
isOpen={this.props.visible}
>
<button onClick={this.props.handleCloseModal}>Close Modal</button>
{this.state.loading ?
<div>loading...</div>
:
<ul>
<li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
<li>Gender: {user.gender}</li>
<li>Phone: {user.phone}</li>
</ul>
}
</ReactModal>
)
}
}
我們抽象了一個包含業務邏輯的 RandomUserModal,該 Modal 的展示與否由父組件控制,因此會傳入參數 visible 和 handleCloseModal(用于 Modal 關閉自己)。
為了實現在 Modal 打開的時候才進行數據獲取,我們需要同時在 componentDidMount 和 componentDidUpdate 兩個生命周期里實現數據獲取的邏輯,而且 constructor 里的一些初始化操作也少不了。
其實我們的要求很簡單:在合適的時候通過 API 獲取新的信息,這就是我們抽象出來的一個業務邏輯,為了這個業務邏輯能在 React 里正確工作,我們需要將其按照 React 組件生命周期進行拆解。這種拆解除了代碼冗余,還很難復用。
下面我們看看采用 Hooks 改造后會是什么樣:
function RandomUserModal(props) {
const [user, setUser] = React.useState({});
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
if (!props.visible) return;
setLoading(true);
fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
setUser(json.results[0]);
setLoading(false);
});
}, [props.visible]);
return (
// View 部分幾乎與上面相同
);
}
很明顯地可以看到我們把 Class 形式變成了 Function 形式,使用了兩個 State Hook 進行數據管理(類比 constructor
),之前 cDM
和 cDU
兩個生命周期里干的事我們直接在一個 Effect Hook 里做了(如果有讀取或修改 DOM 的需求可以看 這里)。做了這些,最大的優勢是代碼精簡,業務邏輯變的緊湊,代碼行數也從 50+ 行減少到 30+ 行。
Hooks 的強大之處還不僅僅是這個,最重要的是這些業務邏輯可以隨意地的的抽離出去,跟普通的函數沒什么區別(僅僅是看起來沒區別),于是就變成了可以復用的自定義 Hook。具體可以看下面的進一步改造:
// 自定義 Hook
function useFetchUser(visible) {
const [user, setUser] = React.useState({});
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
if (!visible) return;
setLoading(true);
fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
setUser(json.results[0]);
setLoading(false);
});
}, [visible]);
return { user, loading };
}
function RandomUserModal(props) {
const { user, loading } = useFetchUser(props.visible);
return (
// 與上面相同
);
}
這里的 useFetchUser 為自定義 Hook,它的地位跟自帶的 useState 等比也沒什么區別,你可以在其它組件里使用,甚至在這個組件里使用兩次,它們會天然地隔離開。
業務邏輯復用
這里說的業務邏輯復用主要是需要跨生命周期的業務邏輯。單單按照組件堆積的形式組織代碼雖然也可以達到各種復用的目的,但是會導致組件非常復雜,數據流也會很亂。組件堆積適合 UI 布局,但是不適合邏輯組織。為了解決這些問題,在 React 發展過程中,產生了很多解決方案,我認知里常見的有以下幾種:
Mixins
壞處遠遠大于帶來的好處,因為現在已經不再支持,不多說,可以看看這篇文章:Mixins Considered Harmful。
Class Inheritance
官方 很不推薦此做法,實際上我也沒真的看到有人這么做。
High-Order Components (HOC)
React 高階組件 在封裝業務組件上簡直是屢試不爽,它的實現是把自己作為一個函數,接受一個組件,再返回一個組件,這樣它可以統一處理掉一些業務邏輯并達到復用目的。
比較常見的一個就是 react-redux
里的 connect
函數:
但是它也被很多人吐槽嵌套問題:
Render Props
Render Props 其實很常見,比如 React Context API:
class App extends React.Component {
render() {
return (
<ThemeProvider>
<ThemeContext.Consumer>
{val => <div>{val}</div>}
</ThemeContext.Consumer>
</ThemeProvider>
)
}
}
它的實現思路很簡單,把原來該放「組件」的地方,換成了回調,這樣當前組件里就可以拿到子組件的狀態并使用。
但是,同樣這會產生 Wrapper Hell 問題:
Hooks
Hooks 本質上面說了,是把面向生命周期編程變成了面向業務邏輯編程,寫法上帶來的優化只是順帶的。
這里,做一個類比,await/async
本質是把 JS 里異步編程思維變成了同步思維,寫法上表現出來的特點就是原來的 Callback Hell 被打平了。
總結對比:
-
await/async
把 Callback Hell 干掉了,異步編程思維變成了同步編程思維 - Hooks 把 Wrapper Hell 干掉了,面向生命周期編程變成了面向業務邏輯編程
這里不得不客觀地說,HOC 和 Render Props 還是有存在的必要,一方面是支持 React Class,另一方面,它們不光適用于純邏輯封裝,很多時候也適合邏輯 + 組件的封裝場景,雖然此時使用 Hooks 也可以,但是會顯得啰嗦點。另外,上面詬病的最大的問題 Wrapper Hell,我個人覺得使用 Fragment 也可以基本解決。
狀態盒子
首先,React Hooks 的設計是反直覺的,為什么這樣說呢?可以先試著問自己:為什么 Hooks 只能在其它 Hooks 的函數或者 React Function 組件里?
在我們的認知里,React 社區一直推崇函數式、純函數等思想,引入 Hooks 概念后的 Functional Component
變的不再純了,useXxx
與其說是一條執行語句,不如說是一個聲明。聲明這里放了一個「狀態盒子」,盒子有輸入和輸出,剩下的內部實現就一無所知,重要的是,盒子是有記憶的,下次執行到此位置時,它有之前上下文信息。
類比「代碼」和「程序」的區別,前者是死的,后者是活的。表達式 c = a + b
表示把 a
和 b
累加后的值賦值給 c
,但是如果寫成 c := a + b
就表示 c
的值由 a
和 b
相加得到。看起來表述差不多,但實際上,后者隱藏著一個時間的維度,它表示的是一種聯系,而不單單是個運算。這在 RxJS 等庫中被大量使用。
這種聲明目前是通過很弱的 use 前綴標識的(但是設計上會簡潔很多),為了不弄錯每個盒子和狀態的對應關系,書寫的時候 Hooks 需要 use 開頭且放在頂層作用域,即不可以包裹 if/switch/when/try 等。如果你按文章開頭引入了那個 ESLint Plugin 就不用擔心會弄錯了。
總結
這篇文章可能并沒有一個很條理的目錄結構,大多是一些個人理解和相關思考。因此,這不能替代你去看真正的文檔了解更多。如果你看完后還是覺得廢話太多,不知所云,那我希望你至少可以在下面幾點上跟作者達成共鳴:
- Hooks 本質是把面向生命周期編程變成了面向業務邏輯編程;
- Hooks 使用上是一個邏輯狀態盒子,輸入輸出表示的是一種聯系;
- Hooks 是 React 的未來,但還是無法完全替代原始的 Class。
參考:
https://zhuanlan.zhihu.com/p/92211533
https://segmentfault.com/a/1190000017182184