useEffect Hook 是如何工做的

做者:Dave Ceddiahtml

译者:前端小智前端

来源:daveceddia.react


阿里云最近在作活动,低至2折,有兴趣能够看看promotion.aliyun.com/ntms/yunpar…git


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

想象一下:你有一个很是好用的函数组件,而后有一天,我们须要向它添加一个生命周期方法。json

呃...数组

刚开始我们可能会想怎么能解决这个问题,而后最后变成,一般的作法是将它转换成一个类。但有时候我们就是要用函数方式,怎么破? 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中尝试一下。dom

单击“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

原文:daveceddia.com/useeffect-h…

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

交流(欢迎加入群,群工做日都会发红包,互动讨论技术)

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

github.com/qq449245884…

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

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

相关文章
相关标签/搜索