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

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

開發(fā) 前端
本篇我們會用案例來探討 Context 存在什么樣的性能問題,并思考如何設(shè)計一個方案來替代 Context,解決它的性能問題

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

接下來我們會用案例來探討 context 存在什么樣的性能問題,并思考如何設(shè)計一個方案來替代 context,解決它的性能問題。

一、context 存在啥問題

我們需要通過一個實踐案例來分析 context 存在的性能問題。我計劃把幾個不同的 counter 狀態(tài)分散放到不同的子組件中去。項目結(jié)構(gòu)如圖。

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

在入口文件中,使用 Provider 把所有的子組件包裹起來。

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

在 Provider 中,我們創(chuàng)建好 context,并在 state 中定義好數(shù)據(jù),并通過 value 向子組件傳遞。

import {createContext, Dispatch, SetStateAction, useState} from 'react'


interface Props {
  children: any
}

const initialState = {
  counter01: 0,
  counter02: 0,
  counter03: 0
}

type State = typeof initialState

interface 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>
  )
}

每個子組件里,都會顯示一個 counter,并帶有一個按鈕點擊能遞增 counter,為了方便查看該子組件是否被 re-render,我們會在內(nèi)部邏輯中執(zhí)行 console.log 來觀察。

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

除此之外,為了驗證 memo 的效果,我們還使用 memo 將一個子組件包裹起來。

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 組件中只會重置對應(yīng)的數(shù)據(jù)為初始狀態(tài)。

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,全部代碼大概如此。運行,測試之后,我們發(fā)現(xiàn)此時存在嚴(yán)重的 re-render 現(xiàn)象:當(dāng)我們修改任何一個狀態(tài)時,所有的子組件都會 re-render,即使這個組件跟這個狀態(tài)毫無關(guān)系。就算你使用 memo 將子組件包裹起來,該子組件依然會 re-render。因此,當(dāng)你基于 context 開發(fā)頂層狀態(tài)管理器時,你的 React 項目的性能,將會很差。

梳理一下,具體的糟糕表現(xiàn)為:

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

為什么會出現(xiàn)這個問題呢?

我們前面已經(jīng)分析過,React 組件的 re-render 機制,需要同時保證 state、props、context 都不變,組件才不會 re-render。

我們觀察一下 Provider 的寫法

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ā)生變化時,value 總會被重新聲明,context.Provider 的 props.value 總是會發(fā)生變化,那么他的子組件的穩(wěn)定結(jié)構(gòu)從頂層就被破壞了,因此當(dāng) state 發(fā)生變化時,被他包裹的所有子組件都會 re-render。

二、context 的替代方案

在思考 context 的替代方案之前,我們先總結(jié)一下 context 的能力。

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

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

創(chuàng)建一個 store.ts 文件來完成我們的構(gòu)想。

首先創(chuàng)建一個對象用來存儲所有的數(shù)據(jù),并約定好數(shù)據(jù)的格式。

interface StoreItem {
  value: any,
  dispatch: Set<any>
}

interface Store {
  [key: string]: StoreItem
}

const store: Store = {}

理解這個數(shù)據(jù)格式,是整個功能實現(xiàn)的關(guān)鍵。不同的數(shù)據(jù)會對應(yīng)不同的 key 值,相同的數(shù)據(jù)會對應(yīng)不同的 setState,我們在 store 中用對應(yīng)的格式把這個關(guān)系存儲起來。

另外我再單獨定義一個對象,去存儲每一個狀態(tài)的初始化狀態(tài)。

interface KeyMap {
  [key: string]: boolean
}

const isInitStore: KeyMap = {}

修改數(shù)據(jù),本質(zhì)上是執(zhí)行 setState,因此,我們需要先定義好一個 set 方法用于觸發(fā)存儲在 dispatch 中的所有 setState 執(zhí)行,該方法只能在 store 模塊內(nèi)部被調(diào)用。

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

我們還需要定義一個 useSubscribe 用于在子組件內(nèi)部訂閱狀態(tài)。該方法用于收集每個組件的 setState,并返回當(dāng)前組件對應(yīng)的狀態(tài),和修改該狀態(tài)的方法。

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)]
}

