React useEffect中使用定時器所產生的閉包陷阱

前言

其實關於這個問題在知乎和百度上都有說起,可是在掘金上卻沒有詳細的文章,所以準備出一篇文章來解決在useEffect中如何使用定時器。有一篇文章寫得特別好,若是你們想深刻理解能夠點擊此連接 使用 React Hooks 聲明 setIntervalcss

眉頭一皺,發現問題並不簡單

首先讓咱們作一個小Demo,設置一個名爲value的state,並每隔5秒,產生一個隨機數,並讓value加上這個數react

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

import './App.css';

function App() {
  const [value, setValue] = useState<number>(0);
  useEffect(() => {
    const timer: NodeJS.Timeout = setInterval(() => {
      const random = (Math.random() * 10) | 0;

      setValue(value + random);
    }, 5000);

    return () => {
      clearInterval(timer);
    };
  }, []);
  return <div>{value}</div>;
}

export default App;

複製代碼

你自信滿滿的打開了瀏覽器,too simple! 忽然發現了value的值只發生了一次變化,你檢查發現,原來useEffect的依賴性並無填入,因而你當心翼翼的將value填入依賴性數組

useEffect(() => {
    // .....
  }, [value]);
複製代碼

perfect!,代碼如期執行,每隔5秒都會增長一個值,內心想着‘不愧是我!’瀏覽器

你覺得的你覺得的不是你覺得的

若是咱們在代碼中加入一個定時器數組,用來記錄你添加了多少個定時器閉包

function App() {
  const [value, setValue] = useState<number>(0);
  const [timers, setTimers] = useState<Array<NodeJS.Timeout>>([]);
  useEffect(() => {
    const timer: NodeJS.Timeout = setInterval(() => {
      const random = (Math.random() * 10) | 0;
      setValue(value + random);
    }, 5000);
    timers.push(timer);
    setTimers(timers);
    console.log(timers);
    return () => {
      clearInterval(timer);
    };
  }, [value]);
  return <div>{value}</div>;
}

複製代碼

你會驚訝的發現你不只value發生變化了,並且又多生成了一個定時器。以下圖所示。 dom

這對於瀏覽器性能來講是絕對不能夠接受的,那這是爲何產生的呢, 產生的緣由是由於useEffect 在第一次渲染時獲取值爲 0 的 value,將再也不次執行 effect,因此 setInterval 一直引用第一次渲染時的閉包 value,所以每次都是在0的基礎上添加一個隨機數,而不是依次累加,最關鍵的是若是你開啓了Vscode的eslint插件,它還會給你自動補齊依賴項, 這裏推薦一篇關於react-hooks閉包問題的文章 陳舊閉包問題,解釋的比較清楚。

不要慌,問題不大

爲了解決這個問題,咱們須要引入react中另一個hook ,useRef。useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變,利用這個特性,咱們把它用在咱們的demo中看看效果性能

function App() {
  const [value, setValue] = useState<number>(0);
  const [timers, setTimers] = useState<Array<NodeJS.Timeout>>([]);
  const saveCallBack: any = useRef();
  const callBack = () => {
    const random: number = (Math.random() * 10) | 0;
    setValue(value + random);
  };
  useEffect(() => {
    saveCallBack.current = callBack;
    return () => {};
  });
  useEffect(() => {
    const tick = () => {
      saveCallBack.current();
    };
    const timer: NodeJS.Timeout = setInterval(tick, 5000);
    timers.push(timer);
    setTimers(timers);
    console.log(timers);
    return () => {
      clearInterval(timer);
    };
  }, []);
  return <div>{value}</div>;
}
複製代碼

使用了useRef後,定時器不會被重複建立,可是value的值變成了依次累加,達到了預期的效果,真是讓人神清氣爽,so cool!spa

相關文章
相關標籤/搜索