React Hooks 正憑藉其 Function Component 的特性,已經在實際項目中被普遍應用,而對於邏輯是重複且可被複用的組件,藉助第三方 React Hooks 庫來加快開發效率無疑是正確的選擇。html
我在 Github 上選取了 3 個 React Hooks 庫,它們分別是:前端
以上庫中,都包含了 useTitle
這個 hook 函數,調用它能改變當前頁面的文檔標題(document.title
),須要注意的是,當調用 useTitle
的組件卸載時,須要將文檔標題還原。react
你能夠先嚐試本身手寫,思考事後,咱們依次來看這三個庫是怎麼進行設計的。webpack
react-use 做爲熱度最高的 hooks 庫,早在 18 年由國外開發者開源,發展至今,包含了大量的處理函數,但質量層次不一,爲何我會這麼說,且看下面分析。git
以 useTitle
爲例,先展現該庫的源碼:github
// src/useTitle.ts
/* eslint-disable */
import { useRef, useEffect } from "react";
export interface UseTitleOptions {
restoreOnUnmount?: boolean;
}
const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = {
restoreOnUnmount: false,
};
function useTitle( title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS ) {
const prevTitleRef = useRef(document.title);
document.title = title;
useEffect(() => {
if (options && options.restoreOnUnmount) {
return () => {
document.title = prevTitleRef.current;
};
} else {
return;
}
}, []);
}
export default typeof document !== "undefined"
? useTitle
: (_title: string) => {};
複製代碼
大體就是 useTitle
在每次調用時,先調用 useRef(document.title)
將初始的 document.title
保存至 prevTitleRef.current
中,隨後修改文檔標題(注意,這是個伏筆)。web
在組件被銷燬時,調用 useEffect
返回的函數,將 document.title
設置成以前保存的標題。數組
有同窗可能會疑惑,爲何能導出一個三元表達式,這是由於 ES Modules 導出的是一個引用,等到真正執行該模塊時,纔會調用三元表達式,從而動態判斷當前應用是否具備 document 對象,具體可查看 利用 webpack 理解 CommonJS 和 ES Modules 的差別 。瀏覽器
爲了更直觀的體驗,我使用 create-react-app 初始化了一個新項目,並安裝 react-use.app
修改 App.js :
import React, { useState } from "react";
import { useTitle } from "react-use";
const Demo = () => {
useTitle("Hello world!", {
restoreOnUnmount: true,
});
return <h1>document.title has changed</h1>;
};
export default () => {
const [showDemo, setShowDemo] = useState(true);
return (
<div> <button onClick={() => setShowDemo(!showDemo)}> {showDemo ? "unmount" : "mount"} </button> {showDemo ? <Demo /> : ""} </div>
);
};
複製代碼
首次加載,顯示 document.title 已被修改(原標題爲 React App,可查看 public/index.html)。
當我點擊按鈕,卸載組件,卻發現標題仍是 Hello world!
這是由於在 index.js
中,使用了嚴格模式:
ReactDOM.render(
<React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ); 複製代碼
當我將包裹在最外層的 <React.StrictMode></React.StrictMode>
註釋後,當組件被卸載時,就能正確顯示初始標題,完整文檔參照 嚴格模式 – React
從概念上講,React 分兩個階段工做:
渲染 階段會肯定須要進行哪些更改,好比 DOM。在此階段,React 調用 render
,而後將結果與上次渲染的結果進行比較。 提交 階段發生在當 React 應用變化時。(對於 React DOM 來講,會發生在 React 插入,更新及刪除 DOM 節點的時候。)在此階段,React 還會調用 componentDidMount
和 componentDidUpdate
之類的生命週期方法。
提交 階段一般會很快,但渲染過程可能很慢。所以,即將推出的 concurrent 模式 (默認狀況下未啓用) 將渲染工做分解爲多個部分,對任務進行暫停和恢復操做以免阻塞瀏覽器。這意味着 React 能夠在提交以前屢次調用渲染階段生命週期的方法,或者在不提交的狀況下調用它們(因爲出現錯誤或更高優先級的任務使其中斷)。
嚴格模式不能自動檢測到你的反作用,但它能夠幫助你發現它們,使它們更具肯定性。經過故意重複調用如下函數來實現的該操做:
也就是說,useTitle
在嚴格模式下,初始化階段和更新階段都會被執行了兩次。
回顧以前的源碼:
function useTitle( title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS ) {
const prevTitleRef = useRef(document.title);
document.title = title;
useEffect(() => {
...
}, []);
}
複製代碼
document.title = title
這個語句具備反作用(side effect),但卻沒包裹在 useEffect()
中,這是不嚴謹的,顯然違背了 React Hooks 的設計初衷。
注意:
這僅適用於開發模式。生產模式下生命週期不會被調用兩次。
ahooks 做爲阿里集團內部沉澱的 Hooks 庫,基於 UI、SideEffect、LifeCycle、State、DOM 等分類提供了經常使用的 Hooks。
話很少上,直接上源碼:
// packages/hooks/src/useTitle/index.ts
import { useEffect, useRef } from "react";
export interface Options {
restoreOnUnmount?: boolean;
}
const DEFAULT_OPTIONS: Options = {
restoreOnUnmount: false,
};
function useTitle(title: string, options: Options = DEFAULT_OPTIONS) {
const titleRef = useRef(document.title);
document.title = title;
useEffect(() => {
if (options && options.restoreOnUnmount) {
return () => {
document.title = titleRef.current;
};
}
}, []);
}
export default typeof document !== "undefined"
? useTitle
: (_title: string) => {};
複製代碼
使人失望的是,代碼幾乎與 react-use 如出一致,因此上一節提到的開發模式下的小 bug,依舊是會存在。
對於相同的 useTitle
,react-use 的首次 commit 時間是 Oct 27, 2018,而 ahooks 是 Jul 5, 2020,你們也就見仁見智(前端重複造輪子的不良風氣 or KPI 驅使的開源)。
react-hooks 是由百度在實際開發過程的基礎上開源的 hooks 工具集合。
這裏想誇誇百度,不愧是技術的「黃埔軍校」,直接上源碼:
// packages/document-title/src/index.ts
import { useEffect } from "react";
export function useDocumentTitle(title: string) {
useEffect(() => {
const previous = document.title;
document.title = title;
return () => {
document.title = previous;
};
}, [title]);
}
複製代碼
代碼很是簡潔,它將 document.title = title
置於 useEffect()
中,避免了反作用產生的影響。
用 previous
常量去保存初始的標題,並在組件卸載時,還原標題。別忘了在 deps 數組中加入 title 變量。
但我我的以爲,下面這種寫法是最好的:
import { useEffect } from "react";
export function useTitle(title: string) {
const prevTitleRef = useRef(title);
useEffect(() => {
document.title = title;
return () => {
document.title = prevTitleRef.current;
};
}, [title]);
}
複製代碼
因爲 useRef
返回的對象存在於當前組件的整個生命週期(The returned object will persist for the full lifetime of the component.),相較於百度的寫法:
因爲在 useEffect 中使用到了 prevTitleRef.current,lint 工具會報 react-hooks/exhaustive-deps 警告。
能夠嘗試使用 // eslint-disable-next-line 註釋。
咱們從一個簡簡單單的 useTitle
,看到了三個庫之間的差距,總之你須要切實來選擇正確的 library,也不要盲目信任 library。
適合本身的,纔是最好的。