![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwIjNx8CX39CXy8CXycXZpZVZnFWbp9zZuBnL1kXZyhXcmhmMq9CX5IDO2gDNz8CXlx2YpRnch9CXzV2Zh1WatIXZw9GblZXZk9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
comapi.png
開源不易,感謝你的支援,❤ star me if you like concent ^_^
序言
composition api
(組合api) 和
optional api
(可選api) 兩種組織代碼的方式,相信大家在
vue3
各種相關的介紹文裡已經了解到不少了,它們可以同時存在,并非強制你隻能使用哪一種,但組合api兩大優勢的确讓開發者們更傾向于使用它來替代可選api。
- 以函數為基礎機關來打包可複用邏輯,并注入到任意元件,讓視圖和業務解耦更優雅-
- 讓相同功能的業務更加緊密的放置到一起,不被割裂開,提高開發與維護體驗
以上兩點在react裡均被
hook
優雅的解決了,那麼相比
hook
,組合api還具有什麼優勢呢?這裡就不賣關子了,相信已有小夥伴在尤大大介紹組合api時已經知道,組合api是靜态定義的,解決了
hook
必需每次渲染都重新生成臨時閉包函數的性能問題,也沒有了
hook
裡閉包舊值陷阱,人工檢測依賴等編碼體驗問題。
但是,react是all in js的編碼方式,是以隻要我們敢想、敢做,一切優秀的程式設計模型都可以吸納進來,接下來我們用原生
hook
和concent的
setup
并通過執行個體和講解,來徹底解決尤大提到的這個關于
hook
的痛點吧^_^
react hook
我們在此先設計一個傳統的計數器,要求如下
- 有一個小數,一個大數
- 有兩組加、減按鈕,分别對小數大數做操作,小數按鈕加減1,大數按鈕加減100
- 計數器初次挂載時拉取歡迎問候語
- 當小數達到100時,按鈕變為紅色,否則變為綠色
- 當大數達到1000時,按鈕變為紫色,否則變為綠色
- 當大數達到10000時,上報大數的數字
- 電腦解除安裝時,上報目前的數字
為了完成此需求,我們需要用到以下5把鈎子
useState
過完需求,我們需要用到第一把鈎子
useState
來做元件首次渲染的狀态初始化
function Counter() {
const [num, setNum] = useState(6);
const [bigNum, setBigNum] = useState(120);
}
複制
useCallback
如需使用緩存函數,則要用到第二把鈎子
useCallback
,此處我們使用這把鈎子來定義加減函數
const addNum = useCallback(() => setNum(num + 1), [num]);
const addNumBig = useCallback(() => setBigNum(bigNum + 100), [bigNum]);
複制
useMemo
如需用到緩存的計算結果,則要用到第三把鈎子
useMemo
,此處我們使用這把鈎子來計算按鈕顔色
const numBtnColor = useMemo(() => {
return num > 100 ? 'red' : 'green';
}, [num]);
const bigNumBtnColor = useMemo(() => {
return bigNum > 1000 ? 'purple' : 'green';
}, [bigNum]);
複制
useEffect
處理函數的副作用則需用到第四把鈎子
useEffect
,此處我們用來處理一下兩個需求
- 當大數達到10000時,上報大數的數字
- 電腦解除安裝時,上報目前的數字
useEffect(() => {
if (bigNum > 10000) api.report('reach 10000')
}, [bigNum])
useEffect(() => {
return ()=>{
api.reportStat(num, bigNum)
}
}, [])
複制
useRef
上面使用清理函數的
useEffect
寫法在IDE是會被警告的,因為内部使用了
num, bigNum
變量(不寫依賴會陷入閉包舊值陷阱),是以要求我們聲明依賴
可是如果為了避免IDE警告,我們改為如下方式顯然不是我們表達的本意,我們隻是想元件解除安裝時報告一下數字,而不是每一輪渲染都觸發清理函數
useEffect(() => {
return ()=>{
api.reportStat(num, bigNum)
}
}, [num, bigNum])
複制
這個時候我們需要第5把鈎子
useRef
,來幫忙我們固定依賴了,是以正确的寫法是
const ref = useRef();// ref是一個固定的變量,每一輪渲染都指向同一個值
ref.current = {num, bigNum};// 幫我們記住最新的值
useEffect(() => {
return () => {
const {num, bigNum} = ref.current;
reportStat(num, bigNum);
};
}, [ref]);
複制
完整的計數器
使完5把鈎子,我們完整的元件如下
function Counter() {
const [num, setNum] = useState(88);
const [bigNum, setBigNum] = useState(120);
const addNum = useCallback(() => setNum(num + 1), [num]);
const addNumBig = useCallback(() => setBigNum(bigNum + 100), [bigNum]);
const numBtnColor = useMemo(() => {
return num > 100 ? "red" : "green";
}, [num]);
const bigNumBtnColor = useMemo(() => {
return bigNum > 1000 ? "purple" : "green";
}, [bigNum]);
useEffect(() => {
if (bigNum > 10000) report("reach 10000");
}, [bigNum]);
const ref = useRef();
ref.current = {num, bigNum};
useEffect(() => {
return () => {
const {num, bigNum} = ref.current;
reportStat(num, bigNum);
};
}, [ref]);
// render ui ...
}
複制
當然我們可以基于
hook
可定制的特性,将這段代碼單獨抽象為一個鈎子,這樣的話隻需将資料和方法導出,以便讓多種ui表達的Counter元件可以複用,同時也做到ui與業務隔離,利于維護。
function useMyCounter(){
// .... 略
return { num, bigNum. addNum, addNumBig, numBtnColor, bigNumBtnColor}
}
複制
concent setup
hook
函數在每一輪渲染期間一定是需要全部重新執行一遍的,是以不可避免的在每一輪渲染期間都會産生大量的臨時閉包函數,如果我們能省掉他們,的确能幫gc減輕一些回收壓力的,現在我們來看看使用
setup
改造完畢後的Counter會是什麼樣子吧。
使用
concent
非常簡單,隻需要在根元件之前,先使用
run
api啟動即可,是以處我們沒有子產品定義,直接調用就可以了。
import { run } from 'concent';
run();// 先啟動,在render
ReactDOM.render(<App />, rootEl)
複制
接着我們将以上邏輯稍加改造,全部包裹到
setup
内部,setup函數内部的邏輯隻會被執行一次,需要用到的由渲染上下文
ctx
提供的api有
initState
、
computed
、
effect
、
setState
,同時配合
setState
調用時還需要讀取的狀态
state
,也由
ctx
獲得。
function setup(ctx) {// 渲染上下文
const { initState, computed, effect, state, setState } = ctx;
// setup僅在元件首次渲染之前執行一次,我們可在内部書寫相關業務邏輯
}
複制
initState
initState
用于初始化狀态,替代了
useState
,當我們的元件狀态較大時依然可以不用考慮如何切分狀态粒度。
initState({ num: 6, bigNum: 120 });
複制
此處也支援函數是寫法初始化狀态
initState(()=>({ num: 6, bigNum: 120 }));
複制
computed
computed
用于定義計算函數,從參數清單裡解構時就确定了計算的輸入依賴,相比
useMemo
,更直接與優雅。
// 僅當num發生變化時,才觸發此計算函數
computed('numBtnColor', ({ num }) => (num > 100 ? 'red' : 'green'));
複制
此處我們需要定義兩個計算函數,可以用你計算對象描述體來配置計算函數,這樣隻需調用一次
computed
即可
computed({
numBtnColor: ({ num }) => num > 100 ? 'red' : 'green',
bigNumBtnColor: ({ bigNum }) => bigNum > 1000 ? 'purple' : 'green',
});
複制
effect
effect
的用法和
useEffect
是一模一樣的,差別僅僅是依賴數組僅傳入key名稱即可,同時
effect
内部将函數元件和類元件的生命周期進行了統一封裝,使用者可以将業務不做任何修改便遷移到類元件身上
effect(() => {
if (state.bigNum > 10000) api.report('reach 10000')
}, ['bigNum'])
effect(() => {
// 這裡可以書寫首次渲染完畢時需要做的事情
return () => {
// 解除安裝時觸發的清理函數
api.reportStat(state.num, state.bigNum)
}
}, []);
複制
setState
用于修改狀态,我們在
setup
内部基于
setState
定義完方法後,然後傳回即可,接着我們可以在任意使用此
setup
的元件裡,通過
ctx.settings
拿到這些方法句柄便可調用
function setup(ctx) {// 渲染上下文
const { state, setState } = ctx;
return {// 導出方法
addNum: () => setState({ num: state.num + 1 }),
addNumBig: () => setState({ bigNum: state.bigNum + 100 }),
}
}
複制
完整的Setup Counter
基于上述幾個api,我們最終的Counter的邏輯代碼如下
function setup(ctx) {// 渲染上下文
const { initState, computed, effect, state, setState } = ctx;
// 初始化資料
initState({ num: 6, bigNum: 120 });
// 定義計算函數
computed({
// 參數清單解構時就确定了計算的輸入依賴
numBtnColor: ({ num }) => num > 100 ? 'red' : 'green',
bigNumBtnColor: ({ bigNum }) => bigNum > 1000 ? 'purple' : 'green',
});
// 定義副作用
effect(() => {
if (state.bigNum > 10000) api.report('reach 10000')
}, ['bigNum'])
effect(() => {
return () => {
api.reportStat(state.num, state.bigNum)
}
}, []);
return {// 導出方法
addNum: () => setState({ num: state.num + 1 }),
addNumBig: () => setState({ bigNum: state.bigNum + 100 }),
}
}
複制
定義完核心的業務邏輯,緊接着,我們可在任意函數元件内部使用
useConcent
裝配我們定義好的
setup
來使用它了,
useConcent
會傳回一個渲染上下文(和setup函數參數清單裡指的是同一個對象引用,有時我們也稱執行個體上下文),我們可按需獲從
ctx
上取出目标資料和方法,針對此示例,我們可以導出
state
(資料),
settings
(setup打包傳回的法法),
refComputed
(執行個體的計算函數結果容器)這3個key來使用即可。
import { useConcent } from 'concent';
function NewCounter() {
const { state, settings, refComputed } = useConcent(setup);
// const { num, bigNum } = state;
// const { addNum, addNumBig } = settings;
// const { numBtnColor, bigNumBtnColor } = refComputed;
}
複制
我們上面提到
setup
同樣可以裝配給類元件,使用
register
即可,需要注意的是裝配後的類元件,可以從
this.ctx
上直接擷取
concent
為其生成的渲染上下文,同時呢
this.state
和
this.ctx.state
是等效的,
this.setState
和
this.ctx.setState
也是等效的,友善使用者代碼0改動即可接入
concent
使用。
import { register } from 'concent';
@register(setup)
class NewClsCounter extends Component{
render(){
const { state, settings, refComputed } = this.ctx;
}
}
複制
結語
對比原生hook,
setup
将業務邏輯固定在隻會被執行一次的函數内部,提供了更友好的api,且同時完美相容類元件與函數元件,讓使用者可以逃離
hook
的使用規則煩惱(想想看 useEffect 配合 useRef,是不是都有不小的認知成本?),而不是将這些限制學習障礙轉嫁給使用者, 同時對gc也更加友好了,相信大家都已預設了
hook
是
react
的一個重要發明,但是其實它不是針對使用者的,而是針對架構的,使用者其實是不需要了解那些燒腦的細節與規則的,而對于concent使用者來說,其實隻需一個鈎子開啟一個傳送門,即可在另一個空間内部實作所有業務邏輯,而且這些邏輯同樣可以複用到類元件上。
親愛的客官看了這麼多,還不趕緊上手試試,以下提供了兩種寫法的連結,供你把玩😀
- 原始hook Counter
- setup Counter
one more thing
上訴兩個hook Counter如果想做狀态共享,我們需要改造代碼接入
redux
或者自建
Context
,但是在
concent
的開發模式下,
setup
無需任何改造,僅僅隻需要提前聲明一個子產品,然後注冊元件内屬于該子產品即可,這種絲滑般的遷移過程可以讓使用者靈活應對各種複雜場景。
import { run } from 'concent';
run({
counter:{
state: { num:88, bigNum: 120 },
},
//reducer: {...}, // 如操作資料流程複雜,可再将業務提升到此處
})
// 對于函數元件
useConcent({setup});
// ---> 改為
useConcent({setup, module:'counter'})
// 對于函數元件
@register({setup});
// ---> 改為
@register({setup, module:'counter'});
複制
- shared Counter
完
❤ star me if you like concent ^_^
Edit on CodeSandbox
https://codesandbox.io/s/concent-guide-xvcej
Edit on StackBlitz
https://stackblitz.com/edit/cc-multi-ways-to-wirte-code
如果有關于concent的疑問,可以掃碼加群咨詢,以便幫助你了解更多😀。