使用React Hooks有什么优势?
什么是hooks
hook 是一些可以让你在函数组件里面钩入react state 以及生命周期的特定的函数。
- Hooks 本质是把面向生命周期编程变成了面向业务逻辑编程;
- Hooks 使用上是一个逻辑状态盒子,输入输出表示的是一种联系;
- Hooks 是 React 的未来,但还是无法完全替代原始的 Class。
- 每个 Hook 都为Function Component提供使用 React 状态和生命周期特性的通道。Hooks 不能在Class Component中使用。
疑问
- 为什么只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用?
- 为什么 useEffect 第二个参数是空数组,就相当于 ComponentDidMount ,只会执行一次?
- 自定义的 Hook 是如何影响使用它的函数组件的?
- Capture Value 特性是如何产生的?
class 组件
主要问题
- 在hooks出来之前,常见的代码重用方式是HOCs和render props,这两种方式带来的问题是:你需要解构自己的组件,非常的笨重,同时会带来很深的组件嵌套
- 复杂的组件逻辑:复杂的业务逻辑里面存在各种生命周期,导致代码拆分比较困难,很难复用
- 难理解的class 组件
hooks 的到来:
带组件状态的逻辑很难重用:
class 组件复用主要通过引入render props或higher-order components这样的设计模式。如react-redux提供的connect方法。这种方案不够直观,而且需要改变组件的层级结构,极端情况下会有多个wrapper嵌套调用的情况。
Hooks可以在不改变组件层级关系的前提下,方便的重用带状态的逻辑。也可以自己定义状态组件
复杂组件难于理解:
大量的业务逻辑需要放在componentDidMount和componentDidUpdate等生命周期函数中,而且往往一个生命周期函数中会包含多个不相关的业务逻辑,如日志记录和数据请求会同时放在componentDidMount中。另一方面,相关的业务逻辑也有可能会放在不同的生命周期函数中,如组件挂载的时候订阅事件,卸载的时候取消订阅,就需要同时在componentDidMount和componentWillUnmount中写相关逻辑。
Hooks可以封装相关联的业务逻辑,让代码结构更加清晰。
难于理解的 Class 组件:
JS 中的this关键字让不少人吃过苦头,它的取值与其它面向对象语言都不一样,是在运行时决定的
Hooks可以在不引入 Class 的前提下,使用 React 的各种特性。
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() {
// 打开loading
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>
)
}
}
该 Modal 的展示与否由父组件控制,因此会传入参数 visible 和 handleCloseModal(用于 Modal 关闭自己)。 实现在 Modal 打开的时候才进行数据获取,我们需要同时在 componentDidMount 和 componentDidUpdate 两个生命周期里实现数据获取的逻辑
我们需要将其按照 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 部分几乎与上面相同
);
}
优势是代码精简, 可以通过 自定义 的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 (
// 与上面相同
);
}
useState
useState 是一个hook,它的入参是state 的初始值,返回一个数组,包含当前state 和 用于更改 state 的函数
- class 组件有一个大的state 对象,通过this.setState 一次改变整个state对象
- 函数组件根本没有状态,但useState hook允许我们在需要时添加很小的状态块
React有能力在调用每个组件之前做一些设置,这就是它设置这个状态的时候。
其中做的一件事设置 Hooks 数组。 它开始是空的, 每次调用一个hook时,React 都会向该数组添加该 hook。
假如有这样一个函数
function AudioPlayer() {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);
.....
}
因为它调用useState 3次,React 会在第一次渲染时将这三个 hook 放入 Hooks 数组中。
下次渲染时,同样的3个hooks以相同的顺序被调用,所以React可以查看它的数组,并发现已经在位置0有一个useState hook ,所以React不会创建一个新状态,而是返回现有状态。
1、React 创建组件时,它还没有调用函数。React 创建元数据对象和Hooks的空数组。假设这个对象有一个名为nextHook的属性,它被放到索引为0的位置上,运行的第一个hook将占用位置0。
2、React 调用你的组件(
这意味着它知道存储hooks的元数据对象
)。3、调用useState,React创建一个新的状态,将它放在hooks数组的第0位,返回
[volume,setVolume]
对,并将volume 设置为其初始值80,它还将nextHook索引递增1。4、再次调用useState,React查看数组的第1位,看到它是空的,并创建一个新的状态。 然后它将nextHook索引递增为2,并返回
[position,setPosition]
。5、第三次调用useState。 React看到位置2为空,同样创建新状态,将nextHook递增到3,并返回
[isPlaying,setPlaying]
。
useEffect
你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
默认情况下,它在第一次渲染之后和每次更新之后都会执行。
useEffect做了什么
引用官方文档的例子:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。
- 在渲染的时候被创建,在浏览器绘制之后运行。
- 如果给出了销毁指令,它们将在下一次绘制前被销毁。
- 它们会按照定义的顺序被运行。
渲染函数只是创建了 fiber 节点,但是并没有绘制任何内容。
通常来说,应该是 fiber 保存包含了 effect 节点的队列。每个 effect 节点都是一个不同的类型,并能在适当的状态下被定位到:
hook effect 将会被保存在 fiber 一个称为 updateQueue 的属性上,每个 effect 节点都有如下的结构(详见源码):
type Effect = {
tag: HookEffectTag, // 它控制了 effect 节点的行为
create: () => mixed, // 绘制之后运行的回调函数
destroy: (() => mixed) | null,
inputs: Array<mixed>, // 一个集合,该集合中的值将会决定一个 effect 节点是否应该被销毁或者重新创建。
next: Effect, //它指向下一个定义在函数组件中的 effect 节点
};
export type HookEffectTag = number;
export const NoEffect = /* */ 0b00000000;
export const UnmountSnapshot = /* */ 0b00000010;
export const UnmountMutation = /* */ 0b00000100;
export const MountMutation = /* */ 0b00001000;
export const UnmountLayout = /* */ 0b00010000;
export const MountLayout = /* */ 0b00100000;
export const MountPassive = /* */ 0b01000000;
export const UnmountPassive = /* */ 0b10000000;
// 这个 tag 属性值是由二进制的值组合而成
React 提供了一些特殊的 effect hook:比如 useMutationEffect() 和 useLayoutEffect()。这两个 effect hook 内部都使用了 useEffect(),实际上这就意味着它们创建了 effect hook,但是却使用了不同的 tag 属性值。
react 又是如何检查处罚的呢?
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
const destroy = effect.destroy;
effect.destroy = null;
if (destroy !== null) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
const create = effect.create;
let destroy = create();
if (typeof destroy !== 'function') {
if (__DEV__) {
if (destroy !== null && destroy !== undefined) {
warningWithoutStack(
false,
'useEffect function must return a cleanup function or ' +
'nothing.%s%s',
typeof destroy.then === 'function'
? ' Promises and useEffect(async () => ...) are not ' +
'supported, but you can call an async function inside an ' +
'effect.'
: '',
getStackByFiberInDevAndProd(finishedWork),
);
}
}
destroy = null;
}
effect.destroy = destroy;
}
effect = effect.next;
}
React 组件中有两种常见的副作用:
需要清理的副作用
不需要清理的副作用。
无需清除的effect:
只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。
需要清除的effect:
例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!
使用class
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。
使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用
使用 Hook 的示例:
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分
多个effect:
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
它会在调用一个新的 effect 之前对前一个 effect 进行清理
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 运行第一个 effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 运行下一个 effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 运行下一个 effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect
effect优化
每次渲染后都执行清理或者执行 effect 可能会导致性能问题
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用
useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
useReducer:
useState的替代方案。接受类型为(state,action)=> newState的reducer,并返回与dispatch方法配对的当前状态
const [state, dispatch] = useReducer(reducer, initialArg, init);
有两种不同初始化 useReducer state 的方式
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
初始值也可以延迟初始化, useReducer(reducer, initialCount, init), init 是一个函数, 初始值将设置为init(initialArg)
如果从Reducer Hook返回与当前状态相同的值,则React将退出而不渲染子项或触发效果。
useCallback
useCallback将返回一个回调的memoized(一种优化手段,遇到计算开销很大的函数时,会缓存其计算结果,下次同样的输入就可以直接返回缓存的结果)版本,该版本仅在其中一个依赖项发生更改时才会更改。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
useRef
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染
然而,useRef()
比ref
属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式
useRef这个hooks函数,除了传统的用法之外,它还可以“跨渲染周期”保存数据。
在一个组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state可以在多次渲染之后依旧不变。但是,state的问题在于一旦修改了它就会造成组件的重新渲染。
import React, { useState, useEffect, useMemo, useRef } from 'react';
export default function App(props){
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => {
return 2 * count;
}, [count]);
const timerID = useRef();
useEffect(() => {
timerID.current = setInterval(()=>{
setCount(count => count + 1);
}, 1000);
}, []);
useEffect(()=>{
if(count > 10){
clearInterval(timerID.current);
}
});
// 用ref对象的current属性来存储定时器的ID
return (
<>
<button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
</>
);
}
为什么只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用?
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
// useState无论调用多少次,相互之间是独立的
react是根据useState出现的顺序来定的
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
useState('banana'); //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
useState([{ text: 'Learn Hooks' }]); //...
如果放在循环或者判断里面
let showFruit = true;
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
if(showFruit) {
const [fruit, setFruit] = useState('banana');
showFruit = false;
}
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
// useState('banana');
useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错
这样一来不能确保hooks 的执行顺序一致。
useMemo
我们都知道 react 一个组件的更新,然后下面的字组件都会更新,有一次被问到class 组件中,如果让不需要更新的组件不更新,当时只想起来了 shouldComponentUpdate,他是在重新渲染的过程中触发的,
PureComponent 就是自动为我们加了shouldComponentUpdate 的方法,如果组件的 props 和 state 都没发生改变, render 方法就不会触发
1、 上面提到了 PureComponent来优化 class 组件,
2、 React.memo() (16.6正式发布的)用户函数组件和PureComponent 很相似
hooks引入了useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
记住,传入 useMemo 的函数会在渲染期间执行
不要再这个函数执行的内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴。
不适用useMemo 之前:
import React , {useState,useMemo} from 'react';
function Home(){
const [name, setName] = useState('名称');
const [content,setContent] = useState('内容')
return (
<>
<button onClick={() => setName(new Date().getTime())}>name</button>
<button onClick={() => setContent(new Date().getTime())}>content</button>
<ChildComponent name={name}>{content}</ChildComponent>
</>
)
}
function ChildComponent({name,children}){
function changeName(name){
console.log('触发了changeName')
return name+',改变了name'
}
const _changeName = changeName(name)
return (
<>
<div>{_changeName}</div>
<div>{children}</div>
</>
)
}
// 点击修改内容,changeName也会触发,每次都会执行。如果我们想要name 变化的时候 changeName 才触发。
使用useMemo 优化:
// 父组件不变
function ChildComponent({name,children}){
function changeName(name){
console.log('触发了changeName')
return name+',改变了name'
}
// const _changeName = changeName(name)
const _changeName = useMemo(()=>changeName(name),[name])
return (
<>
<div>{_changeName}</div>
<div>{children}</div>
</>
)
}
// name 变化的时候,changeName 才会触发
useMemo仅在其依赖项数组中的元素发生更改时重新计算值(如果没有依赖项 - 即数组为空,则会在每次调用/呈现时重新计算)。调用该函数不会导致重新渲染。它也在组件的渲染过程中运行,而不是之前