全面理解useEffect——useEffect用法解析与使用技巧


全面理解useEffect

useEffect可以说是react的hook中最强大的一个,他能够以一种极为优雅的方式模拟我们的生命周期。但是优雅的代价就是这个hook极其的晦涩,不容易被理解与掌握。下面我们就来全方位的探讨一下,useEffect到底怎么用

useEffect的结构

在上一篇关于react的文章我提到过,函数式组件每次reRender都会重新执行一遍,如果我们需要生命周期的效果,那我们就需要useEffect函数。我们先来看一下useEffect的结构:

useEffect(setup, dependencies?)
  • 参数

    • setup:一个实现了你需要的副作用逻辑的函数,这个函数将会在组件首次被绑定以及每次重新渲染导致dependencies更新时调用。同时他还可以return一个清除函数,这个函数将会在函数卸载以及每次重新渲染导致dependencies更新之前调用

    • dependencies:在setup中引用的所有reactive values(state、参数以及在组件内部定义的变量和函数)或更新凭据组成的数组。这个数组必须是有限的并且偏平的,每次更新react将使用Object.is()比较每个依赖项是否与之前相同,如果不同则更新。

  • return

    • undefinded (无返回值)

中文博客中经常会漏掉setup函数的返回值,但是它的返回值是这个函数的半壁江山,我们决不能将其忽略不提,如果忽略掉的话我们就有一半的效果无法实现了。

❗:useEffect也是一个hook,请在组件或者自定义hook的最上层使用,不要在循环或分支结构中定义hook

模拟生命周期

react生命周期大概分为三种,mountupdateunmount,当然在类式组件,我们已经可以规定是在mount之前,mount时还是mount之后调用,在函数式组件我们就不分的这么清楚了,因为在函数式组件我们的生命周期其实是独立的,是面向状态的。

下面我们就使用useEffect简单模拟一下类式组件的生命周期:

只在初始化时执行(DidMounted)

// 不依赖任何变量,则传入一个空数组
useEffect(() => {
    console.log("init");
    return () => {};
  }, []);

每次更新时执行(render)

// 依赖所有变量,则不传入
useEffect(() => {
    console.log("rerender");
  });

每次卸载时执行(beforeUnmount)

// 不依赖任何变量,return的函数将会在卸载之前被调用
useEffect(() => {
    return () => {console.log("unmount");};
  }, []);

实际上,我们经常说useEffect的作用是模拟声明周期,而这似乎违背了函数式组件的初衷,也违背了useEffect的初衷。我们应该多从状态的角度去思考,useEffect并非是什么生命周期函数,而是状态更新函数。

控制更新

如果某个变量在每次组件更新的时候都需要更新,那我们完全没有必要使用useEffect,我们使用副作用钩子一定是因为我们需要控制某个变量的更新。

我们可以使用useEffect组件内部的状态与组件外部系统同步

// 比如说,我们可以在父组件更新props的时候调用useEffect
const Child = ({ count }) => {
  const [childContext, setChildContext] = useState(0);
  useEffect(() => {
    setChildContext(count);
  }, [count]);
  return <p>this is a child, count = {childContext}</p>;
};

或者我们可以使用useEffect组件组件外部系统与内部的状态同步

import "./styles.css";
import { useEffect, useState } from "react";

const fetch = async (count) => {
  // 假设这是一个网络请求
  // 当然,这个外部状态也可以是一个外部组件,外部变量,jsAPI等等
  await new Promise(() => {
    setTimeout(() => {
      console.log("fetch date");
    }, count);
  });
};

export default function App() {
  const [count, setCount] = useState(0);
  const onClick = () => {
    setCount(count + 1);
  };
  // 每次修改count都会将更改同步外部系统
  useEffect(() => {
    fetch(count);
  }, [count]);
  return (
    <div className="App">
      <button onClick={onClick}>Change state in father</button>
      <p>count is {count}</p>
    </div>
  );
}

其实生命周期也不过是对状态的统一更改,react现在只是将生命周期将颗粒度从组件降低到了状态。

一些tips

在useEffect中使用state

有时候我们可能会需要在useEffect中使用和修改state,但是这是极端危险的行为,因为当我们修改state时会触发页面的rerender,然后因为useEffect依赖了state,这将会导致state被再次更改继而引起死循环……

const Child = ({ count }) => {
    const [childContext, setChildContext] = useState(0);
    useEffect(() => {
        setChildConte(childContext);
    }, [count, childContext]);
    // !! 千万不要这么做,会引发死循环
    return <p>this is a child, count = {childContext}</p>;
};

如果我们在useEffect中根据之前的state更新state,我们可以这么写:

useEffect(() => {
  setChildContext(c => c + 1);
}, [count]);
// 不需要依赖childContext,安全

如果你真的既要在Effect中读取state,又要修改state,那么我给出的建议是,:使用两个依赖同一个更新凭证useEffect

千万别这么做:

useEffect(() => {
  setChildContext(Math.random())
  console.log(childContext)
}, [count, childContext])
// 死循环!!😭

正确的做法如下:

useEffect(() => {
  setChildContext(Math.random())
}, [count]);
useEffect(() => {
  console.log(childContext)
}, [count, childContext])
// 安全😊

消除不必要的对象与函数依赖

如果我们在组件内定义了一个函数或对象,我们在上一篇文章详细讲过,它每次都会被初始化。就像这样:

const childContext = {
    count: 1
}
useEffect(() => {
    childContext.count = count;
    console.log("run effect")
    // 不要这样,每次Render都会触发effect😔
}, [count, childContext]);

取而代之的是,如果你的对象或者函数只在useEffect中使用,则写在setup内部

const Child = ({count}) => {
  useEffect(() => {
    const childContext = {
      count: 1
    }
    childContext.count = count;
    console.log("run effect")
  }, [count]);
  // 推荐,只有在依赖更新的时候才会触发effect 😊
  return <p>this is a child</p>;
};

如果你是希望有一个在整个组件都可以被访问的静态变量,那么可以将对象写在组件外部:

const childContext = {
  count: 1
}
const Child = ({count}) => {
  useEffect(() => {
    childContext.count = count;
    console.log("run effect")
  }, [count]);
  // 推荐,只有在依赖更新的时候才会触发effect 😊
  return <p>this is a child</p>;
};

useLayoutEffect

我们在使用useEffect的时候有时会出现一种问题:我们的组件会发生闪烁或突然的变化。

这时因为useEffect是在mount之后执行的,因此第一次渲染出的值是我们定义的初始值,如果更新的性能比较差或者更新的范围比较大,可能会导致我们的view发生闪烁的现象。

这个时候我们就可以使用useLayoutEffect,他和useEffect的最大区别就是他是在mount之前执行的,所以不会导致view的闪烁。

但是如果我们没有很明显的ui闪烁问题,我们应该尽可能的使用useEffect,因为它的运行效率更好。

发布于:
编辑于: