日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當(dāng)前位置:首頁 > 科技  > 軟件

React 性能優(yōu)化終章,成為頂尖高手的最后一步

來源: 責(zé)編: 時(shí)間:2024-02-01 12:46:02 244觀看
導(dǎo)讀在前面的章節(jié)中,我們學(xué)習(xí)了 context 的使用方式,基于它我們可以搞一個(gè)自己的狀態(tài)管理庫。不過,他存在性能上的問題,以致于雖然從功能的實(shí)現(xiàn)上來說,他非常不錯(cuò),但是從性能上來說,context 的表現(xiàn)非常糟糕,雖然很少有 React 學(xué)習(xí)

lTn28資訊網(wǎng)——每日最新資訊28at.com

在前面的章節(jié)中,我們學(xué)習(xí)了 context 的使用方式,基于它我們可以搞一個(gè)自己的狀態(tài)管理庫。不過,他存在性能上的問題,以致于雖然從功能的實(shí)現(xiàn)上來說,他非常不錯(cuò),但是從性能上來說,context 的表現(xiàn)非常糟糕,雖然很少有 React 學(xué)習(xí)者關(guān)注到這個(gè)問題,但是如果你關(guān)注項(xiàng)目的整體架構(gòu),并且想要成為頂尖高手的話,這是你必須掌握的最后一步。lTn28資訊網(wǎng)——每日最新資訊28at.com

接下來我們會(huì)用案例來探討 context 存在什么樣的性能問題,并思考如何設(shè)計(jì)一個(gè)方案來替代 context,解決它的性能問題。lTn28資訊網(wǎng)——每日最新資訊28at.com

一、context 存在啥問題

我們需要通過一個(gè)實(shí)踐案例來分析 context 存在的性能問題。我計(jì)劃把幾個(gè)不同的 counter 狀態(tài)分散放到不同的子組件中去。項(xiàng)目結(jié)構(gòu)如圖。lTn28資訊網(wǎng)——每日最新資訊28at.com

+ App  - index.tsx  - Provider.tsx  - Counter01.tsx  - Counter02.tsx  - Counter03.tsx  - Reset.tsx

在入口文件中,使用 Provider 把所有的子組件包裹起來。lTn28資訊網(wǎng)——每日最新資訊28at.com

import Provider from './Provider';import Counter01 from './Counter01';import Counter02 from './Counter02';import Counter03 from './Counter03';import Reset from './Reset';/** * @description 性能有問題,子組件每次都會(huì)rerender * @returns  */export default function App() {  return (    <Provider>      <Counter01 />      <Counter02 />      <Counter03 />      <Reset />    </Provider>      )}

在 Provider 中,我們創(chuàng)建好 context,并在 state 中定義好數(shù)據(jù),并通過 value 向子組件傳遞。lTn28資訊網(wǎng)——每日最新資訊28at.com

import {createContext, Dispatch, SetStateAction, useState} from 'react'interface Props {  children: any}const initialState = {  counter01: 0,  counter02: 0,  counter03: 0}type State = typeof initialStateinterface Value extends State {  setCounter01: Dispatch<any>,  setCounter02: Dispatch<any>,  setCounter03: Dispatch<any>}export const context = createContext<Value>(initialState as Value)export default function Provider(props: Props) {  const [state, setState] = useState(initialState)  const value = {    ...state,    setCounter01: (value: number) => setState({...state, counter01: value}),    setCounter02: (value: number) => setState({...state, counter02: value}),    setCounter03: (value: number) => setState({...state, counter03: value})  }  return (    <context.Provider value={value}>      {props.children}    </context.Provider>  )}

每個(gè)子組件里,都會(huì)顯示一個(gè) counter,并帶有一個(gè)按鈕點(diǎn)擊能遞增 counter,為了方便查看該子組件是否被 re-render,我們會(huì)在內(nèi)部邏輯中執(zhí)行 console.log 來觀察。lTn28資訊網(wǎng)——每日最新資訊28at.com

import { useContext } from 'react';import {context} from './Provider'export default function Counter01() {  const {counter01, setCounter01} = useContext(context)  console.log('counter01: ', counter01)  function clickHandle() {    setCounter01(counter01 + 1)  }  return (    <button onClick={clickHandle}>      counter01: {counter01}    </button>  )}

除此之外,為了驗(yàn)證 memo 的效果,我們還使用 memo 將一個(gè)子組件包裹起來。lTn28資訊網(wǎng)——每日最新資訊28at.com

import { useContext, memo } from 'react';import {context} from './Provider'function Counter03() {  const {counter03, setCounter03} = useContext(context)  console.log('counter03: ', counter03)  function clickHandle() {    setCounter03(counter03 + 1)  }  return (    <button onClick={clickHandle}>      counter03: {counter03}    </button>  )}export default memo(Counter03)

Reset 組件中只會(huì)重置對應(yīng)的數(shù)據(jù)為初始狀態(tài)。lTn28資訊網(wǎng)——每日最新資訊28at.com

import { useContext } from 'react';import {context} from './Provider'export default function Reset() {  const {setCounter01, setCounter02} = useContext(context)  console.log('reset');  function clickHandle() {    setCounter01(0);    // setCounter02(1);  }  return (    <div>      <button onClick={clickHandle}>        Reset01 02 to 0      </button>    </div>  )}

OK,全部代碼大概如此。運(yùn)行,測試之后,我們發(fā)現(xiàn)此時(shí)存在嚴(yán)重的 re-render 現(xiàn)象:當(dāng)我們修改任何一個(gè)狀態(tài)時(shí),所有的子組件都會(huì) re-render,即使這個(gè)組件跟這個(gè)狀態(tài)毫無關(guān)系。就算你使用 memo 將子組件包裹起來,該子組件依然會(huì) re-render。因此,當(dāng)你基于 context 開發(fā)頂層狀態(tài)管理器時(shí),你的 React 項(xiàng)目的性能,將會(huì)很差。lTn28資訊網(wǎng)——每日最新資訊28at.com

梳理一下,具體的糟糕表現(xiàn)為:lTn28資訊網(wǎng)——每日最新資訊28at.com

  • 1、任何狀態(tài)的變化,所有子組件都會(huì) re-render
  • 2、子組件包裹 memo 無效
  • 3、連續(xù)點(diǎn)擊 reset 按鈕,即使?fàn)顟B(tài)沒有發(fā)生變化,所有子組件也會(huì) re-render

為什么會(huì)出現(xiàn)這個(gè)問題呢?lTn28資訊網(wǎng)——每日最新資訊28at.com

我們前面已經(jīng)分析過,React 組件的 re-render 機(jī)制,需要同時(shí)保證 state、props、context 都不變,組件才不會(huì) re-render。lTn28資訊網(wǎng)——每日最新資訊28at.com

我們觀察一下 Provider 的寫法lTn28資訊網(wǎng)——每日最新資訊28at.com

export default function Provider(props: Props) {  const [state, setState] = useState(initialState)  const value = {    ...state,    setCounter01: (value: number) => setState({...state, counter01: value}),    setCounter02: (value: number) => setState({...state, counter02: value}),    setCounter03: (value: number) => setState({...state, counter03: value})  }  return (    <context.Provider value={value}>      {props.children}    </context.Provider>  )}

在 context 發(fā)生變化時(shí),value 總會(huì)被重新聲明,context.Provider 的 props.value 總是會(huì)發(fā)生變化,那么他的子組件的穩(wěn)定結(jié)構(gòu)從頂層就被破壞了,因此當(dāng) state 發(fā)生變化時(shí),被他包裹的所有子組件都會(huì) re-render。lTn28資訊網(wǎng)——每日最新資訊28at.com

二、context 的替代方案

在思考 context 的替代方案之前,我們先總結(jié)一下 context 的能力。lTn28資訊網(wǎng)——每日最新資訊28at.com

  • 支持全局共享狀態(tài)
  • 支持跨組件傳遞

那么,我們?nèi)绾位?React 現(xiàn)有的機(jī)制,做到和 context 一樣的事情呢?要單獨(dú)想到比較困難,但是答案卻非常簡單。具體的思路是,我們可以利用發(fā)布訂閱模式,收集每個(gè)組件內(nèi)部的 setState,把共享狀態(tài)的 satate 收集到一起,然后利用他們各自的 setState 去觸發(fā)數(shù)據(jù)的更新即可。這樣,我們就可以實(shí)現(xiàn)上面的兩個(gè)要求了。lTn28資訊網(wǎng)——每日最新資訊28at.com

創(chuàng)建一個(gè) store.ts 文件來完成我們的構(gòu)想。lTn28資訊網(wǎng)——每日最新資訊28at.com

首先創(chuàng)建一個(gè)對象用來存儲(chǔ)所有的數(shù)據(jù),并約定好數(shù)據(jù)的格式。lTn28資訊網(wǎng)——每日最新資訊28at.com

interface StoreItem {  value: any,  dispatch: Set<any>}interface Store {  [key: string]: StoreItem}const store: Store = {}

理解這個(gè)數(shù)據(jù)格式,是整個(gè)功能實(shí)現(xiàn)的關(guān)鍵。不同的數(shù)據(jù)會(huì)對應(yīng)不同的 key 值,相同的數(shù)據(jù)會(huì)對應(yīng)不同的 setState,我們在 store 中用對應(yīng)的格式把這個(gè)關(guān)系存儲(chǔ)起來。lTn28資訊網(wǎng)——每日最新資訊28at.com

另外我再單獨(dú)定義一個(gè)對象,去存儲(chǔ)每一個(gè)狀態(tài)的初始化狀態(tài)。lTn28資訊網(wǎng)——每日最新資訊28at.com

interface KeyMap {  [key: string]: boolean}const isInitStore: KeyMap = {}

修改數(shù)據(jù),本質(zhì)上是執(zhí)行 setState,因此,我們需要先定義好一個(gè) set 方法用于觸發(fā)存儲(chǔ)在 dispatch 中的所有 setState 執(zhí)行,該方法只能在 store 模塊內(nèi)部被調(diào)用。lTn28資訊網(wǎng)——每日最新資訊28at.com

function _setValue(key: string, value: any) {  store[key].value = value  store[key].dispatch.forEach((cb: any) => {    cb(value)  })}

我們還需要定義一個(gè) useSubscribe 用于在子組件內(nèi)部訂閱狀態(tài)。該方法用于收集每個(gè)組件的 setState,并返回當(dāng)前組件對應(yīng)的狀態(tài),和修改該狀態(tài)的方法。lTn28資訊網(wǎng)——每日最新資訊28at.com

