React深入useEffect

2021-12-23 前端早茶

收錄於話題 #React系列 6個

本文適合熟悉React、以及在用useEffect遇到難題的小夥伴進行閱讀。

歡迎關注前端早茶,與廣東靚仔攜手共同進階~

作者:廣東靚仔

一、前言

本文基於開源項目:

https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js

    今年廣東靚仔報名了軟考,業餘把精力更多投入到複習中。由於疫情影響,今天廣州區暫停了軟考上半年的相關科目,廣東靚仔又來寫文章了。    廣東靚仔將從三個方面來梳理useEffect相關內容:相信有不少小夥伴在使用useEffect過程中遇到過不少問題,廣東靚仔找來了幾個有bug的例子:
// 彈框顯示觸發定時器
useEffect(() => {
  timer = setInterval(() => {
    if (showModal) {
      requestFun()
    }
  }, 1000)
}, [showModal])  
// 關閉彈框,清除定時器
const closeModal = () => {
  clearInterval(timer)
}

useEffect(() => {
  let intervalId = setInterval(() => {
    fetchData();
  }, 1000 * 60);
  return () => {
    clearInterval(intervalId);
    intervalId = null;
  }
}, [])
const fetchData = () => {
   request({params}).then(ret => {
      if (ret.code === OK) {
          applyResult(ret.data);
      }
   })
}

當我們在useEffect調用第三方庫的實例,然後在其他函數清除這個實例,發現無法清除。其他的小夥伴在用useEffect還遇到過其他的問題,這裡就不一一展開,閱讀完這篇文章後,一定會對useEffect有一個全面的理解。二、useEffect介紹     React16.8版本中描述了在 React 渲染階段,改變 DOM、添加訂閱、設置定時器、記錄日誌以及執行其他包含副作用的操作是不被允許的,因為可能會產生莫名其妙的 bug 並破壞 UI 的一致性。    因此在使用useEffect完成副作用操作,賦值給useEffect的函數會在組件渲染到屏幕之後執行。useEffect一般是在每輪渲染結束後執行,當然我們也可以讓它在只有某些值改變的時候才執行。useEffect有個清除函數,官方demo如下:
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除訂閱
    subscription.unsubscribe();
  };
});

一般在執行一些計時器或者訂閱,我們會在組件卸載後,會清除這些內容。因此可以在清除函數裡面做這些操作。
useEffect為防止內存洩漏,一般情況下如果組件多次渲染,在執行下一個effect 之前,上一個 effect 就已被清除。也就是說組件的每一次更新都會創建新的訂閱。useEffect 的函數會在瀏覽器完成布局與繪製之後,在一個延遲事件中被調用。我們都知道一旦 effect 的依賴發生變化,它就會被重新創建,例如:
useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

 useEffect傳遞第二個參數,它是 effect 所依賴的值數組。只有當依賴改變後才會重新創建訂閱。溫馨提示:有很多小夥伴在日常項目開發的時候,使用這個依賴的時候,很容易留下bug。比如:一個編輯彈框功能,如果useEffect依賴只寫了個id,這個時候如果是對同一條數據進行編輯是不會再次執行useEffect的邏輯的。三、useEffect原理

useEffect實際上是ReactCurrentDispatcher.current.useEffect(源碼解析會講到)

useEffect原理可以簡單理解為:

函數組件在掛載階段會執行MountEffect,維護hook的鍊表,同時專門維護一個effect的鍊表。在組件更新階段,會執行UpdateEffect,判斷deps有沒有更新,如果依賴項更新了,就執行useEffect裡操作,沒有就給這個effect標記一下NoHookEffect,跳過執行,去下一個useEffect。

我們都知道useEffect 在依賴變化時,執行回調函數。這個變化是指本次 render 和上次 render 時的依賴之間的比較。

默認情況下,effect 會在每輪組件渲染完成後執行,而且effect 觸發後會把清除函數暫存起來,等下一次 effect 觸發時執行,大概過程如下:


溫馨提示:使用 hooks 要避免 if、for 等的嵌套使用

四、useEffrct源碼解析

在react源碼中,我們找到react.js中如下代碼,篇幅有限,廣東靚仔進行了簡化,方便小夥伴閱讀:

4.1 useEffect引入與導出

import {
  ...
  useEffect,
  ...
} from './ReactHooks';

