Skip to content

useEffect Hook 使用总结(可能是史上最强)

你真的懂闭包吗?

看下面这段代码,会打印什么?如果下面的代码没看懂,说明,说明你和我一样,没弄懂或者理解错了。。。

js
function TestFunctionComponent() {
  let num = 0
  
  function effect() {
    num ++
    const msg = `第${num}条消息`
    return () => {
      console.log(num);
      console.log(msg)
    }
  }

  return effect
}

const effect = TestFunctionComponent()

const unmount = effect()

// 模拟 react 组件的更新时,会重新执行 useEffect 函数。
effect()
effect()
effect()

unmount()

答案:

如果 num 和 msg 分别是什么呢?如果是答案是 4 和第1条消息,那么恭喜你回答正确!

了解闭包后,再来看 React 函数式组件

定义一个 React 函数式组件

js
import { useState, useEffect } from "react";

export function ProjectList(props: any) {
    const [count, setCount] = useState(0);
    /*
        接下来的代码
    */
    return (
        <>
          <button onClick={() => setCount(count + 1)}> 改变 count 的值按钮 </button>
          <button>{count}</button>
        </>
      );
}

其实 useEffect 钩子的用法就 6 种,全都看懂了也就不那么难了。

它们分别是:

  • 只传第一个回调参数 (函数类型)
    • 第一个参数无返回函数
    • 第一个参数有返回函数
  • 传两个参
    • 第一个是回调函数(无返回函数、有返回函数)
    • 第二个是数组(空数组、有值数组)

下面,就对这些情况写一个小 demo,来加深理解。

1 只传第一个回调参数,不返回函数

不传第二个参数,也就说明每次组件更新时 不用比较更新前后的值是否相同,直接执行传入 useEffect 的回调参数。这种写法应用场景比较少,不过需要知道其表现。

js
  // 执行 1+n 次(1 代表第一次挂载,n 代表组件更新的次数)
  useEffect(() => {
   // count 每次都会输出 0
   console.log("打印 1+"+count+"次");
  });

2 只传第一个回调参数,返回函数

应用场景较少

js
  // 执行 n+1 次(n 代表组件更新的次数,1 代表组件卸载了)
  useEffect(() => {
    return () => {
   // count 每次都会输出 0
      console.log("打印 1+n 次", count);
    };
  });

3 传两个参,第一个参数不返回函数,第二个参为空数组

如果加了第二个参数数组, 也就意味着每次组件更新时,都要比较前后的值是否相同。只有不同时才会执行传入 useEffect 的回调参数。

应用场景:在组件挂载后发请求,将数据渲染到页面上。

js
  // 执行 1 次。
  // 如果第二个参数是空数组,意味着前后值的肯定是一样的,只会在组件挂载时执行一次,之后无论组件更新多少次,都不会触发。
  useEffect(() => {
    console.log("参数二 是空数组,只会在挂载时执行一次");
  }, []);

4 传两个参,第一个参数返回函数,第二个参为空数组

应用场景:组件卸载(更新不会触发)时发一个请求。

js
  // 只会在组件卸载时打印一次。
  useEffect(() => {
    return () => {
      console.log(count,'这里一定是 0,无论 count 加了多少');
    }
  },[])

5 传两个参,第一个参数不返回函数,第二个参不为空数组

应用场景:监视响应式变化,触发回调

js
  // 执行 1+n 次。
  // 第二个参数有值,如果组件更新时传入数组前后的值有一个不相同,回调才会执行。基础数据比较值,引用数据会进行完全比较。
  useEffect(() => {
    console.log("参数二 是有值数组,会执行 1 + "+ count +" 次");
  }, [count]);

6 传两个参,第一个参数返回函数,第二个参不为空数组

应用场景:还没想到

js
  // 打印 n+1 次。
  useEffect(() => {
    return () => {
      console.log(count, '如果是更新,打印的是更新前的值(因为更新前要先卸载)。如果是卸载,获取到的是最新的值');
    }
  }, [count]) // 每当 count 变化,回调都会重新执行一遍,返回的函数也就形成了新的闭包

光看不行,还得做个小案例

需求如下,写一个钩子,能改变网页的 title。当组件销毁时,网页的 title 需要恢复到原先的名字。

js
import { useEffect, useRef } from "react";

// export function usePageTitle(title: string) {
//   const oldTitle = document.title;

//   // 相当更新钩子
//   useEffect(() => {
//     document.title = title;
//   }, [title])

//   // 相当于卸载钩子
//   useEffect(() => () => {
//     document.title = oldTitle
//   }, [])
// }

// 有一个钩子,能改变网页的 title。当组件销毁时,网页的 title 需要恢复到原先的名字。

export function usePageTitle(title: string) {
  // 由于闭包,不使用 useRef 也可以。但是 useRef 更直观易懂。
  // const oldTitle = document.title
  
  // useRef 的作用就是保存参数,当函数式组件更新,被重新定义执行后,其值也不会改变
  const oldTitle = useRef(document.title).current

  // 相当更新钩子
  useEffect(() => {
    document.title = title;
  }, [title])

  // 相当于卸载钩子
  useEffect(() => () => {
    document.title = oldTitle
  }, [])
}
js
  const [count, setCount] = useState(0);
  usePageTitle(count + ' title')

最后总结

如果要在组件的首次挂载、或卸载时执行某些操作(副作用),可以这样用 useEffect

js
useEffect(() => {
    // ... 挂载操作
    
    return () => {
        // ... 卸载操作
    }
}, [])

如果要监视一些响应式数据,可以这样使用

js
useEffect(() => {
    // ...
}, [param])

Released under the MIT License.