转自公司内大佬的文章,深入浅出的写法值得学习
TLDR
源码实现
1 | /** 可在同一函数重复多次使用,互不干扰 */ |
使用例子
1 | function Example() { |
特点
- 可以模拟构造函数,只有一次调用
- 可以在渲染之前调用
- 有返回值时,多次调用的返回值结果一样,就像单例函数
详细说明
函数组件是 React 推出的新的组件形式,让写组件变得简单些,它抛弃了复杂的生命周期回调,专注于展示和数据监听。下面这是一个例子:
1 | import React, { useState } from 'react'; |
函数组件返回一个样式,react 会在每次数据变化的时候就调用一次此方法获取最新样式。
大部分情况,函数组件都能满足需求。但有一个业务场景函数组件不支持,就是构造函数。函数组件实际是一个函数,自然肯定没有 class 的构造函数。那对一些一次性的初始化逻辑该如何实现呢?React 的官方解释是函数组件不需要构造函数,因为状态初始化可以用 useState。
官方解释对不对是值得商榷的,因为初始化阶段不只是状态初始化,也会包含一些业务逻辑。那既然官方不准备支持,那我们有什么办法模拟呢?
首先我们能想到的是 useEffect 方法。useEffect 会在 render 结束后调用(useLayoutEffect同理,只是时机不同)。它会接受第二个参数,只关注某些状态变化才调用。利用这一点,我们可以传入空数组,也就是只关注第一次:
1 | useEffect(() => { |
这样的实现基本满足我们只执行一次的要求,但有一个缺点,执行的时机是在渲染后,我们有时候会想在渲染前作准备。要适应这种场景,我们可以用 useState 来模拟,设置一个标志位来来表示是不是第一次执行。可以写一个自定义 hook 工具方法:
1 | const useConstructor(callBack = () => {}) => { |
这样,我们在函数组件第一行调用这个方法,包住初始化逻辑就能满足在渲染前调用:
1 | function Example() { |
这样是不是完美模拟了呢?其实还是有两个小缺点:
- 调用 setHasBeenCalled 会触发一次不必要的刷新;
- useConstructor 命名不合适,因为不是真的构造函数,实际调用点是靠使用方在什么时候调用;
基于这两点,我们可以优化一下成这样:
1 | const useSingleton = (callBack = () => {}) => { |
这个函数实际是一个单例函数,确保传入的函数只执行一次。useRef 可以理解为函数组件的全局变量,对它修改不会触发刷新。
这里有一点需要说一下,useSingleton 是可以在同一个函数组件里多次使用,useRef 会每次都生成新的变量,React 会保证不会相互干扰并正确执行。
这个模拟构造函数还能不能进一步优化呢,比如期望 useSingleton 有返回值,一次执行,每次都获取相同的结果?
可以的。根据 Reactjs 官方文档的惰性初始State, 可以给 useState 传函数,并且只会在第一次初始化时调用。所以我们可以进一步优化 useSingleton,再加上考虑返回值的话,最终版以及用法见文章开头
参考:https://dev.to/bytebodger/constructors-in-functional-components-with-hooks-280m