用了这么长时间的 React useEffect hook,你用对了吗?

主线

  • 什么是 Effect
  • 与生命周期的关系
  • 运行时机
  • 类实际工作的例子 - 抓取数据
    • 第二个参数
    • 添加其他属性
  • 划重点

useEffect 是 React Hooks 的核心,要保证理解它的运行机制和正确的使用方法才能避免这样那样的坑。在以前的工作中,因为它我碰到过无数个坑,比如拿到的值是旧的,该执行的时候不执行,不该执行的时候执行了……

所以为了避免如上尴尬、节省绞尽脑汁找 bug 的时间、保护咱们的发际线,一定要认真学会如何正确使用 useEffect hook。

什么是 Effect

俗话说,知已知彼,百战不殆,我们先了解下神马是 Effect。其实大家在开发过程中或多或少的都接触过 side effect 的概念,即副作用 —— 对于数据抓取,注册监听事件,修改 DOM 元素等马后炮式的操作都属于副作用,因为我们渲染出来的页面是静态的,任何在之后的操作都会对它产生影响,所以才称之为副作用。而 useEffect 则是专门用来编写副作用代码的,这也是 React 的核心所在。

与生命周期的关系

目前市面上的文章,包括官方文档都让我们把 useEffect 想象成 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个生命周期的结合体。其实并不然,如果非要把 useEffect 的运行机制往生命周期上靠,会造成一些逻辑上的困惑,进而产生 bug。我们所要做的,就是把 useEffect 当成一个全新的特性,专门为函数式组件服务的,这样用起来才不会迷茫。下面我们通过实例来演示它的各种用法。

运行时机

useEffect 必然会在 render 的时候执行一次,其他的运行时机取决于以下情况:

  • 有没有第二个参数。useEffect hook 接受两个参数,第一个是要执行的代码,第二个是一个数组,指定一组依赖的变量,其中任何一个变量发生变化时,此 effect 都会重新执行一次。
  • 有没有返回值。 useEffect 的执行代码中可以返回一个函数,在每一次新的 render 进行前或者组件 unmount 之时,都会执行此函数,进行清理工作。

我们先看一个简单的例子,想看完整代码和随意把玩的,请点击下边按钮

我们首先看最顶层 的代码:

function App() {
  const [showList, setShowList] = useState(false);
  const [postCount, setPostCount] = useState(5);

  return (
    <div className="App">
      <button onClick={() => setShowList(!showList)}>
        {showList ? "隐藏" : "显示"}
      </button>
      <button onClick={() => setPostCount(previousCount => previousCount + 1)}>
        增加数量
      </button>
      {showList && <PostList count={postCount} />}
    </div>
  );
}

此组件用来显示一系列的文章列表,以及控制文章列表是否显示的按钮和控制显示多少条文章的按钮。我们用 showList state 来控制 的显示与否。这是为了让 组件 unmount 再 render ,以证明每次它 render 和 unmount 的时候,useEffect hook 都会跑一次。 组件的代码如下:

function PostList({ count = 5 }) {
  useEffect(() => {
    let p = document.createElement("p");
    p.innerHTML = `当前文章数量:${count}`;
    document.body.append(p);
  });

  return (
    <ul>
      {new Array(count).fill("文章标题").map((value, index) => {
        return (
          <li key={index}>
            {value}
            {index + 1}
          </li>
        );
      })}
    </ul>
  );
}

