React Native面向未來架構

既然面向未來,那么項目的架構設計采用React/React native的新特性,并且為即將來到的新特性預留位置;對核心依賴庫保持最小代碼侵略性和最底依賴;且滿足以下這幾點。

  • 高性能
  • 高效率
  • 易拓展
  • 低耦合
  • 易測試
  • 少bug
  • 協(xié)作開發(fā)

我們先從項目最小的一個點開始講:組件

這里有一個性能關鍵點, 舉個例子

A組件:

const A = ({count}) => {
    React.useEffect(() => {
        console.log('render A')
    });

    return <Text >{count}</Text>
}

B組件

const B = () => {
    React.useEffect(() => {
        console.log('render B')
    });

    return <View />
}

把這兩個組件拼在一起,每次點擊A組件的時候都會讓count+1

const App = () = > {
    const [count, setCount] = React.useState(0);

    React.useEffect(() => {
        console.log('render App')
    });

    return (
        <>
            <Pressable onPress={()=>setCount(count+1)} >
                <A count={count} />
            <Pressable>
            <B />
        </>
    )
}

想象中的打印結果

render App
render A

真實的打印結果:

render App
render A
render B

我第一次發(fā)現(xiàn)的時候,竟然是這樣的打印結果,我懵了都,難道不會幫我控制性能嗎? React 就這?

打印結果表明:每一次父組件的state更新,都會導致所有子組件重新渲染。那這樣性能豈不是太差了!

其實這不并不是真正意義上的渲染,react底層會有diff算法,會幫你更新“真正需要更新的組件”。

所以我們要手動控制,趕在diff算法之前不讓它“渲染”;其實很簡單,React.memo(組件),就這樣套住組件就可以了,就可以得到我們想象中的打印結果了;React.memoReact.PureComponent類似,作用:檢查Props是不是變了,如果變了才會真正的去更新,但得記住這是一個淺比較。

這是我們實現(xiàn)最小粒度更新的關鍵

Ok,正文開始,設計組件。

我們先看一個頁面的組成是這個樣子的

所以大多數(shù)項目的目錄結構都會有一個components目錄,顧名思義這個目錄用來存放公用組件

都知道React是組件化思想。那你真的理解了什么是組件化嗎?開發(fā)的時候,覺得這個組件是可以抽出來的,就丟到components目錄下去了?

這是一個非常糟糕的做法。公用組件意味著無副作用,大膽用放心用。如果只是你覺得可以抽出來做一個組件,放到components目錄下,那這個組件如果包含業(yè)務代碼怎么辦?開發(fā)人員還要閱讀你的這個組件,確保沒有副作用,才能使用,降低了效率,嚴重的話還會產(chǎn)生bug。

React官方建議:多寫無狀態(tài)組件;組件最小粒度化;組件只是數(shù)據(jù)的管道工!

組件其實是分為兩種組件

  • 無狀態(tài)組件 (用函數(shù)式編程理解:相同輸入,相同輸出,毫無副作用;這種組件才該放入components目錄下

  • 有狀態(tài)組件(用函數(shù)式編程理解:相同輸入,不一定相同輸出,還可能修改外部的值)

在React 16.8版本之前,也就是還沒有推出hooks之前,可以這樣設計組件,就可以非常好的區(qū)分

  • 無狀態(tài)組件:使用function來寫組件

  • 有狀態(tài)組件:使用class來寫組件

我們的架構是圍繞React/React native新特性,當然得用hook+函數(shù),無class該如何設計組件?

一個頁面的視圖組成部分應該是這樣的

所以我們需要為組件分兩個目錄

  • components 無狀態(tài)組件存放目錄

  • container 有狀態(tài)組件存放目錄

components目錄下的無狀態(tài)組件

核心:組件最小粒度化;組件只是數(shù)據(jù)的管道工

  • component必須使用React.memo來控制

  • component內(nèi)部不允許改變外部的值(可以這么理解:大多數(shù)項目都會使用狀態(tài)管理,例如redux,mobx等,統(tǒng)稱為store,也就是說,不允許引入store和使用store的方法和變量,也就是不允許有業(yè)務代碼)

  • props必須或盡量是基本類型(例如:string,number,組件保持最小粒度化了開發(fā),所以滿足props是基本類型這一點非常簡單;其次,基本類型的props,才能發(fā)揮React.memo真正的作用)

  • 允許有自己的state,生命周期,也就是hook

component偽代碼像這樣

const ComponentDemo = React.memo((props)=>{

    // --------------------------------
    //              state 部分
    // --------------------------------

    // --------------------------------
    //              生命周期 部分
    // --------------------------------

    return (
       // 視圖
    )
})

container目錄下的有狀態(tài)組件

核心:一個container,渲染部分應該盡量使用component進行拼接組成

  • container同樣使用React.memo來控制

  • container內(nèi)部允許改變外部的值(允許引入store和使用store的方法和變量)

  • props盡量是基本類型

  • 允許有自己的state,生命周期,也就是hook

container偽代碼如下

const ContainerDemo = React.memo((props)=>{

    // --------------------------------
    //              store 部分
    // --------------------------------

    // --------------------------------
    //              state 部分
    // --------------------------------

    // --------------------------------
    //              生命周期 部分
    // --------------------------------

    // --------------------------------
    //              業(yè)務邏輯 部分
    // --------------------------------

    return (
        // --------------------------------
        //          component 1
        // --------------------------------
        // --------------------------------
        //          component 2
        //          ......
        // --------------------------------
        // --------------------------------
        //          拼接的代碼例如像View
        // --------------------------------
    )
})

區(qū)別顯而易見,結論:

  • componentcontainer的唯一區(qū)別就是有無store

  • 最大程度的復用,而且沒有副作用。復用也是間接減少bug的,因為你復用的代碼是上個版本的,上個版本是經(jīng)過測試的,我們可以默認它是無bug,穩(wěn)定的代碼。

  • 實現(xiàn)最小粒度更新,完美控制性能

說完組件設計,接下來是邏輯的復用

componentcontainer描述和偽代碼,我們可以看到有相同部分

  • state
  • 生命周期
  • 業(yè)務邏輯

依靠hook的特性,只要你覺得抽出去有價值那就抽!這樣不光是container還是component都可以復用邏輯,又提高效率了!所以又多出了一個新目錄:hooks目錄

所以視圖部分架構就誕生了!

ok,到現(xiàn)在為止就有3個目錄了

  • components 無狀態(tài)組件

  • container 有狀態(tài)組件

  • hooks 存放可復用hook

說完視圖部分,接下就是狀態(tài)管理了

不用多說,先多一個store目錄

mobxredux大家應該都很熟悉了,這里就不介紹了;這兩個都有為hook,提供了新的狀態(tài)管理庫(但是看下來都比較重,且代碼侵略性比較高,對于hook來說有一種沒有必要抽象的感覺),所以我們采用全新的狀態(tài)管理工具hox

hox優(yōu)點

  • 只有一個api,簡單,上手快,足夠輕

  • 非常易于復用

  • 狀態(tài)按模塊分,清晰更好控制

  • 完美支持typescript

  • 對代碼侵略性非常底,它不會成為我們支持未來的新特性,三方庫的升級,甚至重構的負擔

簡單介紹下用法,大家肯定都有用過mobx/redux,為了更好理解,所以持久化的狀態(tài),就稱之為store。

import { useState } from "react";
import { createModel } from "hox";

// -------------------創(chuàng)建一個持久化store------------------------
const useCounter =() => {
    // count這個state 持久化
    const [count, setCount] = useState(0);
    const decrement = () => setCount(count - 1);
    const increment = () => setCount(count + 1);

    // ----------------------------------------------
    //           根據(jù)需要,這里可以使用自定義hook
    // ----------------------------------------------

    return {
        count,
        decrement,
        increment
    };
}

export default createModel(useCounter);
// 或者干脆整個store,直接用自定義hook
// export default createModel(hook);

// -------------------使用這個store------------------------------
import useCounterModel from "../store/counter";

const App=React.memo((props)=> {

    const {count,decrement,increment} = useCounterModel();

    return (
        <View>
            <Text>{count}</Text>
            <Pressable onPress={increment}>
                Increment
            </Pressable>
            <Pressable onPress={decrement}>
                decrement
            </Pressable>
    </View>
  );
})

ok,介紹完畢。就是這么簡單!

再仔細看useCounter就是一個自定義hook,別忘了還有個hook目錄,這個目錄可全是自定義hook,這就意味著store里可以根據(jù)需求直接使用我們自定義hook,或者用自定義hook + 自己的業(yè)務代碼拼接成一個全新的store!大大提高效率!so cool!

歸結下優(yōu)點:

核心:state最小粒度,是React的最佳實踐

  • store也可以通過hook進行復用,相同業(yè)務代碼不需要寫多次,復用hook就完事了

  • 一個業(yè)務通過模塊拆分多個store,這個模塊的store還可以繼續(xù)拆分成更多store。所以各個store,會變得更加獨立,降低耦合。不會出現(xiàn)動一發(fā)牽全身

狀態(tài)管理的構建設計就出來了

最后一步,處理復合類型數(shù)據(jù),保證最小粒度更新

舉個例子,請求一個api,獲得結果后更新state,然后視圖更新。

********   請求   ************   更新      *************
* Api  * <=====   *   store  * =========> *    state  *
********  =====>  ************            *************
            結果

第一次api返回的結果

res={
    A : {
        a:1,
        b:2
    },
    B : {
        a:1,
        b:2
    }
}

那這個時候直接setState(res),沒問題,視圖也會正常更新

那么接下來第二次訪問api返回結果

res={
    A : {
        a:1,
        b:2
    },
    B : {
        a:1,
        b:3
    }
}
setState(res)

state.B.b=3

以上兩種操作,視圖是都不會更新的,這里涉及到一個深淺拷貝的問題;

React對于復合類型判斷是否更新是一個淺比較也就是只比較內(nèi)存地址。

所以為了讓視圖更新,我們需要進行深拷貝,拷貝到一個新對象中,這個新對象會分配一個新的內(nèi)存地址

  • 對這個對象進行遍歷,把值全部拷貝到一個新的對象,setState(新對象)。(如果是個復雜對象,想想就頭疼)

  • 將這個對象轉換成json,然后再從json轉換成一個新的對象,然后setState(新對象)。(這性能就不用我多說了吧,一個字,差?。?/p>

現(xiàn)在有一個復雜的對象

state = {
    A : {
        a:1,
        obj1: {
            obj100:{
                o:1
            }
            array:[0,1,2]
        }
        obj2:{
            o:1
            obj100:{
                o:1
            }
            array:[0,1,2]
        }
    },
    ...
}

// 現(xiàn)在需要讓state.A.obj1.obj100.o = 100

ok,就算你湊合湊合的使用類型上述的辦法(畢竟換湯不換藥嘛),但是會發(fā)生一個問題就是,用到這個state,或者只是用到這個state里的某個變量,都會導致這些組件重新渲染。還記得我們前面我們組件的設計是,盡量使用基本類型的props和memo來控制,所以大部分視圖是不會被重新渲染的,只有小部分組件會被渲染,因為那一部分組件用的是對象,即使這個對象里的變量并沒有改變。為什么會這樣呢?

// 深拷貝讓state.A.obj1.obj100.o = 100

state <地址被改變> = {
     A <地址被改變>: {
        a:1,
        obj1 <地址被改變>: {
            obj100 <地址被改變>:{
                o:100 <值被改變>
            }
            array <地址被改變>:[0,1,2]
        }
        obj2 <地址被改變>:{
            o:1
            obj100 <地址被改變>:{
                o:1
            }
            array <地址被改變>:[0,1,2]
        }
    },
    ...
}

以上的結果就可以清楚的知道,所以為什么發(fā)生不必要的渲染了,因為不需要更新對象卻被更新了

所以這個時候得用一個庫叫immer,基于immutable不可變數(shù)據(jù)結構。我就不細說了什么是不可變數(shù)據(jù)結構了,只講結果。

使用

import produce from "immer";

setState(
    produce(state, draft => {
        draft.A.obj1.obj100.o = 100
        })
    )

state被改變后的結構

state <地址被改變> = {
     A : {
        a:1,
        obj1: {
            obj100 <地址被改變>:{
                o:100 <值被改變>
            }
            array:[0,1,2]
        }
        obj2:{
            o:1
            obj100:{
                o:1
            }
            array:[0,1,2]
        }
    },
    ...
}

就這樣就可以渲染“真正需要更新的組件”;簡單的用法,改值最佳性能,實現(xiàn)最小粒度更新;

到這里就該結束了,現(xiàn)在再梳理一下整體的設計,完美!

重要的是思想

順帶推薦一個自己寫的react-native UI庫,完美契合這套架構,sparrowcool;

我是否有必要為這套架構寫一個腳手架?

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容