export function useSubscribe(key: string, value?: any) {  const [state, setState] = useState(value || null)  // 如果沒有被初始化,則初始化一次  if (!isInitStore[key]) {    store[key] = { value: value, dispatch: new Set() }    isInitStore[key] = true  }  if (store[key].dispatch.has(setState) === false) {    store[key].dispatch.add(setState)  }  return [state, (_value: any) => _setValue(key, _value)]}

有的時(shí)候我們還需要單獨(dú)調(diào)用某個(gè)方法去修改全局的狀態(tài),因此,我們還需要對外拋出一個(gè) useDispatch 來完成這個(gè)需求。lTn28資訊網(wǎng)——每日最新資訊28at.com

export function useDispatch(key: string) {  return (value: any) => _setValue(key, value)}

OK,簡單的代碼,我們的這個(gè)功能就設(shè)計(jì)好了。我們在子組件中使用他們一下試試看。在子組件中使用時(shí),只需要使用 useSubscribe 訂閱一下即可。該方法返回了狀態(tài)值,和修改狀態(tài)值的 set 方法。lTn28資訊網(wǎng)——每日最新資訊28at.com

import { useSubscribe } from './store';export default function Counter01() {  const [counter, setCounter] = useSubscribe('counter01')  console.log('counter01: ', counter)  function clickHandle() {    setCounter(counter + 1)  }  return (    <button onClick={clickHandle}>      counter01: {counter}    </button>  )}

這里傳入的字符串非常關(guān)鍵,如果你在不同的組件中共享同一個(gè)數(shù)據(jù),那么他們傳入的 key 值需要保持一致才能做到共享。例如我們分別定義下面兩個(gè)組件,他們能共享同一個(gè)狀態(tài)。lTn28資訊網(wǎng)——每日最新資訊28at.com

import { useSubscribe } from './store';function Counter03() {  const [counter, setCounter] = useSubscribe('counter04')  console.log('counter03: ', counter)  function clickHandle() {    setCounter(counter + 1)  }  return (    <button onClick={clickHandle}>      counter03: {counter}    </button>  )}export default Counter03
import {useSubscribe} from './store'export default function Counter04() {  const [counter, setCounter] = useSubscribe('counter04')  console.log('counter04: ', counter)  function clickHandle() {    setCounter(counter + 1)  }  return (    <button onClick={clickHandle}>      counter04: {counter}    </button>  )}

如果我們要單獨(dú)在別的組件中修改全局狀態(tài),則可以利用 useDispatch。lTn28資訊網(wǎng)——每日最新資訊28at.com

import { useDispatch } from './store';export default function Reset() {  const setCounter01 = useDispatch('counter01')  const setCounter02 = useDispatch('counter02')  const setCounter03 = useDispatch('counter04')  console.log('reset');  function clickHandle() {    setCounter01(0);    setCounter02(0);  }  function clickHandle03() {    setCounter03(0)  }  return (    <div>      <button onClick={clickHandle}>        Reset01 02 to 0      </button>      <button onClick={clickHandle03}>        Reset03      </button>    </div>  )}

程序運(yùn)行起來之后,測試一下。lTn28資訊網(wǎng)——每日最新資訊28at.com

發(fā)現(xiàn)我們不僅實(shí)現(xiàn)了全局狀態(tài)共享,也實(shí)現(xiàn)了數(shù)據(jù)跨組件傳遞。也解決了 context 引發(fā)不相干子組件刷新的問題。甚至組組件連 memo 的優(yōu)化手段都不需要用,依然能夠保持最低代價(jià)的 re-render。也就是說,這種方案完美解決了 context 的性能弊病,成為了一個(gè)高性能方案。因此,基于你的需求稍微擴(kuò)展一下,他就能夠成為一個(gè)強(qiáng)大的狀態(tài)管理庫運(yùn)用于你的真實(shí)項(xiàng)目中。lTn28資訊網(wǎng)——每日最新資訊28at.com

在前面的篇幅中,我有強(qiáng)調(diào)過 React 對 JavaScript 的弱侵入性是他的一大優(yōu)勢。在這個(gè)方案里,已經(jīng)展現(xiàn)出來這一優(yōu)勢的巨大作用。我們有機(jī)會(huì)利用各種 JavaScript 的解決方案運(yùn)用到我們的項(xiàng)目中,擴(kuò)展 React 的項(xiàng)目邊界。lTn28資訊網(wǎng)——每日最新資訊28at.com

三、總結(jié)

我們這個(gè)方案基于閉包,利用發(fā)布訂閱模式,在子組件中訂閱組件對應(yīng)的 setState,并在執(zhí)行時(shí)統(tǒng)一觸發(fā)所有相同狀態(tài)的 set 方法。如果對我標(biāo)黑的幾個(gè)基礎(chǔ)知識掌握得比較好的話,對這個(gè)方案理解起來會(huì)比較容易。否則可能會(huì)面臨比較大的理解成本。不過也沒有關(guān)系,加入 React 知命境付費(fèi)群,可以在群里跟群友進(jìn)一步探討該方案,我也會(huì)在群里直播講解該方案lTn28資訊網(wǎng)——每日最新資訊28at.com

除了我們自己利用發(fā)布訂閱模式來解決該問題之外,React 官方文檔也提供了一個(gè) hook 來達(dá)到類似的效果:useSyncExternalStore,因?yàn)橹苯訉W(xué)習(xí)它有不少理解成本,因此我們鋪墊了本文的方案,后續(xù)會(huì)專門寫一篇文章來學(xué)習(xí)它,包括我們熟知的狀態(tài)管理方案 zustand 也是基于這個(gè) hook 來實(shí)現(xiàn)。lTn28資訊網(wǎng)——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-70397-0.htmlReact 性能優(yōu)化終章,成為頂尖高手的最后一步

聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com

上一篇: 利用@Embeddable實(shí)現(xiàn)實(shí)體和級聯(lián)關(guān)系的分開定義

下一篇: 如何在 Npm 上發(fā)布二進(jìn)制文件?

標(biāo)簽:
  • 熱門焦點(diǎn)
Top 主站蜘蛛池模板: 福建省| 博客| 榆林市| 商洛市| 庆城县| 静海县| 芒康县| 荆州市| 开化县| 海原县| 宁都县| 襄汾县| 凤山市| 濉溪县| 城市| 松溪县| 保德县| 明光市| 来宾市| 永嘉县| 商洛市| 怀来县| 卓资县| 井冈山市| 北碚区| 峨眉山市| 土默特左旗| 贺兰县| 湟源县| 和政县| 江孜县| 天津市| 贵溪市| 米脂县| 武夷山市| 扬中市| 河东区| 芜湖县| 比如县| 临清市| 永宁县|