// ReactHooks.js
export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  if (__DEV__) {
    if (dispatcher === null) {
     // React版本不對或者Hook使用有誤什麼的就報錯...
    }
  }

  return ((dispatcher: any): Dispatcher);
}

上面的代碼就是引入與導出過程,不難看出useEffect實際上是ReactCurrentDispatcher.current.useEffect橙色的代碼。

import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;

current的類型是null或者Dispatcher,不難看出接下來我們要找類型定義

// ReactInternalTypes.js
export type Dispatcher = {|
  useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
|};

4.2 組件加載調用mountEffect

函數組件加載時,useEffect會調用mountEffect,接下來我們來看看mountEffect

// ReactFiberHooks.new.js
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
    return mountEffectImpl(
      PassiveEffect | PassiveStaticEffect,
      HookPassive,
      create,
      deps,
    );
  }

PassiveEffect和PassiveStaticEffect是二進位常數,用位運算的方式操作,用來標記是什麼類型的副作用的。mountEffect走了mountEffectImpl方法

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

上面代碼中,往hook鍊表裡追加一個hook,把hook存到鍊表中以後還把pushEffect的返回值存了下來。

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy, // mountEffectImpl傳過來的是undefined
    deps,
    next: (null: any),
  };
  // 一個全局變量,在renderWithHooks裡初始化一下,存儲全局最新的副作用
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // 維護了一個副作用的鍊表,還是環形鍊表
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      // 最後一個副作用的next指針指向了自身
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

最後返回了一個effect對象。

Tips: mountEffect就是把useEffect加入了hook鍊表中,並且單獨維護了一個useEffect的鍊表。

4.3 組件更新時調用updateEffect

函數組件加載時,useEffect會調用updateEffect,接下來我們來看看updateEffect

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 獲取當前正在工作的hook
  const hook = updateWorkInProgressHook();
  // 最新的依賴項
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    // 上一次的hook的effect
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 比較依賴項是否發生變化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果兩次依賴項相同,componentUpdateQueue增加一個tag為NoHookEffect = 0 的effect,
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  // 兩次依賴項不同,componentUpdateQueue上增加一個effect,並且更新當前hook的memoizedState值
  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

從上面代碼中我們看到areHookInputsEqual用來比較依賴項是否發生變化。下面我們看看這個areHookInputsEqual函數

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
 
  if (prevDeps === null) {
    ...
    return false;
  }

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

上面代碼中,廣東靚仔刪掉了一些dev處理的代碼,不影響閱讀。

其實就是遍歷deps數組,對每一項執行Object.is()方法,判斷兩個值是否為同一個值。

以上內容是源碼中的一部分,如果感興趣的小夥伴可以到react倉庫進行閱讀~

五、總結    在我們閱讀完官方文檔後,我們一定會進行更深層次的學習,比如看下框架底層是如何運行的,以及源碼的閱讀。在看源碼前,我們先去官方文檔複習下框架設計理念、源碼分層設計藉助框架的調用棧來進行源碼的閱讀,通過這個執行流程,我們就完整的對源碼進行了一個初步的了解接下來再對源碼執行過程中涉及的所有函數邏輯梳理一遍

關注我,一起攜手進階

如果這篇文章有觸動到你,歡迎關注前端早茶,與廣東靚仔攜手共同進階~