该组件展示了一个

    列表,为了简单起见,生成了一些无聊的文章标题。我们重点来看一下 useEffect 所做的操作:

    • 创建一个 p 元素
    • 设置 p 的文本为当前文章的数量
    • 追加 pbody 的最后

    在这里,此 effect 并没有返回任何值,也没有给它传递任何一个参数,那会是什么样的效果呢?
    用了这么长时间的 React useEffect hook,你用对了吗?_第1张图片

    答案是,此 effect 会在每次 countshowList 改变时每点击一次 显示增加数量 按钮,我们新追加的 p 都会再追加一次。这也是造成内容泄露的坑,如果我们在这里添加了太多耗内存的东西而没有清理,不用多久浏览器就崩溃了~ 解决方法很简单,给 useEffect 添加一个返回值,并在里边删除我们追加的 p 元素即可:

    useEffect(() => {
      let p = document.createElement("p");
      p.innerHTML = `当前文章数量:${count}`;
      document.body.append(p);
    
      return () => {
        p.remove();
      };
    });
    

    这样我们在点击按钮的时候,确保只有一个 p 在当前页面上。看,这样写起来是不是比分散在 componentDidMountcomponentWillUnmount 中方便多了?我们可以方便的在同一个作用域中方便的拿到 p 的引用,直接删除它即可。

    用了这么长时间的 React useEffect hook,你用对了吗?_第2张图片

    类实际工作的例子 - 抓取数据

    为了继续深入 useEffect hook,我仿照实际工作遇到的情况,编写了一个例子,这里我们用 useEffect 进行数据抓取,同样的显示博客文章列表,完整代码请点击下方按钮查看:

    在本例中,组件的代码做了一些修改,首先我们定义两个新的 state:

    • posts。保存远程加载的文章列表
    • loading。记录 ajax 请求状态
    const [posts, setPosts] = useState([]);
    const [loading, setLoading] = useState(false);
    

    我们一开始觉得 fetch 是异步操作,那么得给 useEffect hook 传递个 async 的函数吧?错,传递给 useEffect 的函数不能是 async 的, 因为 async 的本质是返回一个 Promise,而 useEffect 唯一接收的返回值是个函数。使用 async 会收到以下异常:

    Warning: An effect function must not return anything besides a function, which is used for clean-up.

    // 错误
    useEffect(async () => {
      // const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    });
    

    正确的写法是,把抓取数据的逻辑定义到一个单独的函数中,然后在 useEffect 中调用它:

    useEffect(() => {
      // const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    
      const loadPosts = async () => {
        setLoading(true);
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/posts?_limit=${count}`
        );
        const data = await response.json();
        setPosts(data);
        setLoading(false);
      };
    
      loadPosts();
    }, [count]);
    

    它所做的操作是,在请求数据前,把 loading 状态设置为 true,然后根据 count 的值去取对应数量的文章列表,把返回值更新到 posts state 中,再把 loading 设置为 false。最后根据 loading 的状态,我们显示 加载中文章列表

    if (loading) return <div>loading...</div>;
    
    return (
      <ul>
        {posts.slice(0, count).map((post, index) => {
          return <li key={post.id}>{post.title}</li>;
        })}
      </ul>
    );
    

    用了这么长时间的 React useEffect hook,你用对了吗?_第3张图片

    第二个参数

    上边的例子中我们给 useEffect 传递了第二个参数,并把 count 作为依赖的值,每当 count 变化时,此 effect 都会重新执行一次,去加载新的数据。另外,如果我们隐藏列表,再点击 显示 按钮时,effect 也会再跑一次,因为点击隐藏时, 组件被 unmount ,然后再次显示时会重新 render ,我们可以根据 loading... 这个标志就可以看出来了。

    如果我们去掉第二个参数,那么就会陷入死循环的坑,为什么呢?因为 effect 执行时,会更新 postsloading 这两个 state,而 state 变化时,组件又会重新 render 一次,根据 useEffect 在每次 render 必执行一次的定律不难得出结论。

    那么如果我们给它一个空数组呢?那就无论怎么点击增加数量,此 effect 都不会重新执行,导致永远只加载默认 5 篇文章。

    用了这么长时间的 React useEffect hook,你用对了吗?_第4张图片

    添加其他属性

    我们可以再试试添加一个其他属性来测试 useEffect 依赖数组的特性。在 组件中我们添加一个布局状态 vertical 和修改布局的按钮,用来控制 组件的横向、纵向布局:

    // APP
    function App() {
      // 其它代码省略
      const [vertical, setVertical] = useState(true);
    
      return (
        <div className="App">
          {/* 其它代码省略 */}
          <button onClick={() => setVertical(prev => !prev)}>更改布局</button>
          {showList && <PostList count={postCount} vertical={vertical} />}
        </div>
      );
    }
    
    // PostList
    function PostList({ count = 5, vertical = false }) {
      // 其它代码省略
      useEffect(() => {
        // const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    
        const loadPosts = async () => {
          setLoading(true);
          const response = await fetch(
            `https://jsonplaceholder.typicode.com/posts?_limit=${count}`
          );
          const data = await response.json();
          setPosts(data);
          setLoading(false);
        };
    
        loadPosts();
      }, [count, vertical]); // 在这里添加 vertical 作为依赖
    
      // 其它代码省略
    }
    

    在这里我们给第二个参数添加了 vertical 依赖,这样每次点击 更改布局 按钮时,文章列表都会加载一次,这种适合在布局改变时需要重新请求数据的情况:
    用了这么长时间的 React useEffect hook,你用对了吗?_第5张图片

    如果不需要重新加载数据,只需要把 vertical 从依赖数组里去掉就可以了。

    用了这么长时间的 React useEffect hook,你用对了吗?_第6张图片

    划重点

    看看大家对频繁使用的 useEffect 的用法用对了没有?来标一下重点:

    1. 它可不完全是 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个生命周期的结合体哦(人家有自己的想法)。
    2. 会在每次 render 的时候必定执行一次。
    3. 如果返回了函数,那么在下一次 render 之前或组件 unmount 之前必定会运行一次返回函数的代码。
    4. 如果指定了依赖数组,且不为空,则当数组里的每个元素发生变化时,都会重新运行一次。
    5. 如果数组为空,则只在第一次 render 时执行一次,如果有返回值,则同 3。
    6. 如果在 useEffect 中更新了 state,且没有指定依赖数组,或 state 存在于依赖数组中,就会造成死循环。

    大家掌握了吗?有什么问题欢迎评论或私信我!如果觉得文章有帮助请关注博主我哦,感谢,比心。

你可能感兴趣的:(前端)