自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深度探討 useEffect 使用規(guī)范

開發(fā) 后端
在 Vue 和 Mobx 里都有計算屬性這樣的概念。因此有的人就想,在 React Hook 中,是否可以借助 useEffect 來達到計算屬性的目的。

我們在乘坐地鐵或者公交車的時候,在地鐵門或者公交車門的旁邊往往會有「禁止依靠」這樣的標(biāo)識。目的是為了警告乘客不要依靠車門,否則開門的時候容易出現(xiàn)不可預(yù)測的危險。

但是如果我們仔細去分析這個危險的話,就會知道,他的真實情況是,在車輛運行過程中,車門緊閉,你依靠在車門上也并不會出現(xiàn)危險,我們在上班高峰期擠地鐵的時候,大量的人也不得不緊靠車門,甚至有的人被擠扁壓在車門上。

在制定團隊項目規(guī)范時也會這樣,例如,我在帶領(lǐng)團隊時,一定會制定一條規(guī)范,要求每次代碼提交之前,個人必須檢查你的代碼里是否存在意外的修改,可能有的人在提交之前手抖往代碼里輸入了一個空格或者逗號,從而導(dǎo)致重大事故。這是一個低概率發(fā)生的事情,但是我仍然會要求每次提交都要檢查。

那么,是不是也就意味著,如果不遵守我這個規(guī)范,就一定會發(fā)現(xiàn)不可預(yù)測的重大事故呢?其實并非如此,我們制定規(guī)范的目的是為了讓程序變得可控,比如團隊里面有10個人的習(xí)慣都比較好,從來不會出現(xiàn)意外內(nèi)容提交的情況,但是只要有一個人出現(xiàn)兩次因為手抖把意外的修改提交到了代碼倉庫,那么這條規(guī)范就會出現(xiàn)。

既然這條規(guī)范的出現(xiàn)是為了避免意外的發(fā)生,于是有一個項目成員就對我的規(guī)范提出了質(zhì)疑,他認為可以在配置上增加 pre-commit 的代碼規(guī)則檢測,如果有意外的發(fā)生,那么代碼規(guī)則檢測不會通過,我們就不用每次在提交之前花費心力去檢查每一條 diff 里的修改了。

雖然我最終沒有同意他的提議,但這是一個非常好的思路

所以作為一個優(yōu)秀的開發(fā)者,我們到底是應(yīng)該只要遵循規(guī)范就是完事了,還是應(yīng)該去看懂規(guī)范出現(xiàn)的背后邏輯,從而靈活的運用他,或者探尋更好的解決方案呢?

我的答案是后者。

如果在這個觀念的基礎(chǔ)之上我們能達成共識,我們再來一起結(jié)合 React 官方文檔,對 useEffect 的使用規(guī)范進行深入探討。

在這之前,我們要首先明確一下 useEffect 的語法規(guī)則,useEffect 的依賴項必須是 state 與 props,否則依賴項發(fā)生了變化,effect 也不會執(zhí)行。所以有的人說:我不愿意把 state 放到依賴項里,甚至反感這樣的行為,我認為是沒有任何理論依據(jù)的。

一、計算屬性

在 vue 和 mobx 里都有計算屬性這樣的概念。因此有的人就想,在 react hook 中,是否可以借助 useEffect 來達到計算屬性的目的。

官方文檔明確的建議是,不需要這樣做

// 案例來自官方文檔
// https://zh-hans.react.dev/learn/you-might-not-need-an-effect
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // ?? 避免:多余的 state 和不必要的 Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

實際上 react 的 re-render 機制表示 react hook 本身已經(jīng)具備了計算屬性的特性。當(dāng) state 發(fā)生變化,函數(shù)會重新執(zhí)行,內(nèi)部的代碼也會重新執(zhí)行,因此案例中的 fullName 就有一次重新計算結(jié)果的機會

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ? 非常好:在渲染期間進行計算
  const fullName = firstName + ' ' + lastName;
  // ...
}

因此我們不必借助 useEffect 來實現(xiàn)計算屬性,這是非常好的建議。

二、緩存計算結(jié)果

但是如果情況發(fā)生一些變化呢?fullName 這個案例的計算過程非常簡單,如果這個計算過程非常復(fù)雜需要花費大量的時間呢?此時我們就不得不考慮要減少這個計算過程,只在他需要重新計算的時候計算一次

這個時候,這個案例的解決方案就不再適用了,他只適合簡單的運算過程。復(fù)雜運算過程我們還需要借助別的手段來緩存計算結(jié)果。那么使用 useEffect 是否合適?