有的時候我們還需要單獨調(diào)用某個方法去修改全局的狀態(tài),因此,我們還需要對外拋出一個 useDispatch 來完成這個需求。

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

OK,簡單的代碼,我們的這個功能就設(shè)計好了。我們在子組件中使用他們一下試試看。在子組件中使用時,只需要使用 useSubscribe 訂閱一下即可。該方法返回了狀態(tài)值,和修改狀態(tài)值的 set 方法。

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)鍵,如果你在不同的組件中共享同一個數(shù)據(jù),那么他們傳入的 key 值需要保持一致才能做到共享。例如我們分別定義下面兩個組件,他們能共享同一個狀態(tài)。

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

如果我們要單獨在別的組件中修改全局狀態(tài),則可以利用 useDispatch。

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

程序運行起來之后,測試一下。

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

在前面的篇幅中,我有強調(diào)過 React 對 JavaScript 的弱侵入性是他的一大優(yōu)勢。在這個方案里,已經(jīng)展現(xiàn)出來這一優(yōu)勢的巨大作用。我們有機會利用各種 JavaScript 的解決方案運用到我們的項目中,擴展 React 的項目邊界。

三、總結(jié)

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

除了我們自己利用發(fā)布訂閱模式來解決該問題之外,React 官方文檔也提供了一個 hook 來達到類似的效果:useSyncExternalStore,因為直接學(xué)習(xí)它有不少理解成本,因此我們鋪墊了本文的方案,后續(xù)會專門寫一篇文章來學(xué)習(xí)它,包括我們熟知的狀態(tài)管理方案 zustand 也是基于這個 hook 來實現(xiàn)。

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

2024-01-16 08:43:51

React底層機制Hook

2015-08-27 16:21:18

2018-07-13 15:36:52

2021-01-03 15:07:16

開發(fā)編程語言后端.

2020-02-02 19:53:57

數(shù)據(jù)庫數(shù)據(jù)庫優(yōu)化SQL優(yōu)化

2009-03-18 12:20:25

2010-09-10 11:15:15

Opera 10.62

2013-03-18 16:09:27

JavaEEOpenfire

2009-07-06 19:29:37

云計算私有云服務(wù)器虛擬化

2022-08-29 15:19:09

CSS煙花動畫

2021-08-24 05:07:25

React

2024-02-26 10:08:01

2023-08-01 08:47:54

索引數(shù)據(jù)庫MongoDB

2022-09-30 15:37:19

Web網(wǎng)站服務(wù)器

2012-03-22 10:33:33

思杰XenDesktop

2014-08-05 09:15:55

程序員

2014-08-08 10:24:37

程序員

2024-01-19 09:21:31

ReactHooksuseRef

2019-11-04 10:06:19

MySQL索引

2010-07-12 17:10:23

Android應(yīng)用程序
點贊
收藏

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

主站蜘蛛池模板: 国产亚洲精品精品国产亚洲综合 | 亚洲男人天堂2024 | jlzzxxxx18hd护士 | 亚洲国产精品成人无久久精品 | 日本精品一区二区三区视频 | 天堂av免费观看 | 国产激情视频在线免费观看 | 四季久久免费一区二区三区四区 | 成人欧美一区二区三区色青冈 | 欧美日韩视频在线 | 欧美成人精品激情在线观看 | 高清av电影 | 在线观看的av | 亚洲日本欧美日韩高观看 | 天堂一区二区三区四区 | 欧美日韩精品一区 | 成人婷婷 | 精品国产欧美一区二区三区成人 | 精品久久久久久一区二区 | 免费看a | 久久久久久久国产 | 亚洲精品大全 | 欧美又大粗又爽又黄大片视频 | 日日日视频 | 国产一级片一区二区 | 精品粉嫩aⅴ一区二区三区四区 | 国产精品日韩在线观看 | av永久免费 | 91色视频在线观看 | 九一视频在线观看 | 亚洲欧美国产一区二区三区 | 欧美精品video | 一级视频在线免费观看 | 美女一级黄 | 九九热免费视频在线观看 | 亚洲免费视频一区 | 国产欧美在线观看 | 久一精品 | 一级高清免费毛片 | 国产精品揄拍一区二区 | 久久精品色欧美aⅴ一区二区 |