作者:Dave Ceddia
译者:前端小智
来源:daveceddia.
为了保证的可读性,本文采用意译而非直译。
想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!
为了回馈读者,《大迁世界》不定期举行(每个月一到三次),现金抽奖活动,保底200,外加用户赞赏,希望你能成为大迁世界的小锦鲤,快来试试吧
想象一下:你有一个非常好用的函数组件,然后有一天,咱们需要向它添加一个生命周期方法。
呃...
刚开始咱们可能会想怎么能解决这个问题,然后最后变成,通常的做法是将它转换成一个类。但有时候咱们就是要用函数方式,怎么破? useEffect
hook 出现就是为了解决这种情况。
使用useEffect
,可以直接在函数组件内处理生命周期事件。 如果你熟悉 React class 的生命周期函数,你可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。来看看例子:
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
function LifecycleDemo() {
useEffect(() => {
// 默认情况下,每次渲染后都会调用该函数
console.log('render!');
// 如果要实现 componentWillUnmount,
// 在末尾处返回一个函数
// React 在该函数组件卸载前调用该方法
// 其命名为 cleanup 是为了表明此函数的目的,
// 但其实也可以返回一个箭头函数或者给起一个别的名字。
return function cleanup () {
console.log('unmounting...');
}
})
return "I'm a lifecycle demo";
}
function App() {
// 建立一个状态,为了方便
// 触发重新渲染的方法。
const [random, setRandom] = useState(Math.random());
// 建立一个状态来切换 LifecycleDemo 的显示和隐藏
const [mounted, setMounted] = useState(true);
// 这个函数改变 random,并触发重新渲染
// 在控制台会看到 render 被打印
const reRender = () => setRandom(Math.random());
// 该函数将卸载并重新挂载 LifecycleDemo
// 在控制台可以看到 unmounting 被打印
const toggle = () => setMounted(!mounted);
return (
<>
<button onClick={reRender}>Re-render</button>
<button onClick={toggle}>Show/Hide LifecycleDemo</button>
{mounted && <LifecycleDemo/>}
</>
);
}
ReactDOM.render(<App/>, document.querySelector('#root'));
在CodeSandbox中尝试一下。
单击“Show/Hide
”按钮,看看控制台,它在消失之前打印“unmounting...
”,并在它再次出现时打印 “render!
”。
现在,点击Re-render
按钮。每次点击,它都会打render!
,还会打印umounting
,这似乎是奇怪的。
为啥每次渲染都会打印 'unmounting
'。
咱们可以有选择性地从useEffect
返回的cleanup
函数只在组件卸载时调用。React 会在组件卸载的时候执行清除操作。正如之前学到的,effect
在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect
进行清除。这实际上比componentWillUnmount
生命周期更强大,因为如果需要的话,它允许咱们在每次渲染之前和之后执行副作用。
不完全的生命周期
useEffect在每次渲染后运行(默认情况下),并且可以选择在再次运行之前自行清理。
与其将useEffect
看作一个函数来完成3个独立生命周期的工作,不如将它简单地看作是在渲染之后执行副作用的一种方式,包括在每次渲染之前和卸载之前咱们希望执行的需要清理的东西。
阻止每次重新渲染都会执行 useEffect
如果希望 effect
较少运行,可以提供第二个参数 - 值数组。 将它们视为该effect
的依赖关系。 如果其中一个依赖项自上次更改后,effect
将再次运行。
const [value, setValue] = useState('initial');
useEffect(() => {
// 仅在 value 更改时更新
console.log(value);
}, [value])
上面这个示例中,咱们传入 [value]
作为第二个参数。这个参数是什么作用呢?如果value
的值是 5
,而且咱们的组件重渲染的时候 value
还是等于 5,React 将对前一次渲染的 [5]
和后一次渲染的 [5]
进行比较。因为数组中的所有元素都是相等的(5 === 5)
,React 会跳过这个 effect
,这就实现了性能的优化。
仅在挂载和卸载的时候执行
如果想执行只运行一次的 effect
(仅在组件挂载和卸载时执行),可以传递一个空数组([]
)作为第二个参数。这就告诉 React 你的 effect
不依赖于 props
或 state
中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。
useEffect(() => {
console.log('mounted');
return () => console.log('unmounting...');
}, [])
这样只会在组件初次渲染的时候打印 mounted
,在组件卸载后打印: unmounting
。
不过,这隐藏了一个问题:传递空数组容易出现bug
。如果咱们添加了依赖项,那么很容易忘记向其中添加项,如果错过了一个依赖项,那么该值将在下一次运行useEffect
时失效,并且可能会导致一些奇怪的问题。
只在挂载的时候执行
在这个例子中,一起来看下如何使用useEffect
和useRef
hook 将input
控件聚焦在第一次渲染上。
import React, { useEffect, useState, useRef } from "react";
import ReactDOM from "react-dom";
function App() {
// 存储对 input 的DOM节点的引用
const inputRef = useRef();
// 将输入值存储在状态中
const [value, setValue] = useState("");
useEffect(
() => {
// 这在第一次渲染之后运行
console.log("render");
// inputRef.current.focus();
},
// effect 依赖 inputRef
[inputRef]
);
return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
在顶部,我们使用useRef
创建一个空的ref
。 将它传递给input
的ref prop
,在渲染DOM 时设置它。 而且,重要的是,useRef
返回的值在渲染之间是稳定的 - 它不会改变。
因此,即使咱们将[inputRef]
作为useEffect
的第二个参数传递,它实际上只在初始挂载时运行一次。这基本上是 componentDidMount
效果了。
使用 useEffect 获取数据
再来看看另一个常见的用例:获取数据并显示它。在类组件中,无们通过可以将此代码放在componentDidMount
方法中。在 hook 中可以使用 useEffect hook 来实现,当然还需要用useState
来存储数据。
下面是一个组件,它从Reddit获取帖子并显示它们
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
function Reddit() {
const [posts, setPosts] = useState([]);
useEffect(async () => {
const res = await fetch(
"https://www.reddit.com/r/reactjs.json"
);
const json = await res.json();
setPosts(json.data.children.map(c => c.data));
}); // 这里没有传入第二个参数,你猜猜会发生什么?
// Render as usual
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
ReactDOM.render(
<Reddit />,
document.querySelector("#root")
);
注意到咱们没有将第二个参数传递给useEffect
,这是不好的,不要这样做。
不传递第二个参数会导致每次渲染都会运行useEffect
。然后,当它运行时,它获取数据并更新状态。然后,一旦状态更新,组件将重新呈现,这将再次触发useEffect
,这就是问题所在。
为了解决这个问题,我们需要传递一个数组作为第二个参数,数组内容又是啥呢。
useEffect
所依赖的唯一变量是setPosts
。因此,咱们应该在这里传递数组[setPosts]
。因为setPosts
是useState
返回的setter
,所以不会在每次渲染时重新创建它,因此effect
只会运行一次。
当数据改变时重新获取
虚接着扩展一下示例,以涵盖另一个常见问题:如何在某些内容发生更改时重新获取数据,例如用户ID,名称等。
首先,咱们更改Reddit
组件以接受subreddit
作为一个prop,并基于该subreddit
获取数据,只有当 prop
更改时才重新运行effect
.
// 从props中解构`subreddit`:
function Reddit({ subreddit }) {
const [posts, setPosts] = useState([]);
useEffect(async () => {
const res = await fetch(
`https://www.reddit.com/r/${subreddit}.json`
);
const json = await res.json();
setPosts(json.data.children.map(c => c.data));
// 当`subreddit`改变时重新运行useEffect:
}, [subreddit, setPosts]);
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
ReactDOM.render(
<Reddit subreddit='reactjs' />,
document.querySelector("#root")
);
这仍然是硬编码的,但是现在咱们可以通过包装Reddit
组件来定制它,该组件允许咱们更改subreddit
。
function App() {
const [inputValue, setValue] = useState("reactjs");
const [subreddit, setSubreddit] = useState(inputValue);
// Update the subreddit when the user presses enter
const handleSubmit = e => {
e.preventDefault();
setSubreddit(inputValue);
};
return (
<>
<form onSubmit={handleSubmit}>
<input
value={inputValue}
onChange={e => setValue(e.target.value)}
/>
</form>
<Reddit subreddit={subreddit} />
</>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
在 CodeSandbox 试试这个示例。
这个应用程序在这里保留了两个状态:当前的输入值和当前的subreddit
。提交表单将提交subreddit
,这会导致Reddit
重新获取数据。
顺便说一下:输入的时候要小心,因为没有错误处理,所以当你输入的subreddit
不存在,应用程序将会爆炸,实现错误处理就作为你们的练习。
各位可以只使用一个状态来存储输入,然后将相同的值发送到Reddit
,但是Reddit
组件会在每次按键时获取数据。
顶部的useState
看起来有点奇怪,尤其是第二行:
const [inputValue, setValue] = useState("reactjs");
const [subreddit, setSubreddit] = useState(inputValue);
我们把reactjs
的初值传递给第一个状态,这是有意义的,这个值永远不会改变。
那么第二行呢,如果初始状态改变了呢,如当你输入box
时候。
记住,useState
是有状态的。它只使用初始状态一次,即第一次渲染,之后它就被忽略了。所以传递一个瞬态值是安全的,比如一个可能改变或其他变量的 prop
。
许许多多的用途
使用useEffect
就像瑞士军刀。它可以用于很多事情,从设置订阅到创建和清理计时器,再到更改ref
的值。
与 componentDidMount
、componentDidUpdate
不同的是,在浏览器完成布局与绘制之后,传给 useEffect
的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
然而,并非所有 effect
都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React
为此提供了一个额外的 useLayoutEffect
Hook 来处理这类 effect
。它和 useEffect
的结构相同,区别只是调用时机不同。
虽然 useEffect
会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect
。
原文:https://daveceddia.com/useeff...
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
交流
干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。
https://github.com/qq44924588...
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。