如何優雅的消滅掉react生命週期函式

鍾正楷發表於2020-12-26

開源不易,感謝你的支援,❤ star concent^_^

elegant.png

序言

在react應用裡,存在一個頂層元件,該元件的生命週期很長,除了人為的呼叫unmountComponentAtNode介面來解除安裝掉它和使用者關閉掉瀏覽器tab頁視窗,該頂層元件是不會有被銷燬的時機的,它一直伴隨著整個應用,所以我們都會在該元件的componentDidMount函式裡發起一些請求來獲取伺服器端的配置型資料並快取起來,方便整個應用全域性使用。

對於由路由系統掛載的頁面元件,我們通常也會在它的componentDidMount函式裡發起請求來獲取該頁面,如果狀態是由store管理的(如redux、或者mobx),若需要在頁面元件的解除安裝的時候清理相應的store狀態,則還會選擇在componentWillUnmount裡呼叫相應的方法做清理。

image.png

當然了,對於函式元件來說使用useEffect鉤子函式做起來就一步到位,比起類元件顯得更簡單

function PageComp(){
  useEffect(()=>{
    /** 等效於 componentDidMount 發起請求呼叫 */
    return ()=>{
      /** 等效於 componentWillUnmount 做相應的清理 */
    }
  }, [])
}

當前生命週期函式的使用體驗

那本文題目提到的消滅生命週期又作何解釋呢?看起來沒有了它們我們是無法完成類似需求的,在對此作出解釋之前,我們先列舉一下現在的生命週期的使用體驗問題。

無法共用一套邏輯

類元件和函式元件是無法做到0修改共用一套邏輯的,類元件在未來的很長一段時間內都將一直存在,這是我們無法避免的問題,但類元件和函式元件的設計理念導致它們的生命週期函式使用方式是完全不同的,所以共享邏輯需要一定的改造

初始化流程和元件耦合在一起

已提升到store的狀態的初始化流程卻還是和元件耦合在一起,這一點一定要注意一個前提,就是我們通常在頂層元件的生命週期函式裡完成store的某個節點的狀態初始化,不管是根元件還是頁面元件,它們都具有頂層元件的性質,但是把store某節點的狀態初始化流程寫在元件裡會帶來一些額外的問題,

  • 如果另一個頁面元件也需要使用該節點資料時,需要額外的檢查狀態有沒有初始化好
  • 當重構頂層元件的時候要小心翼翼的維護好這些宣告週期邏輯

接下里讓我們看看在concent裡是如何處理這些問題並消滅掉生命週期函式的呢。

使用組合api統一邏輯

雖然類元件和函式的生命週期宣告方式和使用方式完全不一樣,但是我們可以依靠組合api來抹掉這層差異,達到讓類元件和函式元件都真正的只充當ui載體的目的

假設有以下兩個自管理狀態的元件,他們都具有相同的功能,一個是類元件

class ClsPageComp extends React.Component{
  state = {
    list: [],
    page: 1,
  };
  componentDidMount(){
    fetchData();
  }
  componentWillUnmount(){
    /** clear up */
  }
  fetchData = () => {
    const { page } = this.state;
    fetch('xxxx', { page }).then(list => this.setState({ list }))
  }
  nextPage = () => {
    this.setState({ page: this.page + 1 }, this.fetchData);
  }
  render() {
    /** ui logic */
  }
}

一個是函式元件

// 函式元件
function PageComp() {
  const [list, setList] = useState([]);
  const [page, setPage] = useState(1);

  const pageRef = useRef(page);
  pageRef.current = page;

  const fetchData = (page) => {
    // fetch("xxxx", { page }).then((list) => setList(list));
  };

  const nextPage = () => {
    const p = page + 1;
    setPage(p);
    fetchData(p);
  };

  useEffect(() => {
    fetchData(pageRef.current);
    return () => {
      /** clear up */
    };
  }, []);

  /** ui logic */
}

兩者看起來完完全全不一樣,且函式元件裡為了消除useEffect依賴缺失警告還是用useRef來固定住目標值,這些比較燒腦的操作對於新使用者來說是非常大的障礙。

接下來我們看看基於setup的組合api如何來解除這些障礙,setup是一個普通的函式,僅提供一個引數代表當前的渲染上下文,並支援返回一個新的物件(通常都是一堆方法集合),該物件能夠通過settings在渲染塊內獲取到,裝配了setup函式的元件在例項化時,僅被觸發執行一次,所以我們可以看看上述示例改造後,會變為:

function setup(ctx) {
  const { initState, setState, state, effect } = ctx;
  initState({ list: [], page: 0 });

  const fetchData = (page) => {
    fetch('xxxx', { page }).then(list => setState({ list }))
  };

  effect(()=>{
    fetchData(state.page);
    return ()=>{
       /** clear up */
    };
  }, []);

  return {
    nextPage: () => {
      const p = page + 1;
      setState({ page: p });
      fetchData(p);
    }
  };
}

接著在類元件裡和函式元件裡,都可通過渲染上下文ctx拿到資料和方法

import { register, useConcent } from 'concent';

