Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useEffect Hook 是如何工作的 #107

Open
husky-dot opened this issue Sep 5, 2019 · 1 comment
Open

useEffect Hook 是如何工作的 #107

husky-dot opened this issue Sep 5, 2019 · 1 comment

Comments

@husky-dot
Copy link
Owner

作者:Dave Ceddia

译者:前端小智

来源:daveceddia.

为了保证的可读性,本文采用意译而非直译。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

为了回馈读者,《大迁世界》不定期举行(每个月一到三次),现金抽奖活动,保底200,外加用户赞赏,希望你能成为大迁世界的小锦鲤,快来试试吧

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

呃...

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

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

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 不依赖于 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 />,
  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

原文:https://daveceddia.com/useeffect-hook-examples/

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

@zoubingwu
Copy link

useEffect 里是不能直接传 async function 的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants