【React】857- 使用 Context 的 2 個注意事項

作者:張立理 

https://zhuanlan.zhihu.com/p/313983390

Context是個好東西,先不論代數效應之類純理論的概念,能在組件樹上無視深度地透傳狀態確實能給開發帶來很大的便利。

但如果Context的使用上不注意一些細節,使用不當,對應用的性能是有可能造成災難性影響的。近期在做一個產品的性能優化的時候,總結出來微不足道的兩點“常識”。

關於順序

先來看一張圖,我願稱之爲世界名畫:

這就一演示的界面,沒什麼東西,我的邏輯也很簡單:

import React, {memo, useReducer} from 'react';
import {ConfigProvider, Tooltip} from 'antd';
import ZH_CN from 'antd/lib/locale-provider/zh_CN';
import 'antd/dist/antd.min.css';

const Text = ({text}) => {
    return (
        <Tooltip title="Tooltip" placement="top">
            <div>{text}</div>
        </Tooltip>

    );
};

const MemoedText = memo(Text);

const ConfigProviderInside = () => {
    const [counter, inc] = useReducer((v) => v + 11);

    return (
        <ConfigProvider locale={ZH_CN}>
            <div>
                <MemoedText text="This is some text." />
                <div>App Counter: {counter}</div>
                <button type="button" onClick={inc}>
                    FORCE UPDATE
                </button>
            </div>
        </ConfigProvider>

    );
};

點一下按鈕,整個界面都更新了一遍,是不是也還算正常?那如果做一下“簡單”地優化呢:

是不是很酸爽?我幹了什麼呢:

const ConfigProviderOutside = () => {
    const [counter, inc] = useReducer((v) => v + 11);

    return (
        <div>
            <MemoedText text="This is some text." />
            <div>App Counter: {counter}</div>
            <button type="button" onClick={inc}>
                FORCE UPDATE
            </button>
        </div>

    );
};

render(
    <ConfigProvider>
        <ConfigProviderOutside />
    </ConfigProvider>

);

我把antd的ConfigProvider放到了外面,就這一行代碼。

原因也很簡單,antd的ConfigProvider並沒有做什麼優化,它每一次給Context的value都是一個全新的對象(雖然內容並沒有變化),這就會導致所有關聯的組件都觸發更新(雖然毫無意義)。這在你的系統中的下場就是你拼合地用memo、PureComponent之類的方法優化自己寫的組件,但那裏面的antd組件們卻歡快地渲染到停不下來。

所以第一條經驗是:好好梳理Context的上下關係,把那些理論上不會變的放到最外面,把頻繁會變的往裏放。

什麼是不會變的呢,比如Locale、Constant,以及一些系統級的做依賴注入的,這些往往整個生命週期都不會變。

然後是類似CurrentUser這樣系統啓動的時候請求一次數據的,會從null變成固定的值,隨後就不會變了,這一類也儘量往外放。

最後像是Router這樣的,會頻繁變化的,要放到最裏面,免得因爲它的更新把其它不怎麼變的Context也帶進去。

關於粒度

來看一個非常經典的Context:

const DEFAULT_VALUE = {
    propsnull,
    openGlobalModal() => undefined,
    closeGlobalModal() => undefined,
};

const GlobalModalContext = createContext(DEFAULT_VALUE);

const GlobalModalContextProvider = ({children}) => {
    const [props, setProps] = useState(null);
    const closeGlobalModal = useCallback(
        () => setProps(null),
        []
    );
    const contextValue = useMemo(
        () => {
            return {
                props,
                closeGlobalModal,
                openGlobalModal: setProps,
            };
        },
        [props, closeGlobalModal, setProps]
    );
    
    return (
        <GlobalModalContext.Provider value={contextValue}>
            {children}
        </GlobalModalContext.Provider>

    );
};

用一個Context來統一地管理全局單例的對話框,也是一種比較常見的玩法。如果你這麼用:

const EditUserLabel = ({id}) => {
    const {openGlobalModal} = useContext(GlobalMoadlContext);
    const edit = useCallback(
        () => openGlobalModal({title'編輯用戶'children<UserForm id={id} />}),
        [openGlobalModal, id]
    );

    return <span onClick={edit}>編輯</span>;
};

const columns = [
    // ...
    {
        title'操作',
        key'action',
        dataIndex'id'
        renderid => <EditUserLabel id={id} />,
    }
]

const UserList = ({dataSource}) => (
    <Table rowKey="id" dataSource={dataSource} columns={columns} />
);

在一個表格裏每一行放一個“編輯”標籤,然後在全局放一個對話框:

const GlobalModal = () => {
    const {props} = useContext(GlobalMoadlContext);
    
    return !!props && <Modal visible {...props} />;
};

你就會驚訝地發現,每當你編輯一個用戶(或在其它地方觸發對話框),表格中每一行的編輯標籤都會更新。

原因很容易分析,因爲當你打開對話框的時候,props是變化的,因而contextValue也變化了,所以雖然編輯標籤只用了openGlobalModal這個永遠不會變的東西,卻也硬生生被帶着渲染了起來。

如果想追求更少地渲染,就要關注第二條經驗:一個Context中的東西往往並不一起被使用,將它們按使用場景分開,特別是要將多變的和不變的分開。

像上面的代碼,就可以優化成這樣:

const GlobalModalPropsContext = createContext(null);

const DEFAULT_ACTION = {
    openGlobalModal() => undefined,
    closeGlobalModal() => undefined,
};

const GlobalModalActionContext = createContext(DEFAULT_ACTION);

const GlobalModalContextProvider = ({children}) => {
    const [props, setProps] = useState(null);
    const closeGlobalModal = useCallback(
        () => setProps(null),
        []
    );
    const actionValue = useMemo(
        () => {
            return {
                closeGlobalModal,
                openGlobalModal: setProps,
            };
        },
        [closeGlobalModal, setProps]
    );
    
    return (
        // 注意第一條經驗,變得少的在外面
        <GlobalModalActionContext.Provider value={actionValue}>
            <GlobalModalPropsContext.Provider value={props}>
                {children}
            </GlobalModalPropsContext.Provider>
        </GlobalModalActionContext.Provider>

    );
};

只要根據實際的需要,去訪問2個不同的Context,就可以做到最優化的屬性粒度和最少的渲染。

當然我也建議不要直接暴露Context本身,而是將它按照使用場景暴露成若干個hook,這樣你可以在一開始不做特別的優化,當性能出現瓶頸的時候再拆Context,只需要修改hook的實現就能做到對外的兼容。

總結

  1. 關注在應用中使用的Context的順序,讓不變的在外層,多變的在內層。
  2. Context中的內容可以按使用場景和變與不變來拆分成多個更細粒度匠,以減少渲染。

    
    
    
  
      
      
      

1. JavaScript 重溫系列(22篇全)
2. ECMAScript 重溫系列(10篇全)
3. JavaScript設計模式 重溫系列(9篇全)
4.  正則 / 框架 / 算法等 重溫系列(16篇全)
5.  Webpack4 入門(上) ||  Webpack4 入門(下)
6.  MobX 入門(上)  ||   MobX 入門(下)
7. 100 +篇原創系列彙總

回覆“加羣”與大佬們一起交流學習~

點擊“閱讀原文”查看 100+ 篇原創文章



本文分享自微信公衆號 - 前端自習課(FE-study)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章