React 中提供的 hooks:
useState:setState
useReducer:setState,同时 useState 也是该方法的封装
useRef: ref
useImperativeHandle: 给 ref 分配特定的属性
useContext: context,需配合 createContext 使用
useMemo: 可以对 setState 的优化
useCallback: useMemo 的变形,对函数进行优化
useEffect: 类似 componentDidMount/Update, componentWillUnmount,当效果为 componentDidMount/Update 时,总是在整个更新周期的最后(页面渲染完成后)才执行
useLayoutEffect: 用法与 useEffect 相同,区别在于该方法的回调会在数据更新完成后,页面渲染之前进行,该方法会阻碍页面的渲染
useDebugValue:用于在 React 开发者工具中显示自定义 hook 的标签
一、Hooks 初体验
example:
1 | import React, { useState } from 'react'; |
useState 就是一个 Hook,可以在我们不使用 class 组件的情况下,拥有自身的 state,并且可以通过修改 state 来控制 UI 的展示。
1、useState 状态
语法:
1 | const [state, setState] = useState(initialState) |
- 传入唯一的参数: initialState,可以是数字,字符串等,也可以是对象或者数组。
- 返回的是包含两个元素的数组:第一个元素,state 变量,setState 修改 state 值的方法。
与在类中使用 setState 的异同点:
- 相同点:在一次渲染周期中调用多次 setState,数据只改变一次。
- 不同点:类中的 setState 是合并,而函数组件中的 setState 是替换。
使用对比
之前想要使用组件内部的状态,必须使用 class 组件,例如:
1 | import React, { Component } from 'react'; |
而现在,我们使用函数式组件也可以实现一样的功能了。也就意味着函数式组件内部也可以使用 state 了。
1 | import React, { useState } from 'react'; |
优化
创建初始状态是比较昂贵的,所以我们可以在使用 useState API 时,传入一个函数,就可以避免重新创建忽略的初始状态。
普通的方式:
1 | // 直接传入一个值,在每次 render 时都会执行 createRows 函数获取返回值 |
优化后的方式(推荐):
1 | // createRows 只会被执行一次 |
2、useEffect 执行副作用操作
- effect(副作用):指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生 dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。
- 副作用操作可以分两类:需要清除的和不需要清除的。
- 需要清除的,比如开启的定时器,订阅外部数据源等,这些操作如果在组件消亡后不及时清除会导致内存泄漏。
- 不需要清除的,比如发起网络请求,手动变更 DOM,记录日志等。
- 原先在函数组件内(这里指在 React 渲染阶段)改变 dom 、发送 ajax 请求以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
- useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API
- useEffect 接收一个函数,该函数会在组件渲染到屏幕之后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容
- 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。
语法:
1 | 1、useEffect(() => { doSomething }); |
第一个参数为 effect 函数,该函数将在 componentDidMount 时触发和 componentDidUpdate 时有条件触发(该添加为 useEffect 的第二个数组参数)
第二个参数是可选的,根据条件限制看是否触发
如果不传,如语法 1,则每次页面数据有更新(如 componentDidUpdate),都会触发 effect。
如果为空数组[],如语法 2,则每次初始化的时候只执行一次 effect(如 componentDidMmount)
如果只需要在指定变量变化时触发 effect,将该变量放入数组。如语法 3,count 只要变化,就会执行 effect,如观察者监听
清除副作用
副作用函数还可以通过返回一个函数来指定如何清除副作用,为防止内存泄漏,清除函数会在组件卸载前执行。如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除。
例 1、比如 window.addEventListener(‘resize’, handleResize);:监听 resize 等
1 | useEffect(() => { |
例 2、清除定时器
1 | function Counter(){ |
3、useContext 组件之间传值
语法
1 | const value = useContext(MyContext); |
之前在用类声明组件时,父子组件的传值是通过组件属性和 props 进行的,那现在使用方法(Function)来声明组件,已经没有了 constructor 构造函数也就没有了 props 的接收,但是也可以直接收,如下:
1 | 组件: |
React Hooks 也为我们准备了 useContext。它可以帮助我们跨越组件层级直接传递变量,实现共享。
一:利用 createContext 创建上下文
1 | import React, { useState , createContext } from 'react'; |
二:使用 useContext 获取上下文
对于要接收 context 的后代组件,只需引入 useContext() Hooks 即可。
1 | function Counter(){ |
强调一点:
useContext 的参数必须是 context 对象本身:
- 正确: useContext(MyContext)
- 错误: useContext(MyContext.Consumer)
- 错误: useContext(MyContext.Provider)
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 <MyContext.Provider>
的 context value 值。
4、useReducer 处理更为复杂 state 结构
语法
1 | const [state, dispatch] = useReducer(reducer, initialArg, init); |
useReducer 接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
我们可以使用 useReducer 来重新写我们开篇计数器的 demo:
Example:
1 | import React, { useReducer } from 'react'; |
优化:延迟初始化
还可以懒惰地创建初始状态。为此,您可以将 init 函数作为第三个参数传递。初始状态将设置为 init(initialArg)。
它允许您提取用于计算 reducer 外部的初始状态的逻辑。这对于稍后重置状态以响应操作也很方便:
Example.js
1 | import React, { useReducer } from 'react'; |
与 useState 的区别
- 当 state 状态值结构比较复杂时,使用 useReducer 更有优势。
- 使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。
针对第二点区别,我们可以演示一下: 在上面 useState 用法的例子中,我们新增一个 button:
useState 中的 Example.js
1 | import React, { useState } from 'react'; |
点击 测试能否连加两次 按钮,会发现,点击一次, count 还是只增加了 1,由此可见,useState 确实是 异步 更新数据;
在上面 useReducer 用法的例子中,我们新增一个 button: useReducer 中的 Example.js
1 | import React, { useReducer } from 'react'; |
点击 测试能否连加两次 按钮,会发现,点击一次, count 增加了 2,由此可见,每次 dispatch 一个 action 就会更新一次数据,useReducer 确实是 同步 更新数据;
对于 useReducer 和 useState 的区别主要是以下两点:
- 当 state 状态值结构比较复杂时,使用 useReducer 更有优势。
- 使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。
5、useMemo 性能优化
语法:
1 | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
返回一个 memoized 值。 传递“创建”函数和依赖项数组。useMemo 只会在其中一个依赖项发生更改时重新计算 memoized 值。此优化有助于避免在每个渲染上进行昂贵的计算。
useMemo 在渲染过程中传递的函数会运行。不要做那些在渲染时通常不会做的事情。例如,副作用属于 useEffect,而不是 useMemo。
用法
useMemo 可以帮助我们优化子组件的渲染,比如这种场景: 在 A 组件中有两个子组件 B 和 C,当 A 组件中传给 B 的 props 发生变化时,A 组件状态会改变,重新渲染。此时 B 和 C 也都会重新渲染。其实这种情况是比较浪费资源的,现在我们就可以使用 useMemo 进行优化,B 组件用到的 props 变化时,只有 B 发生改变,而 C 却不会重新渲染。
例子:
ExampleA.js
1 | import React from 'react'; |
ExampleB.js
1 | import React from 'react'; |
App.js
1 | import React, { useState } from 'react'; |
此时我们点击上面任意一个按钮,都会看到控制台打印了两条输出, A 和 B 组件都会被重新渲染。
现在我们使用 useMemo 进行优化
App.js
1 | import React, { useState, useMemo } from 'react'; |
此时我们点击不同的按钮,控制台都只会打印一条输出,改变 a 或者 b,A 和 B 组件都只有一个会重新渲染。
6、useCallback 优化函数式组件性能
语法:
1 | const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); |
返回值 memoizedCallback 是一个 memoized 回调。传递内联回调和一系列依赖项。useCallback 将返回一个回忆的 memoized 版本,该版本仅在其中一个依赖项发生更改时才会更改。当将回调传递给依赖于引用相等性的优化子组件以防止不必要的渲染(例如 shouldComponentUpdate)时,这非常有用。
这个 Hook 的 API 不能够一两句解释的清楚,建议看一下这篇文章:useHooks 第一期:聊聊 hooks 中的 useCallback。里面介绍的比较详细。
7、useRef 获取 dom
语法:
1 | const refContainer = useRef(initialValue); |
类组件、React 元素用 React.createRef
,如:remindRef: any = React.createRef();
通过 this.remindRef.current
获取
函数组件使用 useRef,如let globalToolRef: any = useRef(null);
通过globalToolRef.current
获取
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。
- 从本质上讲,useRef 就像一个“盒子”,可以在其.current 财产中保持一个可变的价值。
- useRef() Hooks 不仅适用于 DOM 引用。 “ref” 对象是一个通用容器,其 current 属性是可变的,可以保存任何值(可以是元素、对象、基本类型、甚至函数),类似于类上的实例属性。
注意:useRef() 比 ref 属性更有用。与在类中使用 instance(实例) 字段的方式类似,它可以 方便地保留任何可变值。
注意,内容更改时 useRef 不会通知您。变异.current 属性不会导致重新渲染。如果要在 React 将引用附加或分离到 DOM 节点时运行某些代码,则可能需要使用回调引用。
使用
下面这个例子中展示了可以在 useRef() 生成的 ref 的 current 中存入元素、字符串
Example.js
1 | import React, { useRef, useState, useEffect } from 'react'; |
点击 在 input 上展示文字 按钮,就可以看到第一个 input 上出现 Hello, useRef;在第二个 input 中输入内容,可以看到控制台打印出对应的内容。
8、useLayoutEffect
语法:
1 | useLayoutEffect(() => { doSomething }); |
与 useEffect Hooks 类似,都是执行副作用操作。但是它是在所有 DOM 更新完成后触发。可以用来执行一些与布局相关的副作用,比如获取 DOM 元素宽高,窗口滚动距离等等。
进行副作用操作时尽量优先选择 useEffect,以免阻止视觉更新。与 DOM 无关的副作用操作请使用 useEffect。
用法
用法与 useEffect 类似。
Example.js
1 | import React, { useRef, useState, useLayoutEffect } from 'react'; |
注意:
- useLayoutEffect 相比 useEffect,通过同步执行状态更新可解决一些特定场景下的页面闪烁问题。
- useEffect 可以满足百分之 99 的场景,而且 useLayoutEffect 会阻塞渲染,请谨慎使用。
- useEffect 在全部渲染完毕后才会执行
- useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行
- 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
- 可以使用它来读取 DOM 布局并同步触发重渲染
- 在浏览器执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新
- 尽可能使用标准的 useEffect 以避免阻塞视图更新
forwardRef
因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
1 | function Parent() { |
- forwardRef 可以在父组件中操作子组件的 ref 对象
- forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上
- 子组件接受 props 和 ref 作为参数
1 | function Child(props,ref){ |
10、useImperativeHandle
- useImperativeHandle 可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛
- 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
- 父组件可以使用操作子组件中的多个 ref
1 | import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react'; |
官网介绍 forwardRef 与 useImperativeHandle 结合使用
11、useMemo 和 useCallback 的使用
useMemo
1 | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
也就是说 useMemo 可以让函数在某个依赖项改变的时候才运行,这可以避免很多不必要的开销。举个例子:
不使用 useMemo
1 | function Example() { |
上面这个组件,维护了两个 state,可以看到 getNum 的计算仅仅跟 count 有关,但是现在无论是 count 还是 val 变化,都会导致 getNum 重新计算,所以这里我们希望 val 修改的时候,不需要再次计算,这种情况下我们可以使用 useMemo。
使用 useMemo
1 | function Example() { |
使用 useMemo 后,并将 count 作为依赖值传递进去,此时仅当 count 变化时才会重新执行 getNum。
useCallback
1 | const memoizedCallback = useCallback( |
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
看起来似乎和 useMemo 差不多,我们来看看这两者有什么异同:
useMemo 和 useCallback 接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的结果,useCallback 返回的是函数。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
使用场景
正如上面所说的,当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。也就是说父组件传递一个函数给子组件的时候,由于父组件的更新会导致该函数重新生成从而传递给子组件的函数引用发生了变化,这就会导致子组件也会更新,而很多时候子组件的更新是没必要的,所以我们可以通过 useCallback 来缓存该函数,然后传递给子组件。举个例子:
1 | function Parent() { |
使用 useCallback 之后,仅当 count 发生变化时 Child 组件才会重新渲染,而 val 变化时,Child 组件是不会重新渲染的。