出處:https://segmentfault.com/a/1190000038671479
序言在react應用裡,存在一個頂層元件,該元件的生命週期很長,除了人為的呼叫 unmountComponentAtNode 介面來解除安裝掉它和使用者關閉掉瀏覽器tab頁視窗,該頂層元件是不會有被銷燬的時機的,它一直伴隨著整個應用,所以我們都會在該元件的 componentDidMount 函數里發起一些請求來獲取伺服器端的配置型資料並快取起來,方便整個應用全域性使用。
對於由路由系統掛載的頁面元件,我們通常也會在它的 componentDidMount 函數里發起請求來獲取該頁面,如果狀態是由 store 管理的(如redux、或者mobx),若需要在頁面元件的解除安裝的時候清理相應的store狀態,則還會選擇在 componentWillUnmount 裡呼叫相應的方法做清理。
當然了,對於函式元件來說使用 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.mounted 和 lifecyle.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}
結語
綜上所述,我們可以看到其實並沒有 消滅 生命週期函式,而是轉移並統一了生命週期函式的定義入口,讓其和元件的定義徹底分離,這樣無論我們怎樣重構元件程式碼,都不怕動到整個模組狀態的初始化流程。
出處:https://segmentfault.com/a/1190000038671479