Effect

一般说来,函数组件大多都是纯函数,而且其中的渲染部分一定都是纯粹的。这一部分的输入一般只是props和 State,而纯函数的意思就是对于相同的props和 State,函数组件产生的渲染输出都是一样的。但是一个组件在渲染之前和渲染时所要做的事情远远不止于此,例如有的组件需要通过访问服务器来获取数据。这种操作就决定了这样的函数组件不是一个纯函数,而其中也将包含很多副作用。这种副作用就是 Effect,也就是由渲染本身引起的操作而不是由事件引起的。

Tip

在之后的章节中会讨论由事件引起的副作用操作,也就是事件处理函数。

由渲染引起的 Effect 总是在屏幕更新以后的提交阶段运行,它通常用来暂时中断 React 的代码而与一些外部资源或系统进行交互。

在组件中定义一个 Effect 并不复杂,Hook useEffect()就是设计用来提供这项功能的。useEffect()的使用格式为useEffect(setup, dependencies),其中setup是 Effect 要执行的功能,React 要求其必须是同步函数,这个函数的返回值是 Effect 的清理函数;dependencies是这个 Effect 的setup函数中所引用的响应式值的列表,当这个列表里的值发生变化的时候,Effect 就会重新执行,也就是 Effect 的依赖项。

Effect 的执行过程是,当组件被添加到 DOM 的时候,React 就会执行 Effect 中的setup函数;当 Effect 的依赖项发生变化的时候,React 将使用旧值运行上一次 Effect 中setup函数返回的清理函数,然后再使用新值重新运行setup函数;当组件被从 DOM 移除的时候,React 将再执行一次从 Effect 中的setup函数返回的清理函数。

以下是一个常见的 Effect 的示例。

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `已点击${count}次。`;
  });

  return (
    <div>
      <p>已经点击了{count}次。</p>
      <button onClick={() => setCount(count + 1)}>Click Me</button>
    </div>
  );
}

如果需要在 Effect 中执行异步函数,那么你也必须将其视为同步函数来对待,或者利用Promise来使其组成链式操作。

例如直接像以下示例中这样使用异步操作是不会成功的。

function AsyncFetch() {
  useEffect(async () => {
    await fetch('some_url');
  });
}

位于 Effect 中的异步操作必须要同步调用才可以。

function AsyncFetch() {
  useEffect(() => {
    let fetchData = async () => {
      await fetch('some_url');
    };

    fetchData();
  });
}

清理函数

在 Effect 的setup函数可以返回一个函数,也就是上文中一直提到的清理函数。这个清理函数是十分有用的,尤其是在使用一些成对操作的情况下,例如调用document.addListener()之类的。以下是一个简单的清理函数的示例。

useEffect(() => {
    object.subscribe(...);
    return () => {
        object.unsubscribe(...);
    };
);

所有需要在组件中 DOM 中移除时完成的操作,实际上都可以利用setup函数返回的清理函数来完成。

什么时候需要使用 Effect

要想知道什么时候需要使用 Effect,那么可以了解什么时候不需要使用 Effect。如果你计划的 Effect 只是用来调整其他的 State,那么你可能不会需要 Effect。

Effect 不应该被用来处理props和 State 数据的转换,也不应该用来处理用产生的事件。props和 State 中俄数据转换通常都比较昂贵,会消耗较多的资源,所以这种工作使用useMemo()会更加适合一些。

除此以外的情况,你可以尝试选择使用 Effect 来达到你的设计目标,但是在 Effect 并不想你设想的那样工作的时候,应该首先积极的考虑是否有其他可以替代 Effect 的方法。

同一个 Effect 会执行两次

这种现象主要出现在开发环境中,在开啊环境中,React 会执行两次 Effect 以确保在离开和返回页面时不会导致代码的运行出现问题。但是如果在 Effect 中需要完成一些额外的启动时只需要执行一次的操作,那么可能就会出现设想中应该执行一次的操作,结果却执行了两次的错觉。

出现这种情况时,所需要解决的问题不是如何确保 Effect 只执行一次,而是如何确保 Effect 在重新挂载以后可以正常工作。

如果确实需要在应用加载的时候只执行一次,那么可以参考下面这个示例使用一个顶级变量来记录是否执行过来解决。

let initialized = false;

function App() {
  useEffect(() => {
    if (!initilized) {
      initialized = true;
      // 以下执行需要仅执行一次的功能。
    }
  }, []);
}

依赖一个空数组和没有依赖项的区别

  • 在 Effect 的dependencies中传递一个依赖数组可以使 Effect 监控一些值的变化,并在这些值发生变化的时候重新执行 Effect。如果依赖项中的内容没有发生变化,那么 Effect 就不会执行了。
  • 如果传递一个空数组作为 Effect 的依赖项时,就表示 Effect 没有任何依赖项,这个 Effect 只在初始渲染之后执行。
  • 但如果省略 Effect 的依赖项,那么这个 Effect 就会在每次组件重新渲染的时候都执行一遍。

Tip

注意,在开发模式下,即便是传递空数组作为Effect的依赖项,Effect也会执行两次。

Effect 出现无限循环的执行

在 React 项目开发的过程中,Effect 出现无限循环执行的情况还是非常常见的,要造成这种效果,需要满足两个条件:

  1. Effect 的setup函数中更新了一些 State。
  2. 这些被 Effect 更新的 State 导致了组件的重新渲染,致使 Effect 再次执行。

了解了这个会造成 Effect 无限循环执行的原因,那么你就可以尝试看看如何来解决这个问题了。

两个扩展的 Effect Hook

useEffect Hook 总是在组件渲染完毕即将提交屏幕之前执行,但是有一些操作可能并不需要这个运行时机,所以 React 提供了另外两个 Effect Hook 来扩展 Effect 函数执行的时机。

useLayoutEffect

React 会在运行 Effect 之前先让浏览器绘制出更新后的屏幕,所以如果 Effect 中操作了一些涉及视觉的操作,例如定位组件之类,那么可能会造成屏幕的闪烁,此时可以使用useLayoutEffect来代替useEffect

useLayoutEffect的触发时机在浏览器重新绘制屏幕之前,所以常常用来计算布局。例如:

function Component() {
  const ref = useRef(null);
  const [componentHeight, setComponentHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current?.getBoundingClientRect() ?? 0;
    setComponentHeight(height);
  }, []);
}

useInsertionEffect

useInsertionEffect主要用于 CSS-in-JS 库的开发,允许在布局副作用触发之前将元素插入到 DOM,常常用来在 CSS-in-JS 库中注入动态样式。