hooks

React Hooks

Hooks 是 React 16.8 的新特性,它是一些方法,用来增强函数组件的功能,让它有自己的状态,生命周期特性等。之前需要使用 state 或者其他的一些功能只能通过类组件。因为函数组件没有实例,生命周期函数等。

使用它需要注意的地方:

  • 只能在 React 的函数组件中调用

  • 只能在函数内部的最外层调用 Hook,不要在循环、条件判断、或者子函数中使用。

为什么要使用它

一、状态逻辑难复用

React 将页面拆分成多个独立、可复用的组件,并且采取单向数据流的方式传递数据。有些类组件有包含了自己的 state,所以更难复用。之前可以通过 Render PropsHigher-Order Components 解决,但是这两种方式都要求你重新组织你的组件结构。

Render Props(渲染属性):一个组件包含一个 render 输入属性,它是一个方法,并且返回 React element。

1
2
3
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}

class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>

{/*
使用 Render Props 的方式,你可以动态的传入你想要渲染的组件,来复用已有的状态
*/}
{this.props.render(this.state)}
</div>
);
}
}

class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={mouse => ( // 这里传入 render 输入属性,返回 React element
<Cat mouse={mouse} />
)}/>
</div>
);
}
}

Higher-Order Components(高阶组件):高阶组件是一个方法,接受一个组件,并且返回一个新的组件

1
const EnhancedComponent = higherOrderComponent(WrappedComponent);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// CommentList 组件与 BlogPost 组件没写

const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);

// 这个函数接收的第一个参数是个 组件
function withSubscription(WrappedComponent, selectData) {
// 返回另外一个 组件
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}

componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}

componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}

handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}

render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}

上面的这两种方式,解决了组件状态和逻辑复用的问题。但是把原先的组件,包裹了好几层的感觉,层级冗余。

补充一下关于 mixins,它可以用于抽出通用部分,React现在已经不再推荐使用Mixin来解决代码复用问题,因为Mixin带来的危害比他产生的价值还要巨大。

它的危害

  • Mixin 可能会相互依赖,相互耦合,不利于代码维护
  • 不同的Mixin中的方法可能会相互冲突
  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

二、项目复杂难以维护

  • 在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug )

  • 类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件

三、this 指向

用class来创建react组件时,你必须了解 this 的指向问题。在组件中使用,需要通过各种方式绑定 this 来避免问题。

Hooks

一、useReducer

它也是 useState 的替代方案,在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等

1
const [state, dispatch] = useReducer(reducer, initialArg, init);

1)它有两种方式初始化 state

  • 作为 initialArg 传入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
  • init 传入一个函数,延迟创建初始值,可以用来进行复杂的计算,返回最终的结果 init(initialArg)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function init(initialCount) {
return {count: initialCount};
}

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}

function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

2)使用它需要注意的地方:

在 dispatch 触发之后,它是使用 Object.is 来判断新旧值是否相等,如果被判断为相等的话就不会进入调度,所以也就不会更新视图。

1
2
3
4
5
6
7
8
9
// reactFiberHooks.js
...
if (objectIs(eagerState, currentState)) {
// objectIs === Object.is 这个会判断两个值是否是相等的值
// eagerState 新的值
// currentState 旧的值
return;
}
...

煮个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useReducer } from "react";

const initialState = { obj: { count: 1 } };

function reducer(state, action) {
switch (action.type) {
case "change":
initialState.obj.count = 2;
return initialState;// 这里不是返回一个新的值
default:
throw new Error();
}
}

export default function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.obj.count}
<button onClick={() => dispatch({ type: "change" })}>change</button>
</>
);
}

当点击触发 dispatch 的时候,initialState.obj.count 的值被更新为 2,但是因为 Object.is 比较两个值还是相等的,所以直接 return,没有进入任务调度阶段,也就导致视图没更新。

WX20200403-103426

二、useState

通过在函数组件内部调用 useState 来添加变量。它返回一个数组,分别是当前状态的值和一个更新值的方法。这个方法类似于 this.setState。(只是这个方法不会像 this.setState 一样,合并新旧值。对比 useState 与 this.setState。)并且你可以在组件内多次使用 useState。通过数组解构可以给返回的值添加不同的变量名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState } from 'react';

function Example() {
const [count, setCount] = useState(0);
const [fruit, setFruit] = useState('banana');
return (
<div>
<p>You clicked {count} times</p>
<p>{fruit}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

简单了解一下 this.setStateuseState 返回的 dispatch 更新值的方法 的区别

  • useState:它是直接做的值的替换,同时在调用 dispatch 更新方法的时候有对新值和旧值做浅比较,判断是否相等。如果相等就不会进行更新视图。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState } from "react";

function Counter() {
const [info, setInfo] = useState({name:"qhw", age:26});

return (
<div>
<p>{info.name}</p>
<p>{info.age}</p>
<button onClick={() => setInfo({name: "qhw1"})}>Click me</button>
</div>
);
}
export default Counter;
  • this.setState:它是一个合并的过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from "react";

export default class Example extends React.Component {
constructor() {
super();
this.state = {
name:'qhw',
age:"26"
};
}
setInfo = () => {
this.setState({ name: "qhw1"});
this.setState({ name: "qhw2"});
this.setState({ name: "qhw3"});
this.setState({ name: "qhw4"});
this.setState({ name: "qhw5"});
};
render() {
return (
<div>
<p>{this.state.name}</p>
<p>{this.state.age}</p>
<button onClick={() => this.setInfo()}>Click me</button>
</div>
);
}
}

合并过程:

在多次调用 setState 的时候,会生成一条链表,之后通过这条链表来合成出最后的结果。

WX20200415-222022@2x

1
2
3
4
5
6
...
{
...
return _assign({}, prevState, partialState);// prevState 是旧的值,partialState 是新的,循环的通过 _assign 合并对象来得到最后的结果
}
...

三、useEffect

  • 可以将 useEffect 理解为 componentDidMount, componentDidUpdate, 加 componentWillUnmount 的结合。它可以在组件渲染之后做某些事情,React 会记住你传入的方法,在每次渲染的时候都会调用到它,不光光只是第一次渲染时候被调用。

  • 在更新 DOM 的时候需要触发一些代码,比如:网络请求、DOM 操作、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等,这些被称为副作用。副作用也分为需要清除与不需要清除的。

  • useEffect 接收一个函数,这个函数会在组件渲染到屏幕后执行。如果需要清除副作用,那么这个函数应该返回一个新的函数,如果不需要清除,则不用返回任何内容。

1)每次渲染都会调用到 useEffect 传入的副作用函数,而不是只调用一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);

