既然面向未來,那么項目的架構設計采用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.memo和React.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ū)別顯而易見,結論:
component和container的唯一區(qū)別就是有無store最大程度的復用,而且沒有副作用。復用也是間接減少bug的,因為你復用的代碼是上個版本的,上個版本是經(jīng)過測試的,我們可以默認它是無bug,穩(wěn)定的代碼。
實現(xiàn)最小粒度更新,完美控制性能
說完組件設計,接下來是邏輯的復用
component和container描述和偽代碼,我們可以看到有相同部分
state生命周期業(yè)務邏輯
依靠hook的特性,只要你覺得抽出去有價值那就抽!這樣不光是container還是component都可以復用邏輯,又提高效率了!所以又多出了一個新目錄:hooks目錄
所以視圖部分架構就誕生了!
ok,到現(xiàn)在為止就有3個目錄了
components無狀態(tài)組件container有狀態(tài)組件hooks存放可復用hook
說完視圖部分,接下就是狀態(tài)管理了
不用多說,先多一個store目錄
mobx和redux大家應該都很熟悉了,這里就不介紹了;這兩個都有為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;
我是否有必要為這套架構寫一個腳手架?




