1. 首页

请问useEffect Hook 是如何执行的,你懂了没?

想象一下:你有一个非常好用的函数组件,然后有一天,咱们需要向它添加一个生命周期方法。

呃…

刚开始咱们可能会想怎么能解决这个问题,然后最后变成,通常的做法是将它转换成一个类。但有时候咱们就是要用函数方式,怎么破? useEffect hook 出现就是为了解决这种情况。

使用useEffect,可以直接在函数组件内处理生命周期事件。 如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。来看看例子:

码农进阶题库,每天一道面试题 or Js小知识


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!”。

JS中文网 - 前端进阶资源分享

现在,点击Re-render按钮。每次点击,它都会打render!,还会打印umounting,这似乎是奇怪的。

JS中文网 - 前端进阶资源分享

为啥每次渲染都会打印 ‘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])

码农进阶题库,每天一道面试题 or Js小知识

上面这个示例中,咱们传入 [value] 作为第二个参数。这个参数是什么作用呢?如果value的值是 5,而且咱们的组件重渲染的时候 value 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

仅在挂载和卸载的时候执行

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 propsstate 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。


useEffect(() => { console.log('mounted'); return () => console.log('unmounting...'); }, [])

这样只会在组件初次渲染的时候打印 mounted,在组件卸载后打印: unmounting

不过,这隐藏了一个问题:传递空数组容易出现bug。如果咱们添加了依赖项,那么很容易忘记向其中添加项,如果错过了一个依赖项,那么该值将在下一次运行useEffect时失效,并且可能会导致一些奇怪的问题。

只在挂载的时候执行

在这个例子中,一起来看下如何使用useEffectuseRef 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。 将它传递给inputref 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 />, 码农进阶题库,[每天一道面试题 or Js小知识](https://www.javascriptc.com/interview-tips) document.querySelector("#root") );

注意到咱们没有将第二个参数传递给useEffect,这是不好的,不要这样做。

不传递第二个参数会导致每次渲染都会运行useEffect。然后,当它运行时,它获取数据并更新状态。然后,一旦状态更新,组件将重新呈现,这将再次触发useEffect,这就是问题所在。

为了解决这个问题,我们需要传递一个数组作为第二个参数,数组内容又是啥呢。

useEffect所依赖的唯一变量是setPosts。因此,咱们应该在这里传递数组[setPosts]。因为setPostsuseState返回的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的值。

componentDidMountcomponentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect

*

码农进阶题库,每天一道面试题 or Js小知识 https://www.javascriptc.com/interview-tips/

作者:Dave Ceddia
译者:前端小智
链接:https://daveceddia.com/useeffect-hook-examples/

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:请问useEffect Hook 是如何执行的,你懂了没?

链接:https://www.javascriptc.com/3434.html

« JavaScript 中有趣的事实
程序员升级打怪[2019]:前端基础和底层原理»
Flutter 中文教程资源

相关推荐

QR code