useEffect(() => {// 每次渲染都会调用
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return function cleanup() {// 返回的这个函数在组件 unmounts 的时候被调用,用来清除副作用
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}

2)useEffect 与 useState 一样,都可以支持多次使用,可以拆分多个出来处理不同的业务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
};
});
// ...
}

3)为什么每次渲染都会调用副作用函数

假设有这么个类组件,在 componentDidMount 的时候订阅了事件,使用的 props.friend.id 是 1。如果 props.friend.id 变为了 2,那么在组件调用 componentWillUnmount 钩子的时候,会传入错误的 props.friend.id ,也就会导致 bug 的出现。所以就需要添加 componentDidUpdate 钩子,在组件更新的时候,解除之前的事件订阅、并且重新添加订阅,从而避免 bug 出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}

// 通过在组件更新的时候解除之前的事件订阅、并且重新添加订阅。
componentDidUpdate(prevProps) {
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}

如果在函数组件内使用 useEffect,就不需要考虑上面的问题

1
2
3
4
5
6
7
8
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
4)性能优化,如何跳过一些副作用函数

可以通过在 useEffect 的第二个参数添加一个数组,当数组里的值产生变化的时候,才会调用副作用函数。

1
2
3
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

如果你希望副作用函数和清除副作用,只被调用一次。可以在第二个参数传入一个空数组。它的意思就是告诉 React 它不依赖于任何的 props 或者 state,它也不需要被再次执行。所以 useEffect 就变成了类似于使用 componentDidMountcomponentWillUnmount 的方式。

四、useCallback & useMemo

这两个 hook 分别用于缓存函数 和 缓存函数的返回值

useCallback

接收一个内联的回调函数和数组作为入参,返回该回调函数的 memoized 版本,只有依赖项被改变的时候,才会重新计算 memoized 的值。

1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

煮个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { useState, useCallback, useEffect } from "react";

export default function Example() {
const [count, setCount] = useState(0);
const [age, setAge] = useState(26);

const getFetchUrl = useCallback(() => {
console.log(`count: ${count}`);
}, [count]);

useEffect(() => {
console.log();
getFetchUrl();
}, [getFetchUrl]);

return (
<>
<h1>{count}</h1>
<h1>{age}</h1>
<button onClick={()=>setCount(count + 1)}>setCount</button>
<button onClick={()=>setAge(age + 1)}>setAge</button>
</>
);
}

在第一次渲染之后,点击 setAge 的时候,不会调用到 getFetchUrl 方法和 useEffect 传入的的函数。只有当 count 和 getFetchUrl 发生变化的时候,才会调用它们。

在每次的视图更新过程中,都有调用这个方法组件,并且都调用了 useCallback 方法。它会比较新的依赖项与旧的依赖项是否相等,也是浅比较。如果依赖项相等,就会返回缓存的方法。

WX20200420-222939@2x

在 areHookInputsEqual 方法里遍历新旧依赖项,通过 objectIs 比较是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function areHookInputsEqual(nextDeps, prevDeps) {

...

for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (objectIs(nextDeps[i], prevDeps[i])) {
continue;
}

return false;
}

return true;
}

useMemo

接收一个创建函数与数组作为入参,返回 memoized 值。 只有当数组中的值发生变化的时候,才会重新计算。

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

煮个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useState, useMemo, useEffect } from "react";

export default function Example() {
const [count, setCount] = useState(0);
const [age, setAge] = useState(26);

const data = useMemo(() => {
return {
age
};
}, [age]);

useEffect(() => {
console.log('call');
}, [data]);

return (
<>
<h1>{count}</h1>
<h1>{data.age}</h1>
<button onClick={() => setCount(count + 1)}>setCount</button>
<button onClick={() => setAge(age + 1)}>setAge</button>
</>
);
}

useMemo 和 useCallback 差不多。不过它缓存的一个是值,而不是一个是函数,useMemo 在判断如果不相等的时候,会调用 useMemo 传入的回调,来生成新的值。

WX20200420-234410@2x

使用useMemo的时候需要注意它本身也有开销。useMemo 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo 可能会影响程序的性能。

自定义钩子

自定义钩子就是一个包含 use 开头的方法,你可以自由决定需要传入哪些参数,返回哪些值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});

return isOnline;
}

如何使用自定义的 Hook

1
2
3
4
5
6
7
8
9
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);

return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}

资料

https://reactjs.org/docs/hooks-intro.html

https://juejin.im/post/5be3ea136fb9a049f9121014

https://juejin.im/post/5dbbdbd5f265da4d4b5fe57d

https://juejin.im/post/5ceb36dd51882530be7b1585