不合適。官方文檔中,提供了一個更適合的 hook:useMemo 來緩存運算結(jié)果。

但是為什么呢?

因為執(zhí)行時機的問題。事實上,useEffect 和 useMemo 都有記憶能力,他們的底層實現(xiàn)有部分相似之處,但是有一個不同之處導(dǎo)致了他們的差別非常大,那就是傳入的第一個參數(shù)的執(zhí)行時機。useMemo 在發(fā)現(xiàn)依賴項有改變之后,會立即重新運算緩存的函數(shù)并返回運算結(jié)果。但是 useEffect 則需要等待組件渲染完整之后才會開始執(zhí)行緩存的函數(shù)。類似于

setTimeout(effect, 0)

也就意味著,當(dāng)前一輪執(zhí)行的 JSX 中無法得到 useEffect 的運算結(jié)果。除非我們將運算結(jié)果存儲在一個 state 中,讓 state 發(fā)生改變而得到一輪新的 render。

因此在這種場景之下,useMemo 會比 useEffect 更快更合適。

官方文檔給我們提供了一個案例。

現(xiàn)在有一個復(fù)雜列表 todos,然后還有一個過濾條件 filter,todos 和 filter 運算之后可以得到一個列表 visibleTodos。

const visibleTodos = getFilteredTodos(todos, filter);
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // ?? 避免:多余的 state 和不必要的 Effect
  // 假設(shè) JSX中使用了 visibleTodos
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}

正是由于使用了 useEffect,因為執(zhí)行時機的問題,如果不將運算結(jié)果存儲在 state 中,當(dāng)前一輪的 render,在 JSX 中無法得到新的運算結(jié)果,因此只有通過 state 的重新出發(fā)一次 render 的機會讓渲染結(jié)果保持最新。

所以不推薦使用 useEffect,直接去掉就行了

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ? 如果 getFilteredTodos() 的耗時不長,這樣寫就可以了。
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

但是由于此案例中設(shè)定的是 getFilteredTodos 是一個耗時操作,因此我們需要使用 useMemo 來緩存他的運算結(jié)果。這樣就可以做到當(dāng)其他 state 發(fā)生變化時,getFilteredTodos 不會重新執(zhí)行。

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // ? 除非 todos 或 filter 發(fā)生變化,否則不會重新執(zhí)行
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

這個案例充分說明了 useMemo 的作用。

但是案例有一些不太合理的地方。例如,todos 和 fitler 都是外部傳入的 props,也就是說,下面這一行代碼更合理的方案是在組件外部計算好,因為他運算所需的條件都是外部條件。

const visibleTodos = getFilteredTodos(todos, filter);

這樣我們就完全不需要考慮因為 re-render 而處理他的冗余運算成本問題了。因為他的運算次數(shù)將會嚴格和 todos、filter 的變化保持一致。

三、與事件相關(guān)的爭議

現(xiàn)在我們來思考一個類似的交互方案,依然是一個任務(wù)列表

給他們設(shè)定一個過濾條件,類別,例如有兩個類別是工作類與旅游類,當(dāng)類別發(fā)生變化的時候,部分任務(wù)會隱藏

此時你就會發(fā)現(xiàn)一個問題,如果類別也需要在 UI 中進行顯示,那么我們就不得不把類別這個過濾條件存放在 state 中去觸發(fā) UI 的變化,與此同時,類別的變化還會導(dǎo)致 todos 也發(fā)生變化

這個時候就存在兩種比較有爭議的寫法

第一種寫法完全更符合語義和解耦的思考。從語義上來說,當(dāng)我們點擊了單選按鈕切換了類別,此時只需要修改 fitler 即可,因為我們只做了這一個操作。但是 filter 的修改,還會造成別的改動:列表也會發(fā)生變化,這是一種額外的副作用。因此我們使用 useEffect 來處理這部分副作用邏輯。

從解耦的角度來說,當(dāng)點擊切換按鈕時,我們不需要關(guān)注額外的邏輯,這對于開發(fā)而言是一種理解上的簡化,因為我們在點擊時只需要關(guān)注按鈕本身,而不需要關(guān)注按鈕切換之后的后續(xù)變化。這種解耦思路更有利于后續(xù)的封裝

副作用:我的修改,導(dǎo)致了除我之外的 UI 發(fā)生變化

function TodoList({ todos }) {
  const [newTodos, setNewTodos] = state(todos)
  const [fitler, setFilter] = state(1)

  useEffect(() => { }, [
    setNewTodos(getFilteredTodos(todos, filter));
 ], [filter])

  function onFilterChange(value) {
    setFilter(value)
  }
}

