开源不易,感谢你的支持, ❤ star concent^_^
序言
在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 }
结语
综上所述,我们可以看到其实并没有 消灭 生命周期函数,而是转移并统一了生命周期函数的定义入口,让其和组件的定义彻底分离,这样无论我们怎样重构组件代码,都不怕动到整个模块状态的初始化流程。
附录
和本期主题相近的其他文章
CloudBase CMS
欢迎小哥哥们来撩 CloudBase CMS ,打造一站式云端内容管理系统,它是云开发推出的,基于 Node.js 的 Headless 内容管理平台,提供了丰富的内容管理功能,安装简单,易于二次开发,并与云开发的生态体系紧密结合,助力开发者提升开发效率。
concent
已为其管理后台提供强力支持,新版的管理界面更加美观和体贴了。
FFCreator
也欢迎小哥哥们来撩 FFCreator ,它是一个基于node.js的轻量、灵活的短视频加工库。您只需要添加几张图片或视频片段再加一段背景音乐,就可以快速生成一个很酷的视频短片。
FFCreator
是一种轻量又简单的解决方案,只需要很少的依赖和较低的机器配置就可以快速开始工作。并且它模拟实现了animate.css90%的动画效果,您可以轻松地把 web 页面端的动画效果转为视频,真的很给力。