React Native面向未來(lái)架構(gòu)

既然面向未來(lái)殃恒,那么項(xiàng)目的架構(gòu)設(shè)計(jì)采用React/React native的新特性,并且為即將來(lái)到的新特性預(yù)留位置;對(duì)核心依賴(lài)庫(kù)保持最小代碼侵略性和最底依賴(lài)掰读;且滿足以下這幾點(diǎn)。

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

我們先從項(xiàng)目最小的一個(gè)點(diǎn)開(kāi)始講:組件

這里有一個(gè)性能關(guān)鍵點(diǎn), 舉個(gè)例子

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

把這兩個(gè)組件拼在一起,每次點(diǎn)擊A組件的時(shí)候都會(huì)讓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 />
        </>
    )
}

想象中的打印結(jié)果

render App
render A

真實(shí)的打印結(jié)果:

render App
render A
render B

我第一次發(fā)現(xiàn)的時(shí)候叭莫,竟然是這樣的打印結(jié)果蹈集,我懵了都,難道不會(huì)幫我控制性能嗎雇初? React 就這拢肆?

打印結(jié)果表明:每一次父組件的state更新,都會(huì)導(dǎo)致所有子組件重新渲染靖诗。那這樣性能豈不是太差了郭怪!

其實(shí)這不并不是真正意義上的渲染,react底層會(huì)有diff算法刊橘,會(huì)幫你更新“真正需要更新的組件”鄙才。

所以我們要手動(dòng)控制,趕在diff算法之前不讓它“渲染”促绵;其實(shí)很簡(jiǎn)單攒庵,React.memo(組件),就這樣套住組件就可以了败晴,就可以得到我們想象中的打印結(jié)果了浓冒;React.memoReact.PureComponent類(lèi)似,作用:檢查Props是不是變了位衩,如果變了才會(huì)真正的去更新裆蒸,但得記住這是一個(gè)淺比較。

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

Ok糖驴,正文開(kāi)始僚祷,設(shè)計(jì)組件佛致。

我們先看一個(gè)頁(yè)面的組成是這個(gè)樣子的

所以大多數(shù)項(xiàng)目的目錄結(jié)構(gòu)都會(huì)有一個(gè)components目錄,顧名思義這個(gè)目錄用來(lái)存放公用組件

都知道React是組件化思想辙谜。那你真的理解了什么是組件化嗎俺榆?開(kāi)發(fā)的時(shí)候,覺(jué)得這個(gè)組件是可以抽出來(lái)的装哆,就丟到components目錄下去了罐脊?

這是一個(gè)非常糟糕的做法。公用組件意味著無(wú)副作用蜕琴,大膽用放心用萍桌。如果只是你覺(jué)得可以抽出來(lái)做一個(gè)組件,放到components目錄下凌简,那這個(gè)組件如果包含業(yè)務(wù)代碼怎么辦上炎?開(kāi)發(fā)人員還要閱讀你的這個(gè)組件,確保沒(méi)有副作用雏搂,才能使用藕施,降低了效率,嚴(yán)重的話還會(huì)產(chǎn)生bug凸郑。

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

組件其實(shí)是分為兩種組件

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

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

在React 16.8版本之前配紫,也就是還沒(méi)有推出hooks之前,可以這樣設(shè)計(jì)組件午阵,就可以非常好的區(qū)分

  • 無(wú)狀態(tài)組件:使用function來(lái)寫(xiě)組件

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

我們的架構(gòu)是圍繞React/React native新特性躺孝,當(dāng)然得用hook+函數(shù),無(wú)class該如何設(shè)計(jì)組件底桂?

一個(gè)頁(yè)面的視圖組成部分應(yīng)該是這樣的

所以我們需要為組件分兩個(gè)目錄

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

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

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

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

  • component必須使用React.memo來(lái)控制

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

  • props必須或盡量是基本類(lèi)型(例如:string秀存,number,組件保持最小粒度化了開(kāi)發(fā)羽氮,所以滿足props是基本類(lèi)型這一點(diǎn)非常簡(jiǎn)單或链;其次,基本類(lèi)型的props档押,才能發(fā)揮React.memo真正的作用)

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

component偽代碼像這樣

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

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

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

    return (
       // 視圖
    )
})

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

核心:一個(gè)container令宿,渲染部分應(yīng)該盡量使用component進(jìn)行拼接組成

  • container同樣使用React.memo來(lái)控制

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

  • props盡量是基本類(lèi)型

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

container偽代碼如下

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

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

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

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

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

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

區(qū)別顯而易見(jiàn)粒没,結(jié)論:

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

  • 最大程度的復(fù)用筛婉,而且沒(méi)有副作用。復(fù)用也是間接減少bug的革娄,因?yàn)槟銖?fù)用的代碼是上個(gè)版本的倾贰,上個(gè)版本是經(jīng)過(guò)測(cè)試的,我們可以默認(rèn)它是無(wú)bug拦惋,穩(wěn)定的代碼匆浙。

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

說(shuō)完組件設(shè)計(jì)厕妖,接下來(lái)是邏輯的復(fù)用

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

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

依靠hook的特性,只要你覺(jué)得抽出去有價(jià)值那就抽言秸!這樣不光是container還是component都可以復(fù)用邏輯软能,又提高效率了!所以又多出了一個(gè)新目錄:hooks目錄

所以視圖部分架構(gòu)就誕生了举畸!

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

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

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

  • hooks 存放可復(fù)用hook

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

不用多說(shuō)抄沮,先多一個(gè)store目錄

mobxredux大家應(yīng)該都很熟悉了跋核,這里就不介紹了;這兩個(gè)都有為hook叛买,提供了新的狀態(tài)管理庫(kù)(但是看下來(lái)都比較重砂代,且代碼侵略性比較高,對(duì)于hook來(lái)說(shuō)有一種沒(méi)有必要抽象的感覺(jué))率挣,所以我們采用全新的狀態(tài)管理工具hox

hox優(yōu)點(diǎn)

  • 只有一個(gè)api刻伊,簡(jiǎn)單,上手快,足夠輕

  • 非常易于復(fù)用

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

  • 完美支持typescript

  • 對(duì)代碼侵略性非常底智什,它不會(huì)成為我們支持未來(lái)的新特性,三方庫(kù)的升級(jí)讼呢,甚至重構(gòu)的負(fù)擔(dān)

簡(jiǎn)單介紹下用法撩鹿,大家肯定都有用過(guò)mobx/redux,為了更好理解悦屏,所以持久化的狀態(tài)节沦,就稱(chēng)之為store。

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

// -------------------創(chuàng)建一個(gè)持久化store------------------------
const useCounter =() => {
    // count這個(gè)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);
// 或者干脆整個(gè)store甫贯,直接用自定義hook
// export default createModel(hook);

// -------------------使用這個(gè)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,介紹完畢。就是這么簡(jiǎn)單看蚜!

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

歸結(jié)下優(yōu)點(diǎn):

核心:state最小粒度惨奕,是React的最佳實(shí)踐

  • store也可以通過(guò)hook進(jìn)行復(fù)用,相同業(yè)務(wù)代碼不需要寫(xiě)多次竭钝,復(fù)用hook就完事了

  • 一個(gè)業(yè)務(wù)通過(guò)模塊拆分多個(gè)store梨撞,這個(gè)模塊的store還可以繼續(xù)拆分成更多store。所以各個(gè)store香罐,會(huì)變得更加獨(dú)立卧波,降低耦合。不會(huì)出現(xiàn)動(dòng)一發(fā)牽全身

狀態(tài)管理的構(gòu)建設(shè)計(jì)就出來(lái)了

最后一步庇茫,處理復(fù)合類(lèi)型數(shù)據(jù)港粱,保證最小粒度更新

舉個(gè)例子,請(qǐng)求一個(gè)api旦签,獲得結(jié)果后更新state啥容,然后視圖更新。

********   請(qǐng)求   ************   更新      *************
* Api  * <=====   *   store  * =========> *    state  *
********  =====>  ************            *************
            結(jié)果

第一次api返回的結(jié)果

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

那這個(gè)時(shí)候直接setState(res)顷霹,沒(méi)問(wèn)題,視圖也會(huì)正常更新

那么接下來(lái)第二次訪問(wèn)api返回結(jié)果

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

state.B.b=3

以上兩種操作击吱,視圖是都不會(huì)更新的淋淀,這里涉及到一個(gè)深淺拷貝的問(wèn)題;

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

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

  • 對(duì)這個(gè)對(duì)象進(jìn)行遍歷袍辞,把值全部拷貝到一個(gè)新的對(duì)象鞋仍,setState(新對(duì)象)。(如果是個(gè)復(fù)雜對(duì)象搅吁,想想就頭疼)

  • 將這個(gè)對(duì)象轉(zhuǎn)換成json威创,然后再?gòu)膉son轉(zhuǎn)換成一個(gè)新的對(duì)象,然后setState(新對(duì)象)谎懦。(這性能就不用我多說(shuō)了吧肚豺,一個(gè)字,差=缋埂)