但是這種更符合語義和解耦的方案,違背了剛才的規(guī)范。因為我們使用 useEffect 去監(jiān)聽一個 state,修改另外一個 state。不過剛才的規(guī)范的目的之一,是為了避免出現(xiàn)冗余的 state,本案例里面并沒有冗余的 state,filter 也是必須存在的。那么看上去前提條件跟規(guī)范有一些出入

于是,React 官方文檔還存在另外一條規(guī)范

react 官方文檔把 useEffect 稱為一種逃離方案「逃生艙」,我們應(yīng)該在最后使用它。因此在這個情況下,官方文檔建議把邏輯放到事件中處理,而不是 useEffect。

在這個案例中,下面的寫法是官方文檔更推薦的寫法

function TodoList({todos}) {
  const [newTodos, setNewTodos] = state(todos)
  const [fitler, setFilter] = state(1)

  function onFilterChange(value) {
    setFilter(value)
    setNewTodos(getFilteredTodos(todos, value))
  }
  // ...
}

使用 useEffect 雖然更符合語義和解耦,但是他會額外執(zhí)行一次 render,因此在執(zhí)行速度上,這種寫法是更快的。

useEffect 有更復(fù)雜的執(zhí)行邏輯,如果你對其掌握得不夠準確時,他很容易導(dǎo)致你的程序出現(xiàn)一些你無法理解的迷惑現(xiàn)象,因此在這兩個基礎(chǔ)之上,react 官方文檔的意思就是,useEffect 能不用就不用。

但是如果我們已經(jīng)對 useEffect 的運行機制非常清楚,并且他使用他付出的代價只是一次 re-render,我會更傾向于選擇前者:更符合語義、解耦好更利于封裝,而不是嚴格遵守規(guī)范。

事實上,只要你不亂來,一次 re-render 的成本很低,除非是在一些特殊場景,例如渲染大量的 input 或者高頻渲染

如果在性能上還有爭議的話,那么接下來我們把本次案例進行一個修改,新修改的交互將會更容易出現(xiàn)在我們的實踐中。

當(dāng)過濾條件發(fā)生變化,新的列表并不是從本地數(shù)據(jù)中運算得來,而是接口從服務(wù)端獲取。

那么兩種寫法的代碼就會變成

// 使用 useEffect
function TodoList({ todos }) {
  const [newTodos, setNewTodos] = state(todos)
  const [fitler, setFilter] = state(1)

  useEffect(() => { }, [
    api(filter).then(res => {
      setNewTodos(res.data)
    })
 ], [filter])

  function onFilterChange(value) {
    setFilter(value)
  }
  // ...
}
// 不使用 useEffect
function TodoList({ todos }) {
  const [newTodos, setNewTodos] = state(todos)
  const [fitler, setFilter] = state(1)

  function onFilterChange(value) {
    setFilter(value)
    api(filter).then(res => {
      setNewTodos(res.data)
    })
  }
  // ...
}

此時就會發(fā)現(xiàn),使用 useEffect 的性能劣勢消失不見了。因為即使我們在事件中請求了接口,但是由于異步事件的存在,導(dǎo)致 setFilter 與 setNewTodos 無法合并優(yōu)化,他們只能在不同的時間里觸發(fā) re-render。

而第一種寫法由于解耦做得比較好,因此他可以很容易在自定義 hook 的語法規(guī)則之下,簡化組件的邏輯

function TodoList({ todos }) {
  const {newTodos, filter, setFilter} 
    = useTodoList(api)

  function onFilterChange(value) {
    setFilter(value)
  }
  // ...
}

這樣我們就可以把 useEffect 和 異步邏輯通過封裝的方式藏起來。這種情況之下的選擇上,我更傾向于選擇更好的語義和更好的解耦。他在性能上的犧牲非常非常小。

四、useEffectEvent

在官方文檔中

https://zh-hans.react.dev/learn/separating-events-from-effects。

介紹了一個實驗性 api,useEffectEvent,用于從 Effect 中提取非響應(yīng)式邏輯,他能夠繞開閉包的困擾,讀取到最新的 state 和 props

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });
  // ...

先介紹一下官方案例的交互:

首先我們要完成一個聊天室的切換功能。聊天室切換時,我們需要斷開之前的連接,并接上新的連接。

聊天室在切換后連接成功時,需要有一個提示,表示我進入到了新的聊天室,并已經(jīng)連接成功了。

與此同時,該案例設(shè)計了一個交互點,新增了一個配置,去修改提示組件的風(fēng)格,讓他可以切換到 dark 主題

當(dāng)我選中 Use dark theme 時,那個提示組件也會彈出來露露臉。