@register({ setup })
class ClsComp extends React.Component {
  render() {
    const { state: { page, list }, settings: { nextPage } } = this.ctx;
    // ui logic
  }
}

function PageComp() {
  const {
    state: { page, list }, settings: { nextPage },
  } = useConcent({ setup });
  // ui logic
}

使用lifecyle消除生命週期

當我們的頁面元件狀態提升到模組裡時,我們可以使用lifecyle.mountedlifecyle.willUnmount來徹底解耦生命週期和元件的關係了,concent內部會維護一個模組對應下的例項計數器,所以依靠這個功能可以精確控制模組狀態的初始化時機了。

lifecyle.mounted

當前模組的第一個例項掛載完畢時觸發,且僅觸發一次,即當該模組的所有例項都銷燬後,再次有一個例項掛載完畢,也不會觸發了

run({
  product: { 
    lifecycle: {
      mounted: (dispatch)=> dispatch('initState')
    }  
  }
})

如需反覆觸發,即只要滿足模組的例項數從0到1時就觸發,返回false即可

lifecyle.willUnmount

當前模組的最後一個例項將銷燬時觸發,且僅觸發一次,即當該模組再次生成了很多例項,然後又全部銷燬,也不會觸發了

run({
  counter: { 
    lifecycle: {
      willUnmount: dispatch=> dispatch('clearModuleState'),
    }  
  }
})

同樣的如需反覆觸發,即只要滿足模組的例項數從有變為0時就觸發,返回false即可

lifecyle.loaded

如果該模組的狀態和有無元件掛載無關係,則直接配置loaded即可

run({
  counter: { 
    lifecycle: {
      loaded: (dispatch)=> dispatch('initState'),
    }  
  }
})

改造示例

介紹完lifecyle,我們來看看改造上述函式元件和類元件後的例項長為什麼樣,首先我們定義product模組

import { run } from 'concent';

run({
  product: {
    state: { list: [], page: 1 },
    reducer: {
      async initState() {
        /** init state logic */
      },
      clearState() {
        /** clear state logic */
      },
      async nextPage(payload, moduleState, ac) {
        const p = moduleState.page + 1;
        await ac.setState({ paeg: p });
        const list = await fetch('xxxx', { page: p });
        return { list };
      }
    },
    lifecycle: {
      mounted: dispatch => dispatch('initState'),
      willUnmount: dispatch => dispatch('clearState'),
    }
  }
});

接著我們註冊元件屬於product模組即可,元件例項就可以呼叫product模組的方法和讀取它的資料了。

import { register, useConcent } from 'concent';

@register({ module: 'product' })
class ClsComp extends React.Component {
  render() {
    const { state: { page, list }, mr: { nextPage } } = this.ctx;
    // ui logic
  }
}

function PageComp() {
  const {
    state: { page, list }, mr: { nextPage },
  } = useConcent({ module: 'product' });
  // ui logic
}

我們可以看到此時已沒有了setup,是因為我們不需要額外定義方法和資料了,當我們需要為元件定義一些非模組的方法和資料時,依然可以定義setup

function setup(ctx) {
  const { initState, setState, state, effect } = ctx;
  initState({ xxxx: 'hey i am private' });
  effect(()=>{
   // 等效於useEffect裡,當xxxx改變時執行此副作用
   console.log(state.xxxx);
  }, ['xxxx']);

  return {
    changeXXX: (e)=> setState({xxxx: e.target.value}),
  };
}

然後元件裝配setup即可

import { register, useConcent } from 'concent';

@register({ module: 'product', setup })
class ClsComp extends React.Component {
  render() {
    const { state: { page, list }, mr: { nextPage }, settings } = this.ctx;
    // ui logic
  }
}

function PageComp() {
  const {
    state: { page, list }, mr: { nextPage }, settings,
  } = useConcent({ module: 'product', setup });
  // ui logic
}

結語

綜上所述,我們可以看到其實並沒有消滅生命週期函式,而是轉移並統一了生命週期函式的定義入口,讓其和元件的定義徹底分離,這樣無論我們怎樣重構元件程式碼,都不怕動到整個模組狀態的初始化流程。

附錄

和本期主題相近的其他文章

CloudBase CMS

歡迎小哥哥們來撩CloudBase CMS ,打造一站式雲端內容管理系統,它是雲開發推出的,基於 Node.js 的 Headless 內容管理平臺,提供了豐富的內容管理功能,安裝簡單,易於二次開發,並與雲開發的生態體系緊密結合,助力開發者提升開發效率。

concent已為其管理後臺提供強力支援,新版的管理介面更加美觀和體貼了。

FFCreator

也歡迎小哥哥們來撩FFCreator,它是一個基於node.js的輕量、靈活的短視訊加工庫。您只需要新增幾張圖片或視訊片段再加一段背景音樂,就可以快速生成一個很酷的視訊短片。

FFCreator是一種輕量又簡單的解決方案,只需要很少的依賴和較低的機器配置就可以快速開始工作。並且它模擬實現了animate.css90%的動畫效果,您可以輕鬆地把 web 頁面端的動畫效果轉為視訊,真的很給力。

相關文章