現(xiàn)在有一個(gè)復(fù)雜的對(duì)象

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吸申,就算你湊合湊合的使用類(lèi)型上述的辦法(畢竟換湯不換藥嘛),但是會(huì)發(fā)生一個(gè)問(wèn)題就是享甸,用到這個(gè)state截碴,或者只是用到這個(gè)state里的某個(gè)變量,都會(huì)導(dǎo)致這些組件重新渲染蛉威。還記得我們前面我們組件的設(shè)計(jì)是日丹,盡量使用基本類(lèi)型的props和memo來(lái)控制,所以大部分視圖是不會(huì)被重新渲染的瓷翻,只有小部分組件會(huì)被渲染聚凹,因?yàn)槟且徊糠纸M件用的是對(duì)象,即使這個(gè)對(duì)象里的變量并沒(méi)有改變齐帚。為什么會(huì)這樣呢妒牙?

// 深拷貝讓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]
        }
    },
    ...
}

以上的結(jié)果就可以清楚的知道,所以為什么發(fā)生不必要的渲染了对妄,因?yàn)椴恍枰聦?duì)象卻被更新了

所以這個(gè)時(shí)候得用一個(gè)庫(kù)叫immer湘今,基于immutable不可變數(shù)據(jù)結(jié)構(gòu)。我就不細(xì)說(shuō)了什么是不可變數(shù)據(jù)結(jié)構(gòu)了剪菱,只講結(jié)果摩瞎。

使用

import produce from "immer";

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

state被改變后的結(jié)構(gòu)

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

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

到這里就該結(jié)束了构灸,現(xiàn)在再梳理一下整體的設(shè)計(jì)上渴,完美!

重要的是思想

順帶推薦一個(gè)自己寫(xiě)的react-native UI庫(kù),完美契合這套架構(gòu)稠氮,sparrowcool曹阔;

我是否有必要為這套架構(gòu)寫(xiě)一個(gè)腳手架?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末隔披,一起剝皮案震驚了整個(gè)濱河市赃份,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奢米,老刑警劉巖抓韩,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異恃慧,居然都是意外死亡园蝠,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)痢士,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)彪薛,“玉大人,你說(shuō)我怎么就攤上這事怠蹂∩蒲樱” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵城侧,是天一觀的道長(zhǎng)易遣。 經(jīng)常有香客問(wèn)我,道長(zhǎng)嫌佑,這世上最難降的妖魔是什么豆茫? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮屋摇,結(jié)果婚禮上揩魂,老公的妹妹穿的比我還像新娘。我一直安慰自己炮温,他們只是感情好火脉,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著柒啤,像睡著了一般倦挂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上担巩,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天方援,我揣著相機(jī)與錄音,去河邊找鬼涛癌。 笑死肯骇,一個(gè)胖子當(dāng)著我的面吹牛窥浪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笛丙,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼假颇!你這毒婦竟也來(lái)了胚鸯?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤笨鸡,失蹤者是張志新(化名)和其女友劉穎姜钳,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體形耗,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哥桥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了激涤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拟糕。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖倦踢,靈堂內(nèi)的尸體忽然破棺而出送滞,到底是詐尸還是另有隱情,我是刑警寧澤辱挥,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布犁嗅,位于F島的核電站,受9級(jí)特大地震影響晤碘,放射性物質(zhì)發(fā)生泄漏褂微。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一园爷、第九天 我趴在偏房一處隱蔽的房頂上張望宠蚂。 院中可真熱鬧,春花似錦腮介、人聲如沸肥矢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)甘改。三九已至,卻和暖如春灭抑,著一層夾襖步出監(jiān)牢的瞬間十艾,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工腾节, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留忘嫉,地道東北人荤牍。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像庆冕,于是被迫代替她去往敵國(guó)和親康吵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348