这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性。
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
You clicked {this.state.count} times
);
}
}
由于需要修改document的title属性,不能直接进行绑定,所以需要在组件更新的时候使用代码同步更新title属性的值。同时更新需要在react的生命周期中执行操作,包括了componentDidMount
,componentDidUpdate
。下面使用react的hook进行改写。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
You clicked {count} times
);
}
比较一下hook组件和class组件的写法,可以发现在hook组件中class组件中的componentDidMount
,componentDidUpdate
都没有了,只剩下了useEffect()这个hook函数了。
class组件的写法
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
hook函数组件
useEffect(() => {
document.title = `You clicked ${count} times`;
});
那么我们是不是可以认为useEffect()替代了componentDidMount
,componentDidUpdate
的作用,那么useEffect是不是用来在函数中操作react的生命周期的?
useEffect
的用法useEffect
怎么用?下面是useEffect函数的TS定义文件。
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
通过定义文件,我们可以发现useEffect
函数有来两个参数,第一个参数是effect函数,是在react生命周期中会被调用的函数,第二个参数deps可有可无,deps参数是一个数组,当deps参数不填时,只要react发生数据更新时,就会调用effect函数;当deps参数为state变量时,只在state变量值发生变化时,才会调用effect函数;当deps参数值为[]
时,effect函数只在组件第一次渲染时被执行。
//在组组件第一次渲染和组件数据发生变化时都会执行effect函数
//相当于componentDidMount,componentDidUpdate中调用
useEffect(() => {
document.title = `You clicked ${count} times`;
});
//---------------------------------------------------------------------------
//在组件第一次渲染和count的值发生变化时进行调用,相当于vue中的watch count
//相当于在componentDidUpdate中比较prevState,只有在比较相应state发生变化时进行更新
//对界面的更新进行了相应的优化
useEffect(() => {
document.title = `You clicked ${count} times`;
},[count]);
//等于
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
//---------------------------------------------------------------------------
//effect函数只在组件的第一次渲染时被执行,常用于数据请求和一些页面的初始化工作
useEffect(() => {
document.title = `You clicked ${count} times`;
},[]);
//等于
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
注意,如果effect函数要执行的内容包括异步函数或者Promise,不要直接将effect函数定义为异步函数,只能在effect函数调用异步函数。
useEffect(() => {
document.title = `You clicked ${count} times`;
//也可以在外部定义异步函数和promise
async function fetchData() {
// You can await here
const response = await MyAPI.getData(count);
// ...
}
fetchData();
},[count]);
通过上面的写法,effect函数可以完全替代 componentDidMount
,componentDidUpdate
,并且写法更简单,但是useEffect函数还有一个更重要的作用,那就是清除副作用,什么是清楚副作用,当我们在组件渲染的时候订阅了一个事件,那我们是不是应该在组件卸载是清除订阅,这个用的订阅就是副作用,当我们在组件渲染时生成的外部对象,我们是不是在组件卸载时要删除对象,传统的class组件有 componentWillUnmount()
生命周期函数可以用来清除副作用。
componentDidMount() {
//订阅的函数
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
//删除订阅的函数
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
那么在使用useEffect
hook的函数组件中该怎么,答案还是在effect函数中,我们的effect函数可以有返回值,它的返回值可以返回一个函数,我们可以在这个函数中执行清除操作。
useEffect(() => {
//执行订阅的语句
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
//返回一个清除订阅的函数,这个函数会在组件卸载时进行调用
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
//
});
总后我们发现,通过各种操作,useEffect
函数相当与react生命周期中的componentDidMount
、componentDidUpdate
、omponentWillUnmount()
并且使用起来更加灵活简介。
useEffect
做了什么?通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
useEffect
?将 useEffect
放在组件内部让我们可以在 effect 中直接访问 count
state 变量(或其他 props,函数组件的参数)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect
会在每次渲染后都执行吗?是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。所以在使用useEffect
函数进行更新时,我们一定要清除副作用,这样才能避免重复订阅。对于不同的操作类型,我们要学会分离effect,按顺序使用多个useEffect
。
useEffect
使用useEffect使用时可以根据业务在不同生命周期的逻辑选择不同的参数,对于比较服务的业务逻辑,可以使用多个useEffect
进行effect函数的业务逻辑分离
function FriendStatusWithCounter(props) {
//useState不能写在第一个useEffect后面
//因为第一个useEffect用到了count
const [count, setCount] = useState(0);
//-----------------------------------------
//第一个useEffect,只在count发生改变时,更新document的title值
//count没有发生改变时,没有必要更新title的值
//组件创建更新时第一个执行
useEffect(() => {
document.title = `You clicked ${count} times`;
},[count]);
//-----------------------------------------------
const [isOnline, setIsOnline] = useState(null);
//--------------------------------------------------------
//第二个useEffect,在组件初始化的时候进行订阅,在组件数据发生变化时,不更新订阅
//在组件卸载时清楚订阅
//这两个effect函数的业务逻辑不一样,所以可以分离effect来写
//组件创建更新时第二个执行
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
//返回清除订阅的函数,会在组件卸载的时候调用
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
},[]);
//------------------------------------------------------------------
//useEffect(()=>{to do something})
//useEffect(()=>{to do something})
//more
在分离多个useEffect
时,注意useEffect
的顺序,React hook是按照代码语句的顺序来执行的,如果useState
和useEffect
的顺序写错了,可能会导致出现不正确的业务逻辑,也可能导致代码报错。
useLayoutEffect
的使用其函数签名与 useEffect
相同,
//第一个参数 effect函数
//第一个参数 依赖数组
function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void;
但它会在所有的 DOM 变更之后再同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新,也就是说浏览器会等useLayoutEffect
内部的effect函数执行完毕后才会进行更新,那么如果我们在useLayoutEffect
内部的effect
函数执行非常耗时的操作,这样就会阻塞界面的更新。
所以尽可能使用标准的 useEffect
以避免阻塞视觉更新。
如果你希望界面必须在执行完一些业务代码后才进行渲染,那么可以把代码放到useLayoutEffect
的effect中。
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count === 0) {
const randomNum = 111;
setCount(111);
}
}, [count]);
return (
//点击按钮节目会立刻更新变成0
//然后触发effect函数立刻变成111
//节目短时间更新两次,会出现闪烁
<div onClick={() => setCount(0)}>{count}</div>
);
}
function App() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
if (count === 0) {
const randomNum = 111
setCount(111);
}
}, [count]);
return (
//点击按钮,count的值为0,因为有useLayoutEffect,节面不会立刻更新
//出发因为有useLayoutEffect的effect函数,将count值设为111
//effect函数完毕,界面更新,界面只更新了一次
<div onClick={() => setCount(0)}>{count}</div>
);
}
PS:如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffect
与 componentDidMount
、componentDidUpdate
的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect
,只有当它出问题的时候再尝试使用 useLayoutEffect
。
一般不要在useEffect
中调用 state
变量的set函数,比如setAbc,因为React组件每次更新时,都会调用useEffect
函数,每次调用useEffect
函数时会调用里面的setAbc
函数,setAbc函数会导致组件更新,就会再次调用useEffect
函数,这样就会导致无尽的循环调用,最终导致程序报错堆栈溢出。