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 #25

Open
hacker0limbo opened this issue Jan 18, 2022 · 0 comments
Open

简单聊一聊 useEffect #25

hacker0limbo opened this issue Jan 18, 2022 · 0 comments
Labels
react react 笔记整理

Comments

@hacker0limbo
Copy link
Owner

hacker0limbo commented Jan 18, 2022

翻到几篇文章, 发现之前对于 useEffect 这个 hook 的认识是有问题的, 这里稍微重新整理一下

基本点

官网其实对 useEffect 的讲解已经很清晰了:

Sometimes, we want to run some additional code after React has updated the DOM.

对于这么一个函数:

useEffect(() => {
  // effect
  return () => {
    // cleanup
  };
}, [deps]);

有这么几个点:

  • 传递给 useEffect 的回调函数的调用在 render phase 之后, 并且是在渲染至屏幕之后, 这里与 useLayoutEffect 不同, 不展开
  • dependent array 即依赖数组决定该回调函数触发的时机
  • 返回的 cleanup 函数不仅仅只是在组件 unmount 的时候触发, 如果该组件存在多次 render 的情况, 也会触发, 同时触发时机在"下次" useEffect 的回调函数触发之前

虽然 hooks 官方并没有给出生命周期图, 但是网上有人制作了一份还是很清晰的:

react-hooks-lifecycle

示例

以最基本的计数器为例, 如下:

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

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

  useEffect(() => {
    console.log('effect');

    return () => {
      console.log('cleanup');
    };
  }, [count]);

  console.log('render');

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

export default App;

过程简化如下:

  1. 初始 mount 的时候, 先 render 一次
  2. 执行 useEffect 里的回调
  3. 用户点击按钮, 状态更新, 重新 render 一次
  4. 由于依赖数组的状态也是 count 状态, 执行 useEffect 里的返回函数
  5. 执行 useEffect 里的回调

因此控制台的输出结果依次如下

// mount 之后
render
effect
// 用户点击按钮触发更新之后
render
cleanup
effect

效果图如下:
overall

每次都运行

当没有依赖数组的时候, useEffect 里的回调函数在每次 render 的时候都会执行, 即初始 mount 的时候以及后续组件状态改变重新 re-render 的时候

useEffect(() => {
  console.log('I run on every render: mount + update.');
})

只有 mount 才运行

当依赖数组是空时, useEffect 里的回调函数只在第一次 render 即 mount 之后执行

useEffect(() => {
  console.log('I run only on the first render: mount.');
})

mount 和特定 update 运行

当依赖数组里存在依赖项, 例如组件的 state 或者 props, 当其中任一一个依赖项发生变动时, useEffect 执行其中的回调函数

useEffect(() => {
  console.log('I run if count change (and on mount).');
}, [count])

只有特定 update 运行

这个需求是我需要 console.log() 这条语句只在组件更新的时候触发, 组件第一次渲染 mount 的时候不触发, 实现也很简单, 通过 ref 或者全局变量的状态控制

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

function App() {
  const [count, setCount] = useState(0);
  const didMountRef = useRef(false);

  useEffect(() => {
    if (didMountRef.current) {
      console.log('I run only if count changes, mount is not included');
    } else {
      didMountRef.current = true;
    }
  }, [count]);

  console.log('render');

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

export default App;

当然这里 useEffect 依旧会在 mount 之后执行, 只是里面的判断使得有条件的执行

更具体点的可以抽成一个自定义 hook, 如下:

const useEffectOnlyOnUpdate = (callback, dependencies) => {
  const didMountRef = React.useRef(false);

  React.useEffect(() => {
    if (didMountRef.current) {
      callback(dependencies);
    } else {
      didMountRef.current = true;
    }
  }, [callback, ...dependencies]);
};

组里里使用:

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

  useEffectOnlyOnUpdate(
    (dependencies) => {
      console.log('I run only if count changes, mount is not included');
    },
    [count]
  );

  console.log('render');

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

export default App;

usePrevious

由于 useEffect 里的回调函数是在 render 之后进行的, 可以根据这一个机制获取到之前的状态的值.

const usePrevious = value => {
  const previousRef = useRef()

  useEffect(() => {
    previousRef.current = value
  }, [value])

  return previousRef.current
}

使用:

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

  const previousCount = usePrevious(count);

  console.log(
    `render, previousCount: ${previousCount}, currentCount: ${count}`
  );

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

export default App;

打印结果为:

// mount 之后
render, previousCount: undefined, currentCount: 0
// 用户点击按钮触发一次更新之后
render, previousCount: 0, currentCount: 1

效果如下:
usePrevious

需要注意的是, 这里的实现不一定需要强制使用 ref, 用一个全局变量也是一样的, 只是 ref 提供了一样的功能, 即 useRef 提供的引用在整个组件的声明周期都是持久并且是最新的. ref 一个比较好的作用就是能够解决 hooks 的陈旧闭包问题, 之前的博客里有写, 这里不多描述, 有兴趣可以看这个问题以及我之前的文章

cleanup

如上述提到的, cleanup 函数不仅仅是在 unmount 才执行, 每次组件更新的时候也会执行, 用于消除上次存在的不用资源

以实现自增器为例, 其实可以有很多种写法, 第一种如下:

import React from 'react';

const App = () => {
  const [timer, setTimer] = React.useState(0);

  React.useEffect(() => {
    const interval = setInterval(() => setTimer(timer + 1), 1000);

    return () => {
      clearInterval(interval);
    };
  }, [timer]);

  return <div>{timer}</div>;
};

export default App;

这里每次更新导致 useEffect 执行都会创建一个新的 interval, 而每次的 cleanup 函数都用于清除上次运行时创建的定时器. 这么做虽然没问题但是他的机制在于, 每次都会创建新的定时器, 而我们所需要的往往只是一个定时器实例, 并且每次定时器只跑了一秒就被清除

更好的做法是稍微修改一下依赖数组:

import React from 'react';

const App = () => {
  const [timer, setTimer] = React.useState(0);

  React.useEffect(() => {
    const interval = setInterval(
      () => setTimer((currentTimer) => currentTimer + 1),
      1000
    );

    return () => clearInterval(interval);
  }, []);

  return <div>{timer}</div>;
};

export default App;

保证在 mount 之后只存在一个定时器实例, 同时该定时器在组件 unmount 时也能被销毁.

当然上述需求用 setTimeout 实现也是可以的:

import React from 'react';

const App = () => {
  const [timer, setTimer] = React.useState(0);

  React.useEffect(() => {
    setTimeout(() => {
      setTimer((t) => t + 1);
    }, 1000);
  }, [timer]);

  return <div>{timer}</div>;
};

export default App;

参考

@hacker0limbo hacker0limbo added the react react 笔记整理 label Jan 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
react react 笔记整理
Projects
None yet
Development

No branches or pull requests

1 participant