原创

React Hooks

一、什么是 Hooks

1.Hooks 是 React 16.8 新增的特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

2.React 一直都提倡使用函数组件,但是有时候需要使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有

3.如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以直接在现有的函数组件中使用 Hooks

二、Hooks 解决的问题(类组件的缺点)

1.生命周期复杂,容易写出bug

2.this 指向问题:父组件给子组件传递函数时,必须绑定 this

3.逻辑组件相对难以复用

React 团队希望,组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可。 组件的最佳写法应该是函数,而不是类。

三、Hooks 的优点

1、代码更加简洁 https://static.huhuang.net/blog/image/aaa.png

2、不容易产生bug

这个是类组件的生命周期 https://static.huhuang.net/blog/image/16eacc78d9e9e754.png

这个是hooks的渲染过程

https://static.huhuang.net/blog/image/2419083-2752b426482d181e.webp

四、Hooks 的原理

Hook 这个单词的意思是"钩子"。

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。

你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。

所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 usexxx。

1.函数组件需要解决的问题

(1)内部状态无法持久化

(2)没有生命周期

2.内部状态持久化

hooks采用闭包的方式存储内部状态。

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
type Hook = { memoizedState: any, baseState: any, baseQueue: Update<any, any> | null, queue: UpdateQueue<any, any> | null, next: Hook | null, }; type Update<S, A> = { expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, action: A, eagerReducer: ((S, A) => S) | null, eagerState: S | null, next: Update<S, A>, priority?: ReactPriorityLevel }; type UpdateQueue<S, A> = { pending: Update<S, A> | null, dispatch: (A => mixed) | null, lastRenderedReducer: ((S, A) => S) | null, lastRenderedState: S | null, };

3.hooks渲染过程

React采用自上而下单向数据流的方式,管理自身的数据与状态。在单向数据流中,数据只能由父组件触发,向下传递到子组件。

React中,state与props的改变,都会引发组件重新渲染。如果是父组件的变化,则父组件下所有子组件都会重新渲染。而在函数式组件中,是整个函数重新执行。

每次渲染时都会将hooks组件从头到尾执行一遍。

五、Hooks 的 API

1、useState

每次渲染,函数都会重新执行。我们知道,每当函数执行完毕,所有的内存都会被释放掉。useState就是帮助我们持久化组件内部状态的。

useState利用闭包,在函数内部创建一个当前函数组件的状态。并提供一个修改该状态的方法。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
import { useState } from 'react'; // 利用数组解构的方式接收状态名及其设置方法 // 传入0作为状态 counter的初始值 const [counter, setCounter] = useState(0);

每当setCounter执行,就会改变counter的值。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
import React, { useState } from 'react'; export default function Counter() { const [counter, setCounter] = useState(0); return [ <div key="a">{counter}</div>, <button key="b" onClick={() => setCounter(counter + 1)}> 点击+1 </button> ] }

利用useState声明状态,每当点击时,setCounter执行,counter递增。

需要注意的是,setCounter接收的值可以是任意类型,无论是什么类型,每次赋值,counter得到的,都是新传入setCounter中的值。

举个例子,如果counter是一个引用类型。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
// counter默认值为 { a: 1, b: 2 } const [counter, setCounter] = useState({ a: 1, b: 2 }); // 此时counter的值被改为了 { b: 4 }, 而不是 { a: 1, b: 4 } setCounter({ b: 4 }); // 如果想要得到 { a: 1, b: 4 }的结果,就必须这样 setCounter({ ...counter, b: 4 });

useState接收一个值作为当前定义的state的初始值。并且初始操作只有组件首次渲染才会执行。

              
  • 1
  • 2
  • 3
  • 4
// 首次执行,counter初始值为10 // 再次执行,因为在后面因为某种操作改变了counter,则获取到的便不再是初始值,而是闭包中的缓存值 const [counter, setCounter] = useState(10); setCounter(20);

