成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

深度探討 useEffect 使用規范

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

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

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

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

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

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

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

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

我的答案是后者。

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

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

一、計算屬性

在 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 本身已經具備了計算屬性的特性。當 state 發生變化,函數會重新執行,內部的代碼也會重新執行,因此案例中的 fullName 就有一次重新計算結果的機會

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

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

二、緩存計算結果

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

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

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

但是為什么呢?

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

setTimeout(effect, 0)

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

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

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

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

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

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

  // ...
}

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

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

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

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

import { useMemo, useState } from 'react';

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

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

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

const visibleTodos = getFilteredTodos(todos, filter);

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

三、與事件相關的爭議

現在我們來思考一個類似的交互方案,依然是一個任務列表

給他們設定一個過濾條件,類別,例如有兩個類別是工作類與旅游類,當類別發生變化的時候,部分任務會隱藏

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

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

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

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

副作用:我的修改,導致了除我之外的 UI 發生變化

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

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

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

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

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

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

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

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

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

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

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

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

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

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

當過濾條件發生變化,新的列表并不是從本地數據中運算得來,而是接口從服務端獲取。

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

// 使用 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)
    })
  }
  // ...
}

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

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

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 中提取非響應式邏輯,他能夠繞開閉包的困擾,讀取到最新的 state 和 props

import { useEffect, useEffectEvent } from 'react';

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

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

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

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

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

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

事實上,實踐中不應該出現這種交互,這里之所以出現是因為把他當成一個問題來解決的

在代碼的設計中,isDark 被設計成為了一個響應數據。

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

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

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

在封裝 ChatRoom 時,由于 showNotification 的執行需要 theme 作為參數,于是,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 作為依賴項之后,問題就產生了,由 roomId 切換導致的聊天室的斷開和重連邏輯就變得混亂了,因為當你修改主題時,這段邏輯也會執行。這明顯是不合理的。

因此,react 團隊正在想辦法設計一個 api,將 useEffect 的響應式邏輯與非響應式邏輯區分開。

解決代碼如下:

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]); // ? 聲明所有依賴項
  // ...

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

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

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

const isDark = useRef(false);

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

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}
      />
    </>
  );
}

這樣,在我們前面提到的數據驅動 UI 的哲學邏輯驅動之下,精確分析數據與 UI 的關系,我們也完美的解決了這個問題。

五、總結

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

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

因此,我更倡導大家在學習規范時,去充分理解規范出現的背后邏輯,靈活的運用他,并積極探尋更好的解決方案。

責任編輯:姜華 來源: 這波能反殺
相關推薦

2024-03-07 12:40:28

Python*args開發

2009-11-24 15:44:26

Visual Stud

2010-01-27 16:10:32

C++靜態構造函數

2025-05-12 01:33:00

異步函數Promise

2010-03-19 09:12:05

JRuby

2010-03-17 14:33:44

云計算

2010-12-22 11:19:09

Java字節代碼

2010-05-24 17:13:34

Linux SNMP

2023-11-30 07:45:11

useEffectReact

2012-12-26 10:46:07

2010-01-08 15:06:35

JSON功能

2016-08-12 22:47:17

互聯網計算廣告

2009-11-23 10:31:25

PHP使用JSON

2015-08-06 10:28:24

git規范流程

2018-03-14 08:10:44

深度學習

2016-10-14 13:46:26

2023-02-11 12:43:11

ChatGPT使用規范

2009-12-15 18:30:56

Ruby使用DBI包裝

2009-12-02 15:02:09

PHP simplex

2018-04-19 08:58:17

容器塊存儲
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲一区二区在线免费观看 | 国产综合精品 | 在线国产一区二区 | 超碰免费观看 | 99精品国产一区二区三区 | 亚洲激精日韩激精欧美精品 | 欧美综合久久久 | 美女在线视频一区二区三区 | 亚洲一区二区三区免费视频 | 国产一区免费视频 | 亚洲国产精品一区 | 91啪亚洲精品 | 国产精品一区二区免费 | 国产精品久久久久久婷婷天堂 | 精品一区二区三区在线观看 | 91高清在线观看 | 国产高清在线精品 | 日本偷偷操 | 亚洲精品免费看 | 日韩av大片免费看 | 在线a视频| 亚洲激精日韩激精欧美精品 | 欧美日本久久 | 欧美一区二区三区在线 | 精品视频一区二区三区在线观看 | 在线欧美亚洲 | 欧美性久久久 | 久久久久久久久久久久久久国产 | 日韩高清一区 | 中文字幕亚洲精品 | 国产精品免费一区二区三区四区 | 精品国产久 | 免费看欧美一级片 | 亚洲天堂久久 | 福利精品在线观看 | 中文字幕成人 | 欧美日韩在线观看一区 | 中国一级特黄真人毛片 | 一级黄色绿像片 | 国产激情精品一区二区三区 | 国产精品国产a级 |