相關焦點

  • React深入useState/setState
    reduce = () => {  setTimeout(() => {    console.log('前端早茶, 前age:', this.state.age)    this.setState({      age: this.state.age + 1    });    console.log
  • 【Hooks】:[組]Awesome React Hooks
    We』d use the React local state to keep the current window width, and use a side effect to set that state when the window resizes:
  • 超性感的React Hooks(三):useState
    函數式組件接收props作為自己的參數import React from 'react';interface Props { name: string, age: number}function Demo({ name, age }: Props) { return [ <div>name: {
  • 【Hooks】:[組]How to useReducer in React
    Part 2: How to useReducer in React? 2.1. Reducer in React  2.2. React's useReducer HookPart 3: What about useState? Can’t we use that instead?Part 1: What is a reducer in JavaScript?
  • React、Vue我全都要!React Hook 實現 Vue 的11個基本功能
    ,這一個hook在修改常量的時候比較簡單,但是在修改引用 對象 或者 數組 的時候就需要先進行 淺拷貝 再進行覆蓋修改import { useState } from 'react'function Demo() {  const [msg, setMsg] = useState('我是菜鳥')
  • React 輪播動畫探索
    既然沒有現有的組件可以復用,我們可以怎麼另闢蹊徑呢?接下來就來到本文的正題了,我們來通過一個神奇的 React 動畫庫來實現我們的需求。2. react-transition-groupreact-transition-group 是 React 官方實現的,用於操作過渡效果的組件庫。它可以在組件安裝和卸載時,增加過渡效果。一共提供了 4 個 api,上手成本極低。
  • 從 Vue2.0 到 React17 —— React 開發入門
    import { useState } from 'react';import HelloWorld from '.import { useState } from 'react';export default function HelloWorld() {  const [title,setTitle] = useState('hello world');  const [className,setClassName] = useState
  • 60+ 實用 React 工具庫,助力你高效開發!
    npm地址:https://www.npmjs.com/package/formik3. react-use-form-statereact-use-form-state是一個小型 React Hook,它使用原生表單輸入元素來簡化管理表單的狀態。
  • React-Router v6 新特性解讀及遷移指南
    用useNavigate代替useHistory。新鉤子useRoutes代替react-router-config。1.不需要任何useRouteMatch()!// v5import { useHistory } from 'react-router-dom';function MyButton() { let history = useHistory(); function handleClick() { history.push('/home'); }; return <button
  • 手寫React-Router源碼,深入理解其原理
    本文會繼續深入React-Router講講他的源碼,套路還是一樣的,我們先用官方的API實現一個簡單的例子,然後自己手寫這些API來替換官方的並且保持功能不變。看起來我們要搞懂react-router-dom的源碼還必須得去看react-router和history的源碼,現在我們手上有好幾個需要搞懂的庫了,為了看懂他們的源碼,我們得先理清楚他們的結構關係。
  • 一名 Vue 程式設計師總結的 React 基礎
    要注意的一點是,即使 Hooks 不需要寫 render, 沒有用到 React.xxx,組件內還是要import React from "react";的(至於原因,後續深入 Hooks 學一下,大哥們也可以解釋下)。React 官方也說了,後續的版本會優化掉這一點。
  • 精通react/vue組件設計之配合React Portals實現一個(Drawer)組件
    通過組件的設計過程,大家會接觸到一個完成健壯的組件設計思路和方法,也能在實現組件的過程逐漸對react/vue的高級知識和技巧有更深的理解和掌握,並且在企業實際工作做遊刃有餘.作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少了工作中大量的冗餘代碼, 一切皆組件的思想深得人心.
  • 【英】新版 React DevTools 簡介
    react-domreact-nativeHow do I get the new DevTools?React DevTools is available as an extension for Chrome and Firefox.
  • React v17.0 正式發布!
    安裝使用 npm 安裝 React v17:npm install react@17.0.0 react-dom@17.0.0使用 yarn 安裝 React v17:yarn add react@17.0.0 react-dom@17.0.0我們還提供了由 UMD 構建的 CDN 版本:<script crossorigin src="https://unpkg.com/react@17.0.0/umd/react.production.min.js
  • Knock-on effect
    of people giving up smoking has been a major factor in the smoking decline over recent decades – what does 「the knock-on effect」 mean?
  • React 靈魂 23 問,你能答對幾個?
    :2、聊聊 react@16.4 + 的生命周期相關連接:React 生命周期 我對 React v16.4 生命周期的理解3、useEffect(fn, []) 和 componentDidMount 有什麼差異?useEffect 會捕獲 props 和 state。
  • Side effect? 負面效應
    That’s the indirect or unintended consequence of Trumpism, 「side effect」 being the effect or consequence that happens on the SIDE, alongside the main or intended effect.
  • React開發需要熟悉的JavaScript特性
    React Hooks: https://reactjs.org/hooks  const greeting = 'Hello'const subject = 'World'console.log(`${greeting} ${subject}!
  • React Native 0.62 發布,默認支持 Flipper,新的暗黑模式
    colorScheme = Appearance.getColorScheme();if (colorScheme === 'dark') { // Use dark color scheme}同時還添加了一個 hook 跟蹤用戶首選項的狀態更新:import {Text, useColorScheme
  • 「譯」React Router v6 中非常實用的4個API
    OutletforwardRefuseRoutesuseLocation不論您是React的初學者,還是已經對React了如指掌,相信以下內容都會給予您一定的幫助。} from 'react-router-dom';function App() { let element = useRoutes([ { path: '/', element: <Home /> }, { path: 'users', element: <Users />, children