既然面向未來(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.memo
和React.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é)論:
component
和container
的唯一區(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ù)用
component
和container
描述和偽代碼首尼,我們可以看到有相同部分
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目錄
mobx
和redux
大家應(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è)腳手架?