1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        談一談為什么我對 React 越來越失望

        共 14068字,需瀏覽 29分鐘

         ·

        2022-11-25 06:32

        大廠技術  高級前端  精選文章

        點擊上方 全站前端精選,關注公眾號

        回復1,加入高級前段交流群


        作者 | Fran?ois Zaninotto
        譯者 | 張衛(wèi)濱
        策劃 | 閆園園

        親愛的 React.js:

        我們在一起已經(jīng)快十年了,我們攜手走過了漫長的旅程。但是,事情正在變得越來越糟糕,我們真的需要談談了。

        這確實有點令人尷尬,我知道,沒人愿意進行這樣的談話,所以我就以歌曲的形式來進行表達吧。(作者的每一個標題都是一首英文歌的名稱,在此我們不做翻譯——譯者注)

        You Were The One

        我并不是 JS 方面的新手。在遇到你之前,我已經(jīng)和 jQuery、Backbone.js 以及 Angular.js 打過很久的交道。我知道可以從 JavaScript 框架中得到什么:更好的用戶界面,更高的生產(chǎn)力,以及更流暢的開發(fā)體驗。但是,這也意味著我不得不改變我對代碼的思考方式,以匹配框架的思維模式,這會帶來一定的挫敗感。

        當我遇見你的時候,我剛剛結束了與 Angular.js 的一段長期感情。我已經(jīng)被它的 watch 和 digest 搞得焦頭爛額,更不用提 scope 了。我正在尋找不會讓我感到如此痛苦的東西。

        我對你一見鐘情。相對于其他的方案,你的單向數(shù)據(jù)綁定讓我感到驚艷。我之前遇到的數(shù)據(jù)同步和性能等一系列問題在你身上根本就不存在。你純粹基于 JavaScript,而不是在 HTML 元素中以字符串的形式進行笨拙的表述。你擁有“聲明式組件”,它實在太迷人了,吸引了所有人的目光。

        當然,你并不易于相處。為了與你保持和諧,我不得不改變自己的編碼習慣,但這都是值得的。最初,我對你非常滿意,以至于我一直向所有的人介紹你。

        Heroes Of New Forms

        當我開始要求你處理表單的時候,事情就開始變得不對勁了。在 vanilla JS 中,處理表單和輸入域是很困難的,但是在 React 中,則是難上加難。

        首先,開發(fā)人員必須在受控和非受控輸入之間做出選擇。兩者各有其缺點,在一些極端情況下都有缺陷。但是,歸根到底我們?yōu)槭裁匆獜闹羞M行選擇呢?兩種形式都要難道不好嗎?!

        “推薦”方式是使用受控組件,但它超級繁瑣。如下顯示了實現(xiàn)一個加法功能的表單需要的代碼。

        import React, { useState } from 'react';export default () => {    const [a, setA] = useState(1);    const [b, setB] = useState(2);    function handleChangeA(event) {        setA(+event.target.value);    }    function handleChangeB(event) {        setB(+event.target.value);    }    return (        <div>            <input type="number" value={a} onChange={handleChangeA} />            <input type="number" value= onChange={handleChangeB} />            <p>                {a} +  = {a + b}            </p>        </div>    );};

        如果只有兩種方式的話,我還會很開心。但是,構建一個真正的表單需要默認值、檢驗、輸入依賴和錯誤信息等功能,這需要大量的代碼,所以我不得不使用第三方框架。這些框架各有各的毛病。

        當使用 Redux 的時候,Redux-form 看上去是一個很自然的選擇,但后來它的主要開發(fā)人員放棄了它,然后建立了 React-final-form,這個框架全是未解決的缺陷,而且其主要的開發(fā)人員又放棄了它。所以,我又看了一下 Formik,它很流行,但它是一個重量級的框架,大型表單運行緩慢并且特性有限。所以,我決定使用 React-hook-form,它很快,但是有隱藏的缺陷,而且其文檔就像迷宮一樣。

        在使用 React 構建表單多年之后,我依然努力使用易讀的代碼為用戶提供強大的用戶體驗。當我看到 Svelte 是如何處理表單的時候,我瞬間覺得我一直被錯誤的抽象所羈絆。請看下面這個執(zhí)行加法功能的表單。

        <script>    let a = 1;    let b = 2;</script><input type="number" bind:value={a}><input type="number" bind:value=><p>{a} +  = {a + b}</p>
        You're Too Context Sensitive

        我們見面不久之后,你就向我介紹了你的小寵物 Redux。沒有它,你什么都做不了。起初我并不介意,因為它確實很可愛。但是,后來我意識到所有的一切都在圍繞它來構建。而且,在構建框架的時候,它讓我的生活變得更加困難,其他的開發(fā)人員很難使用現(xiàn)有 reducer 來調(diào)整應用。

        似乎你也注意到了這一點,于是決定擺脫 Redux,轉(zhuǎn)而使用自己的 useContext。只不過,useContext 缺少了 Redux 的一個關鍵特性,那就是響應上下文中局部變更的能力。在性能上,二者是不能同日而語的。

        // Reduxconst name = useSelector(state => state.user.name);// React contextconst { name } = useContext(UserContext);

        在第一個樣例中,該組件只會在用戶名發(fā)生變化的時候進行重新渲染。但是在第二個樣例中,當用戶的任何部分發(fā)生變更都會導致重新渲染。這一點很重要,以至于我們不得不拆分上下文以避免不必要的重新渲染。

        // 這種寫法看上去非常瘋狂,但是我們別無選擇export const CoreAdminContext = props => {    const {        authProvider,        basename,        dataProvider,        i18nProvider,        store,        children,        history,        queryClient,    } = props;    return (        <AuthContext.Provider value={authProvider}>            <DataProviderContext.Provider value={dataProvider}>                <StoreContextProvider value={store}>                    <QueryClientProvider client={queryClient}>                        <AdminRouter history={history} basename={basename}>                            <I18nContextProvider value={i18nProvider}>                                <NotificationContextProvider>                                    <ResourceDefinitionContextProvider>                                        {children}                                    </ResourceDefinitionContextProvider>                                </NotificationContextProvider>                            </I18nContextProvider>                        </AdminRouter>                    </QueryClientProvider>                </StoreContextProvider>            </DataProviderContext.Provider>        </AuthContext.Provider>    );};

        當我遇到性能問題的時候,大多數(shù)情況都是因為龐大的上下文,我別無選擇,只能對其進行拆分。

        我不想使用 useMemo 或 useCallback。因為重新渲染的問題是你造成的,而不是我。但是,你卻強迫我這樣做。請看一下,理想情況下我是如何構建一個簡單而快速的表單的吧:

        // from https://react-hook-form.com/advanced-usage/#FormProviderPerformanceconst NestedInput = memo(    ({ register, formState: { isDirty } }) => (        <div>            <input {...register('test')} />            {isDirty && <p>This field is dirty</p>}        </div>    ),    (prevProps, nextProps) =>        prevProps.formState.isDirty === nextProps.formState.isDirty,);export const NestedInputContainer = ({ children }) => {    const methods = useFormContext();    return <NestedInput {...methods} />;};

        都已經(jīng)十年了,這個缺陷依然還存在。我想問一下,提供一個 useContextSelector 能有多難呢?

        你當然意識到了這一點。但你在顧左右而言他,即便大家都知道這是你最重要的性能瓶頸。

        I Want None Of This

        你跟我說,我不應該直接訪問 DOM,這都是為我好。我從來不認為 DOM 是多臟的東西,但是它卻讓你坐立不安,所以我就聽你的了。現(xiàn)在,按照你的要求,我不得不使用 ref。

        但是 ref 這東西很快就像病毒一樣四處傳播。大多數(shù)時候,當某個組件使用 ref 的時候,它會將其傳遞到子組件中,如果第二個組件是 React 組件,它必須要將 ref 轉(zhuǎn)發(fā)至另一個組件,以此類推,直到樹中的某個組件渲染 HTML 元素為止。所以,代碼中到處都是轉(zhuǎn)發(fā) ref 的代碼,降低了代碼的易讀性。

        轉(zhuǎn)發(fā) ref 本可以非常簡單:

        const MyComponent = props => <div ref={props.ref}>Hello, {props.name}!</div>;

        但是,這不行,這太簡單了,于是你發(fā)明了 react.forwardRef 這個可惡的玩意兒。

        const MyComponent = React.forwardRef((props, ref) => (    <div ref={ref}>Hello, {props.name}!</div>));

        你可能會問,為什么這么難呢?這是因為我們無法使用 forwardRef 構建一個通用組件(在 Typescript 語言下)。

        // 我該如何使用forwardRef呢?const MyComponent = <T>(props: <ComponentProps<T>) => (    <div ref={/* pass ref here */}>Hello, {props.name}!</div>);

        此外,你認為 ref 不僅僅適用于 DOM 節(jié)點,還等價于函數(shù)組件的 this。換句話說,“不觸發(fā)重新渲染的狀態(tài)”。按照我的經(jīng)驗,每次我不得不使用 ref 的時候,都是因為你,因為你那詭異的 useEffect API。也就是說,ref 是你創(chuàng)造出來的問題的解決方案。

        The Butterfly (use) Effect

        說到 useEffect,我本人對它有一個疑問。我承認它是優(yōu)雅的創(chuàng)新,它在一個統(tǒng)一的 API 中,涵蓋了掛載、卸載和更新事件。但是,這怎么能算是進步呢?

        // 使用生命周期回調(diào)class MyComponent {    componentWillUnmount: () => {        // 執(zhí)行某些操作    };}// 使用useEffectconst MyComponent = () => {    useEffect(() => {        return () => {            // 執(zhí)行某些操作        };    }, []);};

        看,就這一行代碼就反應了我對 useEffect 的憂慮。

          }, []);

        我看到我的代碼中到處都是這種難以理解的格式,而這些都是因為 useEffect。另外,你還強迫我跟蹤依賴,比如這段代碼:

        // 如果沒有數(shù)據(jù)的話,對頁面進行變更useEffect(() => {    if (        query.page <= 0 ||        (!isFetching && query.page > 1 && data?.length === 0)    ) {        // 查詢不存在的頁數(shù)時,將頁數(shù)設置為1        queryModifiers.setPage(1);        return;    }    if (total == null) {        return;    }    const totalPages = Math.ceil(total / query.perPage) || 1;    if (!isFetching && query.page > totalPages) {        // 查詢范圍之外的頁數(shù)時,將頁數(shù)設置為最后一頁        // 這種情況會在刪除最后一頁的最后一條數(shù)據(jù)時出現(xiàn)        queryModifiers.setPage(totalPages);    }}, [isFetching, query.page, query.perPage, data, queryModifiers, total]);

        看到最后一行了嗎?我必須在依賴數(shù)組中包含所有的反應式變量(reactive variable)。我以前還認為對于支持垃圾收集的所有語言來說,引用計數(shù)是一項原生提供的功能,但是并非如此,我必須對依賴關系進行微觀管理,因為你不知道該怎樣進行處理。

        而且,在很多情況下,其中的某項依賴是我創(chuàng)建的函數(shù)。因為你沒有區(qū)分變量和函數(shù),我必須通過 useCallback 告訴你,防止進行重新渲染。同樣的結果,同樣詭異的方法簽名:

        const handleClick = useCallback(    async event => {        event.persist();        const type =            typeof rowClick === 'function'                ? await rowClick(id, resource, record)                : rowClick;        if (type === false || type == null) {            return;        }        if (['edit', 'show'].includes(type)) {            navigate(createPath({ resource, id, type }));            return;        }        if (type === 'expand') {            handleToggleExpand(event);            return;        }        if (type === 'toggleSelection') {            handleToggleSelection(event);            return;        }        navigate(type);    },    [        // 天啊,真不想這么做        rowClick,        id,        resource,        record,        navigate,        createPath,        handleToggleExpand,        handleToggleSelection,    ],);

        如果一個簡單組件有多個事件處理器和生命周期回調(diào)的話,代碼瞬間就會變得亂七八糟,因為我必須要管理這個像地獄似的依賴關系。所有的這一切都是因為你決定一個組件可以執(zhí)行任意多次。

        舉例來說,如果我想要實現(xiàn)一個計數(shù)器,每過一秒以及用戶每次點擊按鈕時,它都會增加,我必須這樣實現(xiàn):

        function Counter() {    const [count, setCount] = useState(0);    const handleClick = useCallback(() => {        setCount(count => count + 1);    }, [setCount]);    useEffect(() => {        const id = setInterval(() => {            setCount(count => count + 1);        }, 1000);        return () => clearInterval(id);    }, [setCount]);    useEffect(() => {        console.log('The count is now', count);    }, [count]);    return <button onClick={handleClick}>Click Me</button>;}

        如果我能知道如何跟蹤依賴的話,那么代碼就可以簡化成這個樣子:

        function Counter() {    const [count, setCount] = createSignal(0);    const handleClick = () => setCount(count() + 1);    const timer = setInterval(() => setCount(count() + 1), 1000);    onCleanup(() => clearInterval(timer));    createEffect(() => {        console.log('The count is now', count());    });    return <button onClick={handleClick}>Click Me</button>;}

        實際上,上面就是合法的 Solid.js 代碼。

        最后,想要高效地使用 useEffect 需要閱讀一篇 53 頁的文章。我必須說,那是一篇非常棒的文檔。但是,如果一個庫需要翻閱幾十頁文檔才能正確使用它,這難道不正是它設計得不好的一個標志嗎?

        Makeup Your Mind

        既然我們已經(jīng)談到了 useEffect 這個糟糕的抽象概念,你確實也在嘗試改善它,并提出了 useEvent、useInsertionEffect、useDeferredValue、useSyncWithExternalStore 以及其他吸引眼球的東西。

        它們確實使你變得更漂亮了:

        function subscribe(callback) {    window.addEventListener('online', callback);    window.addEventListener('offline', callback);    return () => {        window.removeEventListener('online', callback);        window.removeEventListener('offline', callback);    };}function useOnlineStatus() {    return useSyncExternalStore(        subscribe, // 只要傳遞相同的函數(shù),React不會解除訂閱        () => navigator.onLine, // 如何獲取客戶端的值        () => true, // 如何獲取服務器的值    );}

        但這對我來講,這就是狗尾續(xù)貂。如果反應式 effect 更易于使用的話,我們根本沒有必要增加其他的 hook。

        換句話說,隨著時間的推移,除了不斷增加核心 API 之外,你別無選擇。對于像我這樣要維護巨大代碼庫的人來說,這種持續(xù)的 API 膨脹是一個噩夢??吹侥忝刻焱恐ǚ郏@反過來就是在不斷提醒你,想想你在試圖掩飾些什么呢。

        Strict Machine

        你的 hook 是一個很好的主意,但它們是有成本的。這就是 Hook 規(guī)則。它們很難記,難以付諸實踐。但是,它們迫使我們必須在不必要的代碼上耗費時間。

        例如,我有一個“inspector”組件,終端用戶可以將它拖來拖去。用戶也可以隱藏它。當隱藏時,inspector 組件不會渲染任何東西。所以,定義組件時我希望“盡早離開”,避免無謂的注冊事件監(jiān)聽器。

        const Inspector = ({ isVisible }) => {    if (!isVisible) {        // 盡早離開        return null;    }    useEffect(() => {        // 注冊事件處理器        return () => {            // 解除事件處理器        };    }, []);    return <div>...</div>;}

        但是,這樣是不行的,因為這違反了 Hook 規(guī)則,useEffect hook 是否執(zhí)行取決于 props。所以,我必須在所有的 effect 上添加一個條件,使其能夠在 isVisible 屬性為 false 時盡早離開:

        const Inspector = ({ isVisible }) => {    useEffect(() => {        if (!isVisible) {            return;        }        // 注冊事件處理器        return () => {            // 解除事件處理器        };    }, [isVisible]);    if (!isVisible) {        // 不像前文那樣,進入之后立即離開        return null;    }    return <div>...</div>;};

        因此,所有的 effect 在它們的依賴關系中都要有 isVisible 屬性,并且可能會頻繁運行(這會損害性能)。我知道,我應該創(chuàng)建一個中間組件,如果 isVisible 為 false,就不渲染。但我憑什么要這樣做呢?這只是 Hook 規(guī)則妨礙我的一個例子,我還有很多其他的例子。這樣帶來的后果就是,我的 React 代碼庫中有很大一部分都是用來滿足 Hook 規(guī)則的。

        Hook 規(guī)則是實現(xiàn)細節(jié)導致的結果,也就是你為 hook 所選擇的實現(xiàn)。但是,它并非必須要這樣。

        You've Been Gone Too Long

        你從 2013 年就開始存在了,而且盡可能地保持了向后兼容性。為此我要感謝你,這也是我能夠與你構建一個龐大代碼庫的原因之一。但是,這種向后兼容性是有代價的,文檔和社區(qū)資源往好了說是過時的,往壞了說就是有誤導性的。

        例如,當我在 StackOverflow 上搜索“React mouse position”時,第一個結果建議使用如下的解決方案,而這個解決方案在一個世紀前就已經(jīng)過時了:

        class ContextMenu extends React.Component {    state = {        visible: false,    };    render() {        return (            <canvas                ref="canvas"                className="DrawReflect"                onMouseDown={this.startDrawing}            />        );    }    startDrawing(e) {        console.log(            e.clientX - e.target.offsetLeft,            e.clientY - e.target.offsetTop,        );    }    drawPen(cursorX, cursorY) {        // Just for showing drawing information in a label        this.context.updateDrawInfo({            cursorX: cursorX,            cursorY: cursorY,            drawingNow: true,        });        // Draw something        const canvas = this.refs.canvas;        const canvasContext = canvas.getContext('2d');        canvasContext.beginPath();        canvasContext.arc(            cursorX,            cursorY /* start position */,            1 /* radius */,            0 /* start angle */,            2 * Math.PI /* end angle */,        );        canvasContext.stroke();    }}

        當我為某個特定的 React 特性尋找 npm 包的時候,我經(jīng)常會找到語法陳舊、過時的廢棄包。以 react-draggable 為例。它是用 React 實現(xiàn)拖放的事實標準。它有許多未解決的問題,而且開發(fā)活躍性很低??赡苓@是因為它仍然是基于類組件的,當代碼庫如此老舊時,很難吸引貢獻者。


        至于你的官方文檔,仍然建議使用 componentDidMount 和 componentWillUnmount 而不是 useEffect。在過去的兩年里,核心團隊一直在開發(fā)一個新的版本,稱為 Beta docs。但是他們依然沒有做好最后的準備。

        總而言之,向 hook 的漫長遷移仍未完成,而且它在社區(qū)中產(chǎn)生了明顯的分裂現(xiàn)象。新的開發(fā)者努力在 React 生態(tài)系統(tǒng)中找到自己的方向,而老的開發(fā)者則努力跟上最新的發(fā)展。

        Family Affair

        起初,你的父親 Facebook 看起來特別酷。Facebook 想要“讓人們更緊密地聯(lián)系在一起”。每當我登錄 Facebook 時,都會遇到一些新朋友

        但后來事情就變得很混亂了。Facebook 加入了一個操縱人群的計劃。他們發(fā)明了“假新聞”的概念。他們未經(jīng)同意就開始保留每個人的檔案。訪問 Facebook 變得很可怕,以至于幾年前我刪除了自己的賬戶。

        我知道,不能讓孩子為父母的行為負責。但你仍然和它生活在一起。他們資助你的發(fā)展。他們是你最大的用戶。你依賴他們。如果有一天,他們因為自己的行為而倒下,你就會和他們一起倒下。

        其他主要的 JS 框架已經(jīng)能夠從它們的父母那里掙脫出來,變得變得獨立,并加入了 The OpenJS Foundation 基金會。Node.js、Electron、webpack、lodash、eslint,甚至 Jest 現(xiàn)在都是由一些公司和個人集體資助的。既然它們可以,你也可以。但你沒有。你被你的父親困住了,為什么呢?

        It's Not Me, It's You

        你和我有相同的生活目的,也就是幫助開發(fā)者建立更好的用戶界面。我正在用 React-admin 實現(xiàn)這一點。所以我理解你面臨的挑戰(zhàn),以及你必須做出的權衡。你并不容易,可能正在解決大量我甚至不知道的問題。

        但我發(fā)現(xiàn)自己正在不斷地隱藏你的缺陷。當我談到你的時候,我從不會提及上述問題,我假裝我們是一對偉大的夫婦,生活沒有陰云。在 react-admin 中,我引入了 API,消除了直接與你打交道的麻煩。當人們抱怨 react-admin 時,我盡力解決他們的問題,但大多數(shù)時候,它們都是你的問題。作為框架的開發(fā)者,我也位于第一線,比其他人能夠更早看到所有的問題。

        我看了其他的框架,他們有自己的缺陷,比如 Svelte 不是 JavaScript,SolidJS 有討厭的陷阱:

        // 這可以在SolidJS中運行const BlueText = props => <span style="color: blue">{props.text}</span>;// 這無法在SolidJS中運行const BlueText = ({ text }) => <span style="color: blue">{text}</span>;

        但它們沒有你身上的缺陷。那些讓我有時想哭的缺陷,那些經(jīng)過多年處理后變得非常煩人的缺陷,那些讓我想嘗試其他新框架的缺陷。相比之下,所有其他框架都令人耳目一新。

        I Can't Quit You Baby、

        問題在于,我無法離開你。

        首先,我喜歡你的朋友們。MUI、Remix、react-query、react-testing-library、react-table... 當我和它們在一起的時候,總是能做出美妙的成果。它們使我成為更好的開發(fā)者,它們使我成為更好的人。要離開你,我就必須離開它們。

        這就是生態(tài)系統(tǒng)。

        我不能否認,你有最好的社區(qū)和最好的第三方模塊。但坦率地說,令人遺憾的是,開發(fā)者選擇你不是因為你的品質(zhì),而是因為你的生態(tài)系統(tǒng)的品質(zhì)。

        第二,我在你身上投資了太多。我已經(jīng)用你建立了一個巨大的代碼庫,遷移到其他框架會讓我感到崩潰。我已經(jīng)圍繞你建立了自己的商業(yè)模式,讓我能夠以可持續(xù)的方式開發(fā)開源軟件。

        我依賴你。

        Call Me Maybe

        我對我的感受一直很坦誠。現(xiàn)在,我希望你也能這樣做。你是否有計劃解決我上面列出的問題,如果是的話,在什么時候做呢?你對像我這樣的庫開發(fā)人員有什么看法?我是否應該忘記你,轉(zhuǎn)而去嘗試其他框架,還是應該呆在一起,為保持我們的關系而繼續(xù)努力?

        我們下一步要走向何方?你能告訴我嗎?

        后續(xù):React 的開發(fā)人員在推特上對作者的這些問題進行了答復,React 承認這些問題,并致力于解決和完善,但這似乎不是一朝一夕能夠完成的。

        前端 社群



        下方加 Nealyang 好友回復「 加群」即可。



        如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個小忙:

        1. 點個「在看」,讓更多人也能看到這篇文章

        點贊和在看就是最大的支持

        瀏覽 44
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产一线二线三线网站 | 中文字幕精品一区二区三区在线 | 久久久久久久久久久久久久久女乱 | 丁香婷婷亚洲 | 欧美一级性爱在线观看 | 尤物视频免费在线观看 | 成人超碰在线观看 | 青草91 | 成人片在线播放 | 色大香蕉|