zoukankan      html  css  js  c++  java
  • How the useEffect Hook Works (with Examples)(译)

    原文地址:How the useEffect Hook Works (with Examples)

    想象一下:你有一个足够好的函数组件,并且有一天,你需要加一个生命周期函数到里头。

    啊。

    “也许我可以用某种方式解决它?” 最终变成“糟糕,我要将它转化成一个类组件”。

    类组件继承自React.Component,将函数的主体复制黏贴到render方法中,然后将代码格式缩进,最后加上生命周期的方法。

    useEffect 给你提供了一个更好的选择。

    通过useEffect,你可以在函数组件内部直接操纵生命周期相关的事件。即,它们中的三个:componentDidMount, componentDidUpdate, and componentWillUnmount. 全部包含在一个函数中!让我们来看一个例子:

    import React, { useEffect, useState } from 'react';
    import ReactDOM from 'react-dom';
    
    function LifecycleDemo() {
      // Pass useEffect a function
      useEffect(() => {
        // This gets called after every render, by default
        // (the first one, and every one after that)
        console.log('render!');
    
        // If you want to implement componentWillUnmount,
        // return a function from here, and React will call
        // it prior to unmounting.
        return () => console.log('unmounting...');
      })
    
      return "I'm a lifecycle demo";
    }
    
    function App() {
      // Set up a piece of state, so that we have
      // a way to trigger a re-render.
      const [random, setRandom] = useState(Math.random());
    
      // Set up another piece of state to keep track of
      // whether the LifecycleDemo is shown or hidden
      const [mounted, setMounted] = useState(true);
    
      // This function will change the random number,
      // and trigger a re-render (in the console,
      // you'll see a "render!" from LifecycleDemo)
      const reRender = () => setRandom(Math.random());
    
      // This function will unmount and re-mount the
      // LifecycleDemo, so you can see its cleanup function
      // being called.
      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!” 。

    Console output of clicking show/hide

    现在,尝试一下Re-render按钮。每一次单击,它都会打印出“render!”和“umounting”,好像很奇怪...

    单击重新渲染的控制台输出

    为什么每次渲染都输出“unmounting”?

    嗯,您可以(可选)从useEffect返回的清理函数不仅会在卸载组件时被调用。 每次效果生效前都会调用它-从上一次运行中清除。 实际上,它比componentWillUnmount生命周期更强大,因为如果需要,它可以让您在每个渲染之前和之后运行副作用。

    不仅仅是生命周期

    useEffect在每次渲染之后运行(默认情况下),并且可以选择在再次运行之前自行清理。

    与其将useEffect当作一个可以完成3个独立生命周期工作的函数,不如将其简单地视为渲染后运行副作用的一种方式(包括您希望在每个操作之前进行的潜在清理)可能会更有帮助。 并在卸载之前。

    每次进行渲染时阻止副作用

    如果您想减少副作用的运行频率,可以提供第二个参数-一个数组。 将它们视为实现此效果的依赖项。 如果自上次以来某个依赖项已更改,则效果将再次运行。 (它还将在初始渲染后运行)

    const [value, setValue] = useState('initial');
    
    useEffect(() => {
      // This effect uses the `value` variable,
      // so it "depends on" `value`.
      console.log(value);
    }, [value])  // pass `value` as a dependency
    

    考虑该数组的另一种方式:它应该包含effect函数在周围范围中使用的每个变量。 那么,如果使用prop呢? 那在数组中。 是否使用state? 那在数组中。

    useEffect不会主动 “监控”

    一些框架是响应式的,这意味着它们会自动检测更改并在发生更改时更新UI。

    React不会这样做-它只会响应状态更改而重新渲染。

    useEffect也不会主动“监视”更改。 您可以将useEffect调用想像为以下伪代码:

    let previousValues = [];
    let hasRun = false;
    function useEffect(effectFunc, dependencyArray = undefined) {
      // Let's pretend there's a function somewhere that will queue
      // this to run after the render is finished -- because we don't
      // want to run this effect NOW, we only want to queue it up.
      afterRenderIsDone(() => {
        // Check each dependency against the last time this was called
        for(let i = 0; i < dependencyArray.length; i++) {
          if(dependencyArray[i] !== previousValues[i]) {
            // One of the values has changed! Update them for next time,
            // and run the effect
            previousValues = dependencyArray;
            effectFunc();
            break;
          }
        }
      });
    }
    

    当您在组件中调用useEffect时,这实际上是在完成渲染后排队或安排可能运行的效果。

    渲染完成后,useEffect将查看依赖项值的列表,如果其中任何一个已更改,则调用您的effect函数。

    在挂载后只运行一次

    您可以通过传递空数组[]的特殊值来表示“仅在挂载时运行,而在卸载时清除”。 因此,如果我们将上面的组件更改为像这样调用useEffect:

    useEffect(() => {
      console.log('mounted');
      return () => console.log('unmounting...');
    }, [])  // <-- add this empty array here
    

    然后,它将在初始渲染后打印“mounted”,在整个生命周期中保持沉默,并在退出时打印“unmounting...”。

    上面的伪代码不包含对此空数组功能的支持。 可能是这样的:

    let previousValues = [];
    let hasRun = false;
    function useEffect(effectFunc, dependencyArray = undefined) {
      // Let's pretend there's a function somewhere that will queue
      // this to run after the render is finished -- because we don't
      // want to run this effect NOW, we only want to queue it up.
      afterRenderIsDone(() => {
        // 'undefined' is a special value, meaning "run every time"
        if(!dependencyArray) {
          effectFunc();
          return;
        }
    
        // empty array '[]' is also a special value: only run once
        if(dependencyArray.length === 0) {
          if(!hasRun) {
            hasRun = true;
            effectFunc();
          }
          return;
        }
    
        // Check each dependency against the last time this was called
        for(let i = 0; i < dependencyArray.length; i++) {
          if(dependencyArray[i] !== previousValues[i]) {
            // One of the values has changed! Update them for next time,
            // and run the effect
            previousValues = dependencyArray;
            effectFunc();
            break;
          }
        }
      });
    }
    

    请注意第二个参数:如果添加了一个依赖项,很容易忘记添加一个项目;如果您错过了一个依赖项,那么下次useEffect运行时该值将过时,并且可能会导致一些奇怪的问题。

    useEffect何时运行?

    默认情况下,useEffect在每次调用该组件的渲染之后运行。 通过示例最容易看到此时间。尝试下CodeSandbox上的交互式示例,确保你打开了控制台,你能看到这个时间。

    在这个示例中,有3个嵌套组件,Top包含Middle,Middle包含Bottom。useEffect的时间取决于每个组件的渲染时间,并且初始将全部渲染3个组件。你会看到控制台打印出3条消息。

    但是请注意,React是自下而上渲染的! 在这种情况下:底部,然后是中间,然后是顶部。 它是递归的-父级直到其所有子级都渲染完毕后才“完成”,并且useEffect仅在组件的渲染完成后才运行。

    从那时起,将不会发生任何事情,直到您单击其中一个元素以增加其计数。 完成后,唯一会重新渲染的组件是您单击的组件及其下方的组件。 (请注意,如果您单击“底部”,您将不会看到“顶部”或“中间”的“已渲染”消息)

    function Top() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        console.log("Top rendered");
      });
    
      return (
        <div>
          <div onClick={() => setCount(count + 1)}>Top Level {count}</div>
          <Middle />
        </div>
      );
    }
    
    function Middle() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        console.log("Middle rendered");
      });
    
      return (
        <div>
          <div onClick={() => setCount(count + 1)}>Middle Level {count}</div>
          <Bottom />
        </div>
      );
    }
    
    function Bottom() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        console.log("Bottom rendered");
      });
    
      return <div onClick={() => setCount(count + 1)}>Bottom Level {count}</div>;
    }
    

    在state变化时会运行useEffect

    默认情况下,useEffect在每次渲染后运行,但是对于响应状态更改而运行一些代码也是完美的选择。 您可以通过将第二个参数传递给useEffect来限制效果的运行时间。

    将第二个参数想像为“依赖项”数组–如果更改了变量,效果应重新运行。 这些可以是任何类型的变量:prop,state或其他任何变量。

    在此示例中,有3个state变量和3个按钮。 该效果仅在count2更改时运行,否则将保持安静。 尝试交互式示例。

    function ThreeCounts() {
      const [count1, setCount1] = useState(0);
      const [count2, setCount2] = useState(0);
      const [count3, setCount3] = useState(0);
    
      useEffect(() => {
        console.log("count2 changed!");
      }, [count2]);
    
      return (
        <div>
          {count1} {count2} {count3}
          <br />
          <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>
          <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
          <button onClick={() => setCount3(count3 + 1)}>Increment count3</button>
        </div>
      );
    }
    

    在prop变化时会运行useEffect

    正如我们能够将useEffect设置为在状态变量更改时运行一样,使用props也可以做到这一点。 请记住,它们都是常规变量! useEffect可以在其中任何一个上触发。

    在此示例中,PropChangeWatch组件正在接收2个props(a和b),并且其效果仅在更改值时才会运行(因为我们正在传递包含[a]作为第二个参数的数组)。

    交互式示例中尝试一下.:

    function PropChangeWatch({ a, b }) {
      useEffect(() => {
        console.log("value of 'a' changed to", a);
      }, [a]);
    
      return (
        <div>
          I've got 2 props: a={a} and b={b}
        </div>
      );
    }
    
    function Demo() {
      const [count1, setCount1] = useState(0);
      const [count2, setCount2] = useState(0);
    
      return (
        <div>
          <PropChangeWatch a={count1} b={count2} />
          <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>
          <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
        </div>
      );
    }
    

    专注于挂载

    有时您只想在挂载时做一件小事,而做一件小事需要将一个函数重写为一个类。

    在此示例中,让我们看一下如何结合使用useEffect和useRef钩子将输入控件集中在第一个渲染上。

    import React, { useEffect, useState, useRef } from "react";
    import ReactDOM from "react-dom";
    
    function App() {
      // Store a reference to the input's DOM node
      const inputRef = useRef();
    
    	// Store the input's value in state
      const [value, setValue] = useState("");
    
      useEffect(
        () => {
          // This runs AFTER the first render,
          // so the ref is set by now.
          console.log("render");
          // inputRef.current.focus();
        },
    		// The effect "depends on" inputRef
        [inputRef]
      );
    
      return (
        <input
          ref={inputRef}
          value={value}
          onChange={e => setValue(e.target.value)}
        />
      );
    }
    ReactDOM.render(<App />, document.querySelector("#root"));
    

    在顶部,我们使用useRef创建一个空的ref。 将其传递到输入的ref属性后,就需要在渲染DOM后对其进行设置。 而且,重要的是,useRef返回的值在渲染之间将保持稳定–不会更改。

    因此,即使我们将[inputRef]作为useEffect的第二个参数传递,它在初始挂载时实际上只会运行一次。 这基本上是“ componentDidMount”(时间原因,我们将在后面讨论)。

    为了证明这一点,请[尝试示例](https://codesandbox.io/s/z6pommq8km)。请注意它的聚焦方式(使用CodeSandbox编辑器时有点麻烦,但是请尝试单击右侧“浏览器”中的刷新按钮)。然后尝试在框中输入。 每个字符都会触发重新渲染,但是如果您查看控制台,则会看到“render”仅打印一次。

    使用useEffect请求数据

    让我们看看另一个常见的用例:请求数据并显示它。在类组件中,您需要将此代码放入componentDidMount方法中。为此,我们将使用useEffect。我们还需要useState来存储数据。

    值得一提的是,当React新的Suspense功能的数据获取部分准备就绪时,这将是获取数据的首选方式。 从useEffect抓取有一个大难题(我们将继续介绍),而Suspense API的使用将变得更加容易。

    这是一个从Reddit获取帖子并显示它们的组件:

    import React, { useEffect, useState } from "react";
    import ReactDOM from "react-dom";
    
    function Reddit() {
      // Initialize state to hold the posts
      const [posts, setPosts] = useState([]);
    
      // effect functions can't be async, so declare the
      // async function inside the effect, then call it
      useEffect(() => {
        async function fetchData() {
          // Call fetch as usual
          const res = await fetch(
            "https://www.reddit.com/r/reactjs.json"
          );
    
          // Pull out the data as usual
          const json = await res.json();
    
          // Save the posts into state
          // (look at the Network tab to see why the path is like this)
          setPosts(json.data.children.map(c => c.data));
        }
    
        fetchData();
      }); // <-- we didn't pass a value. what do you think will happen?
    
      // 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返回的设置器,因此不会在每个渲染器中都重新创建它,因此效果只能运行一次。

    有趣的事实:调用useState时,它返回的setter函数仅创建一次!每次渲染组件时,它都是完全相同的函数实例,这就是为什么效果可以安全依赖于一个实例的原因。这个有趣的事实对于useReducer返回的调度函数也是如此。

    当数据变化时重新发送请求

    让我们在示例上进行扩展,以涵盖另一个常见问题:如何在发生某些变化(例如用户ID或本例中为subreddit的名称)时重请求取数据。

    首先,我们将Reddit组件更改为接受subreddit作为prop,基于该subreddit请求数据,并仅在prop更改时重新运行效果:

    // 1. Destructure the `subreddit` from props:
    function Reddit({ subreddit }) {
      const [posts, setPosts] = useState([]);
    
      useEffect(() => {
        async function fetchData() {
          // 2. Use a template string to set the URL:
          const res = await fetch(
            `https://www.reddit.com/r/${subreddit}.json`
          );
    
          const json = await res.json();
          setPosts(json.data.children.map(c => c.data));
        }
    
        fetchData();
    
        // 3. Re-run this effect when `subreddit` changes:
      }, [subreddit, setPosts]);
    
      return (
        <ul>
          {posts.map(post => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      );
    }
    
    // 4. Pass "reactjs" as a prop:
    ReactDOM.render(
      <Reddit subreddit='reactjs' />,
      document.querySelector("#root")
    );
    

    这仍然是硬编码的,但是现在我们可以通过将Reddit组件包装为一个组件来对其进行自定义,以使我们可以更改subreddit。 添加此新的App组件,并在底部进行渲染:

    function App() {
      // 2 pieces of state: one to hold the input value,
      // another to hold the current subreddit.
      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中运行这个示例

    该应用程序在此处保持2种状态-当前输入值和当前subreddit。提交输入将“提交”子reddit,这将导致Reddit从新选择中重新获取数据。将输入包装在表格中,允许用户按Enter提交。

    顺便说一句:仔细键入。没有错误处理。如果您输入不存在的subreddit,则该应用将崩溃。但是,实现错误处理将是一个很棒的练习! ;)

    我们在这里只可以使用一种状态-存储输入,并将相同的值发送给Reddit-但是Reddit组件将在每次按键时获取数据。

    顶部的useState可能看起来有些奇怪,尤其是第二行:

    const [inputValue,setValue] = useState(“ reactjs”);
    const [subreddit,setSubreddit] = useState(inputValue);
    

    我们正在将“ reactjs”的初始值传递给第一状态,这很有意义。该价值永远不会改变。

    但是第二行呢?如果初始状态改变怎么办? (并且,当您在框中键入内容时,它会显示)

    请记住,useState是有状态的(有关useState的更多信息)。它只使用一次,即第一次渲染时的初始状态。之后,它将被忽略。因此,可以传递一个瞬态值(例如可能会更改的prop或其他变量)是安全的。

    一百种用途

    useEffect函数就像瑞士军刀的钩子。 它可以用于很多事情,从设置订阅到创建和清除计时器,再到更改引用的值。

    不利的一件事是进行对用户可见的DOM更改。 计时的工作方式是,effect函数仅在浏览器完成布局和绘制后才会启动-如果您想进行视觉更改,为时已晚。

    对于这些情况,React提供了useMutationEffect和useLayoutEffect钩子,除了它们被触发时机,它们的作用与useEffect相同。 看看docs for useEffect,尤其是有关the timing of effects的部分,如果您需要进行可见的DOM更改。

    这似乎是一个额外的麻烦。 另一件事要担心。 不幸的是,这有点。 这个(heh)的积极副作用是,由于useEffect在布局和绘制之后运行,因此缓慢的效果不会使UI变得混乱。 不利的一面是,如果要将旧代码从生命周期转移到挂钩,则必须谨慎一点,因为这意味着useEffect在计时方面几乎等同于componentDidUpdate。

    试用useEffect

    您可以在启用了钩子的CodeSandbox中尝试使用useEffect。 一些想法...

    • 渲染输入框并使用useState存储其值。 然后在效果中设置document.title。 (例如Dan在React Conf上的演示

    • 制作一个自定义挂钩,以从URL提取数据

    • 将单击处理程序添加到文档,并在每次用户单击时打印一条消息。 (不要忘记清理处理程序!)

    如果您需要灵感,请访问Nik Graf的React Hooks集合-目前star数量为440! 它们中的大多数很容易自行实现。 (例如useOnMount,我敢打赌,您可以根据您在本文中学到的知识实现该功能!)

    学习React可能会很麻烦-太多的库和工具!
    我的建议? 忽略所有人:)
    有关循序渐进的方法,请访问我的Pure React研讨会

  • 相关阅读:
    (三)认识twisted reactor
    (二)inlineCallbacks,同步方式写异步代码
    (一)使用twisted Deferred
    javascript通过字典思想操作数据
    锱铢必较,从(function(){}())与(function(){})()说起
    针对谷歌默认最小字体12px的正确解决方案 (css、html)
    百度搜索研发部:同义词反馈机制
    LinkedList与ArrayList的区别
    从源码的角度分析List与Set的区别
    springboot整合redisson分布式锁
  • 原文地址:https://www.cnblogs.com/xingguozhiming/p/13758281.html
Copyright © 2011-2022 走看看