天天看點

[Concent小課堂]認識組合api,換個姿勢撸更清爽的react

[Concent小課堂]認識組合api,換個姿勢撸更清爽的react

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

[Concent小課堂]認識組合api,換個姿勢撸更清爽的react

https://codesandbox.io/s/concent-guide-xvcej

Edit on StackBlitz

[Concent小課堂]認識組合api,換個姿勢撸更清爽的react

https://stackblitz.com/edit/cc-multi-ways-to-wirte-code

如果有關于concent的疑問,可以掃碼加群咨詢,以便幫助你了解更多😀。

[Concent小課堂]認識組合api,換個姿勢撸更清爽的react
[Concent小課堂]認識組合api,換個姿勢撸更清爽的react