前言

react学习笔记第四篇,这篇介绍一下hook

为什么要有Hook

在React Hook出现之前的版本中,组件主要分为两种:函数式组件和类组件。其中,函数式组件通常只考虑负责UI的渲染,没有自身的状态也没有业务逻辑代码,是一个纯函数。而类组件则不同,类组件有自己的内部状态,界面的显示结果通常由props 和 state 决定,因此它也不再那么纯净。相比于函数式组件,类组件有如下一些缺点:

  • 状态逻辑难以复用。在类组件中,为了重用某些状态逻辑,社区提出了render props 或者 hoc 等方案,但是这些方案对组件的侵入性太强,并且组件嵌套还容易造成嵌套地狱的问题。
  • 滥用组件状态。大多数开发者在编写组件时,不管这个组件有没有内部状态,会不会执行生命周期函数,都会将组件编写成类组件,这造成不必要的性能开销。
  • 额外的任务处理。使用类组件开发应用时,需要开发者额外去关注 this 、事件监听器的添加和移除等等问题。

在函数式组件大行其道的当前,类组件正在逐渐被淘汰。不过,函数式组件也并非毫无缺点,在之前的写法中,想要管理函数式组件状态共享就是比较麻烦的问题。例如,下面这个函数组件就是一个纯函数,它的输出只由参数props决定,不受其他任何因素影响。

1
2
3
4
5
6
7
8
function App(props) {
const {name, age } = props.info
return (
<div style={{ height: '100%' }}>
<h1>Hello,i am ({name}),and i am ({age}) old</h1>
</div>
)
}

在上面的函数式组件中,一旦我们需要给组件加状态,那就只能将组件重写为类组件,因为函数组件没有实例,没有生命周期

为了解决函数式组件状态的问题,React 在16.8版本新增了Hook特性,可以让开发者在不编写 类(class) 的情况下使用 state 以及其他的 React 特性。HOOK本质上就是一个函数

useState

Hook规则:https://zh-hans.reactjs.org/docs/hooks-rules.html#explanation

useState用于在函数组件中使用状态

  • 函数有一个参数,这个参数的值表示状态的默认值
  • 函数的返回值是一个数组,该数组一定包含两项
    • 第一项:当前状态的值
    • 第二项:改变状态的函数

使用也非常简单

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

export default function App() {
const [n, setN] = useState(0); // 状态的默认值是0
return (
<div>
<button onClick={() => {
setN(n - 1)
}}>-
</button>
<span>{n}</span>
<button onClick={() => {
setN(n + 1)
}}>+
</button>
</div>
);
}

有关它的原理,可以看这几篇文章

从源码剖析useState的执行过程

React Hooks 源码解析:useState

