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。
适合本身的,才是最好的。