事實上,實踐中不應(yīng)該出現(xiàn)這種交互,這里之所以出現(xiàn)是因為把他當(dāng)成一個問題來解決的

在代碼的設(shè)計中,isDark 被設(shè)計成為了一個響應(yīng)數(shù)據(jù)。

const [isDark, setIsDark] = useState(false);

然后我們封裝了一個 CharRoom,使用時將 roomId 與 theme 作為 props 傳入

<ChatRoom
  roomId={roomId}
  theme={isDark ? 'dark' : 'light'}
/>

在封裝 ChatRoom 時,由于 showNotification 的執(zhí)行需要 theme 作為參數(shù),于是,theme 就不得不作為 useEffect 的依賴項傳入,否則 showNotification 無法獲取最新的 theme 值

這是因為閉包的存在

// theme = isDark ? 'dark' : 'light'
const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

但是如果把 theme 作為依賴項之后,問題就產(chǎn)生了,由 roomId 切換導(dǎo)致的聊天室的斷開和重連邏輯就變得混亂了,因為當(dāng)你修改主題時,這段邏輯也會執(zhí)行。這明顯是不合理的。

因此,react 團隊正在想辦法設(shè)計一個 api,將 useEffect 的響應(yīng)式邏輯與非響應(yīng)式邏輯區(qū)分開。

解決代碼如下:

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ? 聲明所有依賴項
  // ...

事實上,在現(xiàn)有的方案之下,我們也有適合的解決方案。

首先我們要考慮一個交互上的特性,主題的更改,對于提示組件的影響并非是實時的。也就是說,當(dāng)我在修改主題時,我們并不需要一個提示組件出來露露臉。

因此,我們此時有機會考慮設(shè)計一個非響應(yīng)式的數(shù)據(jù)來存儲主題的更改。另一個角度,是否選中的 UI 樣式的修改,是 input 組件內(nèi)部自己的交互邏輯,因此此時也不需要外部提供一個響應(yīng)式數(shù)據(jù)來控制 input 是否被選中。

const isDark = useRef(false);

完整的邏輯代碼如下,該代碼可在對應(yīng)的官方案例中運行

import { useState, useEffect, useRef } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme.current ? 'dark' : 'light');
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const isDark = useRef(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          onChange={e => (isDark.current = e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark}
      />
    </>
  );
}

這樣,在我們前面提到的數(shù)據(jù)驅(qū)動 UI 的哲學(xué)邏輯驅(qū)動之下,精確分析數(shù)據(jù)與 UI 的關(guān)系,我們也完美的解決了這個問題。

五、總結(jié)

react 官方文檔在建議與規(guī)范的角度上會盡可能讓大家避免使用 useEffect,我猜測大概是由于許多初學(xué)者在 useEffect 對于依賴項的使用會產(chǎn)生不少疑問而導(dǎo)致的。但并不代表在 useEffect 的思考上,就沒有更合理的使用方式,他也不是一個反模式。

包括我們制定團隊規(guī)范也是一樣,團隊規(guī)范保障的是整個項目的底線,并不一定能代表項目上限,也不一定能代表最佳實踐。

因此,我更倡導(dǎo)大家在學(xué)習(xí)規(guī)范時,去充分理解規(guī)范出現(xiàn)的背后邏輯,靈活的運用他,并積極探尋更好的解決方案。

責(zé)任編輯:姜華 來源: 這波能反殺
相關(guān)推薦

2024-03-07 12:40:28

Python*args開發(fā)

2009-11-24 15:44:26

Visual Stud

2010-01-27 16:10:32

C++靜態(tài)構(gòu)造函數(shù)

2010-03-19 09:12:05

JRuby

2010-03-17 14:33:44

云計算

2010-05-24 17:13:34

Linux SNMP

2010-12-22 11:19:09

Java字節(jié)代碼

2010-01-08 15:06:35

JSON功能

2012-12-26 10:46:07

2023-11-30 07:45:11

useEffectReact

2016-08-12 22:47:17

互聯(lián)網(wǎng)計算廣告

2018-03-14 08:10:44

深度學(xué)習(xí)

2009-11-23 10:31:25

PHP使用JSON

2015-08-06 10:28:24

git規(guī)范流程

2016-10-14 13:46:26

2023-02-11 12:43:11

ChatGPT使用規(guī)范

2009-12-15 18:30:56

Ruby使用DBI包裝

2017-06-09 15:58:23

人工智能AI深度學(xué)習(xí)

2018-04-19 08:58:17

容器塊存儲

2009-12-02 15:02:09

PHP simplex
點贊
收藏

51CTO技術(shù)棧公眾號