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 的标签
官网地址:https://react-1251415695.cos-website.ap-chengdu.myqcloud.com/docs/hooks-reference.html#basic-hooks
example:
import React, { useState } from 'react';
function Example() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
export default Example;
useState
就是一个 Hook
,可以在我们不使用 class
组件的情况下,拥有自身的 state
,并且可以通过修改 state
来控制 UI 的展示。
语法:
const [state, setState] = useState(initialState)
initialState
,可以是数字,字符串等,也可以是对象或者数组。state
变量,setState
修改 state值的方法。与在类中使用 setState
的异同点:
setState
,数据只改变一次。setState
是合并,而函数组件中的 setState
是替换。使用对比
之前想要使用组件内部的状态,必须使用 class 组件,例如:
import React, { Component } from 'react';
export default class Example extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
You clicked {this.state.count} times
);
}
}
而现在,我们使用函数式组件也可以实现一样的功能了。也就意味着函数式组件内部也可以使用 state 了。
import React, { useState } from 'react';
function Example() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
export default Example;
优化
创建初始状态是比较昂贵的,所以我们可以在使用 useState
API 时,传入一个函数,就可以避免重新创建忽略的初始状态。
普通的方式:
// 直接传入一个值,在每次 render 时都会执行 createRows 函数获取返回值
const [rows, setRows] = useState(createRows(props.count));
优化后的方式(推荐):
// createRows 只会被执行一次
const [rows, setRows] = useState(() => createRows(props.count));
ajax
请求、访问原生dom
元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。需要清除的,比如开启的定时器,订阅外部数据源等,这些操作如果在组件消亡后不及时清除会导致内存泄漏。
不需要清除的,比如发起网络请求,手动变更 DOM,记录日志等。
componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 APIcomponentDidMount
或 componentDidUpdate
不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。语法:
1、useEffect(() => { doSomething });
2、useEffect(() => { doSomething },[]);
3、useEffect(() => { doSomething },[count]);
第一个参数为 effect 函数,该函数将在 componentDidMmount 时触发和 componentDidUpdate 时有条件触发(该添加为useEffect 的第二个数组参数)
如果不传,如语法1,则每次页面数据有更新(如componentDidUpdate),都会触发 effect。
如果为空数组[]
,如语法2,则每次初始化的时候只执行一次effect(如componentDidMmount)
如果只需要在指定变量变化时触发 effect,将该变量放入数组。如语法3,count只要变化,就会执行effect,如观察者监听
清除副作用
副作用函数还可以通过返回一个函数来指定如何清除副作用,为防止内存泄漏,清除函数会在组件卸载前执行。如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除。
例1、比如window.addEventListener('resize', handleResize);:监听resize等
useEffect(() => {
window.addEventListener('resize', handleResize);
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
return (() => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
})
}, [globalRef]);
例2、清除定时器
function Counter(){
let [number,setNumber] = useState(0);
let [text,setText] = useState('');
useEffect(()=>{
let $timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
// useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
// useEffect 在执行副作用函数之前,会先调用上一次返回的函数
// 如果要清除副作用,要么返回一个清除副作用的函数
/* return ()=>{
console.log('destroy effect');
clearInterval($timer);
} */
});
// },[]);//要么在这里传入一个空的依赖项数组,这样就不会去重复执行
return (
<>
setText(event.target.value)}/>
{number}
>
)
}
语法
const value = useContext(MyContext);
之前在用类声明组件时,父子组件的传值是通过组件属性和props进行的,那现在使用方法(Function)来声明组件,已经没有了constructor构造函数也就没有了props的接收,但是也可以直接收,如下:
组件:
接收:
const SwitchList = ({dataList = null, isReverse = false}: any): React.ReactElement => {
//TODO
}
React Hooks 也为我们准备了useContext。它可以帮助我们跨越组件层级直接传递变量,实现共享。
一:利用 createContext 创建上下文
import React, { useState , createContext } from 'react';
// 创建一个 CountContext
const CountContext = createContext()
function Example(){
const [ count , setCount ] = useState(0);
return (
You clicked {count} times
{/* 将 context 传递给 子组件,context 值由value props决定*/}
)
}
export default Example;
二:使用useContext 获取上下文
对于要接收context的后代组件,只需引入 useContext() Hooks 即可。
function Counter(){
const count = useContext(CountContext) //一句话就可以得到count
return ({count}
)
}
强调一点:
useContext 的参数必须是 context 对象本身:
当组件上层最近的
语法
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer 接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
我们可以使用 useReducer 来重新写我们开篇计数器的demo:
Example:
import React, { useReducer } from 'react';
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();
}
}
export default () => {
// 使用 useReducer 函数创建状态 state 以及更新状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
}
优化:延迟初始化
还可以懒惰地创建初始状态。为此,您可以将init函数作为第三个参数传递。初始状态将设置为 init(initialArg)
。
它允许您提取用于计算 reducer
外部的初始状态的逻辑。这对于稍后重置状态以响应操作也很方便:
Example.js
import React, { useReducer } from 'react';
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();
}
}
export default ({initialCount = 0}) => {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
>
);
}
与 useState 的区别
state
状态值结构比较复杂时,使用 useReducer
更有优势。useState
获取的 setState
方法更新数据时是异步的;而使用 useReducer
获取的 dispatch
方法更新数据是同步的。针对第二点区别,我们可以演示一下: 在上面 useState
用法的例子中,我们新增一个 button
:
useState 中的 Example.js
import React, { useState } from 'react';
function Example() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
export default Example;
点击 测试能否连加两次 按钮,会发现,点击一次, count
还是只增加了 1,由此可见,useState
确实是 异步 更新数据;
在上面 useReducer
用法的例子中,我们新增一个 button
: useReducer 中的 Example.js
import React, { useReducer } from 'react';
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();
}
}
export default () => {
// 使用 useReducer 函数创建状态 state 以及更新状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
}
点击 测试能否连加两次 按钮,会发现,点击一次, count
增加了 2,由此可见,每次dispatch 一个 action 就会更新一次数据,useReducer
确实是 同步 更新数据;
对于 useReducer 和 useState的区别主要是以下两点:
语法:
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
import React from 'react';
export default ({ text }) => {
console.log('Example A:', 'render');
return Example A 组件:{ text }
}
ExampleB.js
import React from 'react';
export default ({ text }) => {
console.log('Example B:', 'render');
return Example B 组件:{ text }
}
App.js
import React, { useState } from 'react';
import ExampleA from './ExampleA';
import ExampleB from './ExampleB';
import './App.css';
export default () => {
const [a, setA] = useState('ExampleA');
const [b, setB] = useState('ExampleB');
return (
)
}
此时我们点击上面任意一个按钮,都会看到控制台打印了两条输出, A 和 B 组件都会被重新渲染。
现在我们使用 useMemo
进行优化
App.js
import React, { useState, useMemo } from 'react';
import ExampleA from './ExampleA';
import ExampleB from './ExampleB';
import './App.css';
export default () => {
const [a, setA] = useState('ExampleA');
const [b, setB] = useState('ExampleB');
+ const exampleA = useMemo(() => , [a]);
+ const exampleB = useMemo(() => , [b]);
return (
+ {/*
+ */}
+ { exampleA }
+ { exampleB }
)
}
此时我们点击不同的按钮,控制台都只会打印一条输出,改变 a 或者 b,A 和 B 组件都只有一个会重新渲染。
语法:
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
返回值 memoizedCallback
是一个 memoized
回调。传递内联回调和一系列依赖项。useCallback将返回一个回忆的memoized版本,该版本仅在其中一个依赖项发生更改时才会更改。当将回调传递给依赖于引用相等性的优化子组件以防止不必要的渲染(例如shouldComponentUpdate)时,这非常有用。
这个 Hook 的 API 不能够一两句解释的清楚,建议看一下这篇文章:useHooks 第一期:聊聊 hooks 中的 useCallback。里面介绍的比较详细。
语法:
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() 比 ref 属性更有用。与在类中使用 instance(实例) 字段的方式类似,它可以 方便地保留任何可变值。
注意,内容更改时useRef 不会通知您。变异.current属性不会导致重新渲染。如果要在React将引用附加或分离到DOM节点时运行某些代码,则可能需要使用回调引用。
使用
下面这个例子中展示了可以在 useRef()
生成的 ref
的 current
中存入元素、字符串
Example.js
import React, { useRef, useState, useEffect } from 'react';
export default () => {
// 使用 useRef 创建 inputEl
const inputEl = useRef(null);
const [text, updateText] = useState('');
// 使用 useRef 创建 textRef
const textRef = useRef();
useEffect(() => {
// 将 text 值存入 textRef.current 中
textRef.current = text;
console.log('textRef.current:', textRef.current);
});
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.value = "Hello, useRef";
};
return (
<>
{/* 保存 input 的 ref 到 inputEl */}
updateText(e.target.value)} />
>
);
}
点击 在 input 上展示文字 按钮,就可以看到第一个 input 上出现 Hello, useRef
;在第二个 input 中输入内容,可以看到控制台打印出对应的内容。
语法:
useLayoutEffect(() => { doSomething });
与 useEffect
Hooks 类似,都是执行副作用操作。但是它是在所有 DOM 更新完成后触发。可以用来执行一些与布局相关的副作用,比如获取 DOM 元素宽高,窗口滚动距离等等。
进行副作用操作时尽量优先选择 useEffect,以免阻止视觉更新。与 DOM 无关的副作用操作请使用
useEffect
。
用法与 useEffect 类似。
Example.js
import React, { useRef, useState, useLayoutEffect } from 'react';
export default () => {
const divRef = useRef(null);
const [height, setHeight] = useState(100);
useLayoutEffect(() => {
// DOM 更新完成后打印出 div 的高度
console.log('useLayoutEffect: ', divRef.current.clientHeight);
})
return <>
Hello
>
}
注意:
因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
function Parent() {
return (
<>
// 这样是不行的
>
)
}
function Child(props,ref){
return (
)
}
Child = React.forwardRef(Child);
function Parent(){
let [number,setNumber] = useState(0);
// 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
// 只有当它被赋给某个元素的 ref 属性时,才会有值
// 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
// 那么父组件就可以操作子组件中的某个元素
// 但是函数组件无法接收 ref 属性 这样是不行的
// 所以就需要用到 forwardRef 进行转发
const inputRef = useRef();//{current:''}
function getFocus(){
inputRef.current.value = 'focus';
inputRef.current.focus();
}
return (
<>
>
)
}
useImperativeHandle
可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';
function Child(props,parentRef){
// 子组件内部自己创建 ref
let focusRef = useRef();
let inputRef = useRef();
useImperativeHandle(parentRef,()=>(
// 这个函数会返回一个对象
// 该对象会作为父组件 current 属性的值
// 通过这种方式,父组件可以使用操作子组件中的多个 ref
return {
focusRef,
inputRef,
name:'计数器',
focus(){
focusRef.current.focus();
},
changeText(text){
inputRef.current.value = text;
}
}
});
return (
<>
>
)
}
Child = forwardRef(Child);
function Parent(){
const parentRef = useRef();//{current:''}
function getFocus(){
parentRef.current.focus();
// 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
parentRef.current.addNumber(666);
parentRef.current.changeText('');
console.log(parentRef.current.name);
}
return (
<>
>
)
}
官网介绍forwardRef与useImperativeHandle结合使用
交流
对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!欢迎关注公众号共同学习。