其实概括一下就是,每个react组件对应的虚拟DOM中会存储一个链表,这个链表的节点叫Hook节点,Hook节点中存储了现在的state和更新setState的函数,还有一个queue用于合并多次提交,知道了大致的结构,你就可以理解下面的事项了

  • useState严禁出现在判断或者循环中,因为源数据是用链表维护的,如果运行的次数不一样,React数据的对应关系就会错误

  • useState返回的更新函数,引用不变,这样可以节约内存

  • 使用函数改变数据后,若数据和之前的数据完全相等(使用Object.is比较),不会导致重新渲染,以此优化效率

  • 使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。

  • 如果要实现强制刷新组件

    • 类组件:使用forceUpdate函数

    • 函数组件:使用一个空对象的useState

  • 和类组件的状态一样,函数组件中改变状态可能是异步的(在DOM事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。

比如下面的例子

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

export default function App() {
console.log("App render")
const [n, setN] = useState(0);
return
(
<div>
<button onClick={() => {
setN(prevN => prevN - 1);
setN(prevN => prevN - 1);
}}>-</button>
<span>{n}</span>
<button onClick={() => {
setN(prevN => prevN + 1);
setN(prevN => prevN + 1);
}}>+</button>
</div>
)
}

useEffect

useEffect用于在函数组件中处理副作用

  • 接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数
    • 副作用函数的运行时间点,是在组件重新渲染,页面完成重绘之后。因此它的执行是异步的,不会阻塞浏览器。
    • 类组件中的componentDidMount和componentDidUpdate,已经使用了JS修改了DOM,但是浏览器还没有进行回流和重绘,然后就同步运行了这两个生命周期
  • useEffect中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
    • 该函数运行时间点,在每次运行副作用函数之前
    • 首次渲染组件不会运行
    • 组件被销毁时一定会运行
  • useEffect函数,可以传递第二个参数
    • 第二个参数是一个数组,数组中记录该副作用的依赖数据
    • 当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用(使用Object.is比较所有的数组项)
    • 所以,当传递了依赖数据之后,如果数据没有发生变化
      • 副作用函数仅在第一次渲染后运行
      • 清理函数仅在卸载组件后运行
      • 我们可以用这个来模拟生命周期
  • 注意:副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。

下面的例子演示了怎么使用useEffect

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
import React, { useState, useEffect } from 'react'

export default function App() {
const [n, setN] = useState(0);
const [val, setVal] = useState("");

useEffect(() => {
console.log("改变页面标题的副作用操作, 只在n更新后触发")
document.title = `计数器:${n}`;
}, [n]);
useEffect(() => {
console.log("只在val更新后触发的副作用函数")
}, [val])
useEffect(() => {
console.log("副作用函数, 组件更新就会触发");
})
useEffect(() => {
console.log("组件挂载了");
return function () {
console.log("组件卸载了");
}
}, [])
return (
<div>
<p>{n}</p>
<div>
<button onClick={() => {
setN(n + 1);
}}>+</button>
</div>
<div>
<input type="text" defaultValue={""} onChange={(event) => {
setVal(event.target.value)
}}/>
</div>
</div>
)
}

image-20210202134006106

useContext

这个Hook用于让函数式组件更方便的获取上下文数据

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, { useContext } from 'react'

const ctx = React.createContext();

function Comp() {
const context = useContext(ctx);
return <h1>{context.name}</h1>
}

export default function App() {
return (
<div>
<ctx.Provider value={{name : "sena"}}>
<Comp />
</ctx.Provider>
</div>
)
}

// 以前函数组件使用Context
// function Comp() {
// return <ctx.Consumer>
// {value => <h1>Test,上下文的值:{value}</h1>}
// </ctx.Consumer>
// }

image-20210202153202891

useCallback

用于得到一个固定引用值的函数,通常用它进行性能优化

该函数有两个参数

  • 回调函数,useCallback会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
  • 一个数组,用于记录依赖项,只有依赖项变化了才会重新生成函数(和useEffect一样是使用Object.is比较数组的每一项)
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
import React, { useState, useCallback } from 'react'

class Child extends React.PureComponent {
render() {
console.log("Child Render")
return <div>
<h1>{this.props.text}</h1>
<button onClick={this.props.onClick}>改变文本</button>
</div>
}
}

function Parent() {
console.log("Parent Render")
const [txt, setTxt] = useState(1)
const [n, setN] = useState(0)
const handleClick = useCallback(() => {
setTxt(txt + 1)
}, [txt])

return (
<div>
<Child text={txt} onClick={handleClick} />
<input type="number"
value={n}
onChange={e => {
setN(parseInt(e.target.value))
}}
/>
</div>
)
}

export default function App() {

return (
<div>
<Parent />
</div>
)
}

useMemo

useMemo也是用来固定数据的

该函数有两个参数

  • 一个函数,useMemo会固定该函数的返回值的引用,只要依赖项没有发生变化,则始终返回之前的地址
  • 一个数组,用于记录依赖项,只有依赖项变化了才会重新执行传入的函数生成返回值

和useCallback的区别在于,useMemo可以固定的东西多了,所以一般用于存储一些需要大量计算的结果

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
import React, { useState, useMemo } from 'react'

function Item(props) {
return <li>{props.value}</li>
}


export default function App() {
const [range,] = useState({ min: 1, max: 10000 })
const [n, setN] = useState(0);
// 如果不使用useMemo固定,输入框的值一改变,就会重新渲染组件
// 就要重新循环生成节点,很耗费性能
const list = useMemo(() => {
const list = [];
for (let i = range.min; i <= range.max; i++) {
list.push(<Item key={i} value={i}></Item>)
}
return list;
}, [range.min, range.max]);
return (
<div>
<ul>
{list}
</ul>
<input type="number"
value={n}
onChange={e => {
setN(parseInt(e.target.value))
}}
/>
</div>
)
}

useRef

useRef也是用来固定数据的,不过它可以固定ref

  • useRef有一个参数,代表数据的默认值
  • 它返回一个固定的对象,{current: val}

示例如下

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

export default function App() {
const inpRef = useRef();
const [n, setN] = useState(0)
return (
<div>
<input ref={inpRef} type="text" />
<button onClick={() => {
console.log(inpRef.current.value)
}}>得到input的值</button>
<input type="number"
value={n}
onChange={e => {
setN(e.target.value)
}} />
</div>
)
}
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, useRef, useEffect } from 'react'
export default function App() {
const [n, setN] = useState(10);
// 固定setTimeout的timer
const timerRef = useRef()
useEffect(() => {
if (n === 0) {
return;
}
timerRef.current = setTimeout(() => {
console.log(n)
setN(n - 1)
}, 1000)
return () => {
clearTimeout(timerRef.current);
}
}, [n])
return (
<div>
<h1>{n}</h1>
</div>
)
}