如果初始值需要通过较为复杂的计算得出,则可以传入一个函数作为参数,函数返回值为初始值。该函数也只会在组件首次渲染时执行一次。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
const a = 10; const b = 20 // 初始值为a、b计算之和 const [counter, setCounter] = useState(() => { return a + b; })

无论是在class中,还是hooks中,state的改变,都是异步的。

状态异步,也就意味着,当你想要在setCounter之后立即去使用它时,你无法拿到状态最新的值,而之后到下一个事件循环周期执行时,状态才是最新的值。

              
  • 1
  • 2
  • 3
const [counter, setCounter] = useState(10); setCounter(20); console.log(counter); // 此时counter的值,并不是20,而是10

实践中有许多的错误使用,因为异步问题而出现bug。

例如我们想要用一个接口,去请求一堆数据,而这个接口接收多个参数。

当改变各种过滤条件,那么就势必会改变传入的参数,并在参数改变时,立即重新去请求一次数据。

利用hooks,会很自然的想到使用如下的方式。

              
  • 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
import React, { useState } from 'react'; interface ListItem { name: string, id: number, thumb: string } // 一堆各种参数 interface Param { current?: number, pageSize?: number, name?: string, id?: number, time?: Date } export default function AsyncDemo() { const [listData, setListData] = useState<ListItem[]>([]); // 定义一个状态缓存参数,确保每次改变后都能缓存完整的参数 const [param, setParam] = useState<Param>({}); function fetchListData() { // @ts-ignore listApi(param).then(res => { setListData(res.data); }) } function searchByName(name: string) { setParam({ ...param, name }); // 改变param之后立即执行请求数据的代码 // 这里的问题是,因为异步的原因,param并不会马上发生变化, // 此时直接发送请求无法拿到最新的参数 fetchListData(); } return [ <div>data list</div>, <button onClick={() => searchByName('Jone')}>search by name</button> ] }

正确的做法

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
export default function AsyncDemo() { const [param] = useState<Param>({}); const [listData, setListData] = useState<ListItem[]>([]); function fetchListData() { // @ts-ignore listApi(param).then(res => { setListData(res.data); }) } function searchByName(name: string) { param.name = name; fetchListData(); } return [ <div>data list</div>, <button onClick={() => searchByName('Jone')}>search by name</button> ] }

2、useEffect

在函数组件中,每当DOM完成一次渲染,都会有对应的副作用执行,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
import React from 'react'; import './style.scss'; export default class effectDemo extends React.Component { constructor(props){ super(props); this.state={ list:[] } this.refresh=this.refresh.bind(this) } componentDidMount(){ this.refresh(); } refresh(){ recordListApi().then(res => { this.setState({ list:res.data }); }) } render(){ return( <div className="container"> <button onClick={this.refresh}>点击刷新 </button> <FlatList data={list} /> </div> ) } }

我们使用useEffect我们可以做到和生命周期一样的功能,但是使用useEffect我们必须抛开之前使用类组件生命周期的思维方式,我们可以利用state改变,函数组件重新渲染,然后副作用执行这一特点,定义一个loading的state,然后改变这个状态,随之相对应的副作用执行从而达到我们的目的。

              
  • 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, { useState, useEffect } from 'react'; import './style.scss'; export default function effectDemo() { const [list, setList] = useState(0); const [loading, setLoading] = useState(true); // DOM渲染完成之后副作用执行 useEffect(() => { if (loading) { recordListApi().then(res => { setList(res.data); setLoading(false) }) } }, [loading]); return ( <div className="container"> <button onClick={() => setLoading(true)}>点击刷新</button> <FlatList data={list} /> </div> ) }

从上面的例子中可以看到我们使用useEffect代替了类组件,实现了我们的目的,并且更加简洁。

本文于   2019/12/23 上午  发布在  Code  分类下,当前已被围观  246  次

相关标签: React

永久地址: https://huhuang.net/article/3

版权声明: 自由转载-署名-非商业性使用   |   Creative Commons BY-NC 3.0 CN