useImperativeHandleHook

这个Hook用于在Hook转发时更方便的设置Ref的值,比如如果你想把Ref设置成一个对象啥的

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
import React, { useRef, useImperativeHandle } from 'react'

function Test(props, ref) {
useImperativeHandle(ref, () => {
// 函数有三个参数,ref和一个函数,函数的返回值会赋给ref.current
// 第三个参数是依赖项
// 如果不给依赖项,则每次运行函数组件都会调用该方法
// 如果使用了依赖项,则第一次调用后,会进行缓存,只有依赖项发生变化时才会重新调用函数
// 相当于给 ref.current = 1
return {
method(){
console.log("Test Component Called")
}
}
}, [])
return <h1>Test Component</h1>
}

const TestWrapper = React.forwardRef(Test)

export default function App() {
const testRef = useRef();
return (
<div>
<TestWrapper ref={testRef} />
<button onClick={() => {
testRef.current.method();
// console.log(testRef)
}}>点击调用Test组件的method方法</button>
</div>
)
}

useLayoutEffectHook

useLayoutEffectHook和useEffect都是React 提供给用户处理副作用的Hook,它们的函数签名是完全一致的

1
2
3
4
5
6
7
useLayoutEffectHook(() => {
// 执行一些副作用
// ...
return () => {
// 清理函数
}
}, [/*依赖数组*/])

那么他们的区别是什么呢

首先我们知道,浏览器中 JS 线程和渲染线程(注意是线程)是互斥的,对于 React 的函数组件来说,它的更新过程大致分为以下步骤:

  1. state改变
  2. React 内部更新 state 变量
  3. React重新调用render函数生成虚拟dom,进行dom diff并用JS操作DOM
  4. 将更新过后的 DOM 数据绘制到浏览器中
  5. 用户看到新的页面

前三步都是 React 在处理,也就是 JS 线程执行我们所写的代码,都是在内存中进行一系列操作,第四步才是真正将更新后数据交给渲染线程进行处理。

而useEffect只会在第五步后才会调用,也就是在浏览器绘制完后,在帧还有空闲的时候才调用,而且 useEffect 还是异步执行的,所谓的异步就是被 React 使用 requestIdleCallback 封装的,只在浏览器空闲时候才会执行,这就保证了不会阻塞浏览器的渲染过程。

而 useLayoutEffect 就不一样,它会在第三第四步之间执行,而且是同步阻塞后面的流程。和类组件的componentDidMount,componentDidUpdate 调用时机是一致的,且都是被 React 同步调用,都会阻塞浏览器渲染。

在某些场景下,useLayoutEffectHook有一些用处

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

export default function App() {
const [n, setN] = useState(0)
const h1Ref = useRef();
useLayoutEffect(() => {
h1Ref.current.innerText = Math.random().toFixed(2);
})
return (
<div>
<h1 ref={h1Ref}>{n}</h1>
<button onClick={() => {
setN(n + 1)
}}>+</button>
</div>
)
}

比如上面的代码,使用useLayoutEffectHook就不会闪屏,而使用useEffect就会闪屏,因为是在重绘后再修改了DOM,要重新绘制,而useLayoutEffect会一起提交所有的修改,就不会闪屏

自定义Hook

自定义Hook是指将一些常用的、跨越多个组件的Hook功能,抽离出去形成一个函数,该函数就是自定义Hook。

举一个简单的例子,如果很多个组件都要请求一个数据,我们就可以把请求数据的操作做成一个Hook

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
import React from 'react'
import { useEffect, useState } from "react"
async function getData() {
return await new Promise((resolve, reject) => {
setTimeout(() =>{
resolve({
name : "sena"
})
}, 1000)
});
}

function useGetData() {
const [data, setData] = useState([])
useEffect(() => {
(async ()=>{
const val = await getData();
setData(val);
})();
}, [])
return data;
}

function Comp() {
const data = useGetData();
return (<p>{JSON.stringify(data)}</p>)
}

export default function App() {
return (
<div>
<Comp/>
</div>
)
}