深入浅出,动手写个useMount玩玩

2,613 阅读7分钟

今天的目标

自己实现个hook,useMount,一眼就能看出来,如果对应到Class Component组件的话,那就是componentDidMount,顺便我们扫个盲,从理论基础上一一剖析下。但是避免讲的过于生涩,我们会忽略一些无关的概念。

本文中的例子,由于笔者的习惯问题会使用TypeScript去实现,但是除了添加了基本类型声明,不会有其他复杂的概念,大家练习的时候如果有障碍,可以去掉其中的类型。

按照React官网的定义,componentDidMount的描述为:

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

你可以在 componentDidMount() 里直接调用 setState() 。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 和 tooltips 等情况下,你可以使用此方式处理

我们知道,在Function Component中就无法使用生命周期函数了,那么如何在Hooks中,也让我们能够使用类似的效果呢,比如一个非常常见的需求,我们需要在组件加载完成后,调用加载后端数据接口,并渲染到界面上。

理论基础

这里我们需要借助React本身给我们提供的useEffect这个hook来实现

我们可以在社区的很多地方看到会把Effect这个词,翻译成副作用,为了对齐认知,我们不妨沿用下这个定义。比如在dva的实现中,model层的Effect 也是一样的概念。这里不得不称赞下以云谦在首的团队在dvajs文档设计上的两点,就是The dva.js Knowledgemap,呈现给开发者或者初学者一个使用dva需要的最小知识集合,并且用详尽的文档和demo去展示,特别是对初学者非常友好。虽然随着时间的推移,该团队的重心放在了打造Umi生态,但是不妨碍我们从这个角度去学习下他们的专业性和对使用者友好的态度,说明他们在推广上做了很多的思考,而且dva本身的实现相对精炼,特别适合我们对它的设计和源码进行剖析。

那么在JavaScript的世界里面什么是副作用呢?

在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。

简单拿代码介绍下什么是纯函数

我们定义了一个加法的函数,传入两个number类型的参数,返回这两个参数相加得到的和。这个例子有确定的输入,就会产生确定的返回结果,比如1+2,一定等于3,不会有其他值产生,就是同样的输入一定或者同样的输出,那么什么情况下会使得这个函数不纯呢。

我们定义了个乘法,但是在两数相乘过程中,多乘了个随机数,那么由于每次执行的时候,随机数都会变化,因此确定的输入,没有确定的输出,所以这个函数就不是纯函数了。类似的如果我们在函数中,调用了接口,获取了外部数据等等行为,都会是一样的结果。因此useEffect就是给我们用来执行一些副作用的方法。

React 官网对useEffect的定义:

useEffect(didUpdate);

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候才执行。

我们拿一个实际的demo去理解下,useEffect如何使用。为了方便起见,我们还是拿umi的脚手架去生成个项目,具体步骤可以参考笔者前面的文章,这里篇幅原因不再赘述,链接笔者会放在最下面。

我们看下第一步的代码

import React, {useEffect} from 'react';

export default function IndexPage() {
  useEffect(() => {
    console.log('我来了!');
  });
  return (
    <div>
      欢迎来到喵爸的小作坊
      <button onClick={() => {
        console.log('点我干嘛');
      }}>点我下</button>
    </div>
  );
}

然后看下运行起来的效果

的确生效了,但是好像不太对劲,只要我们更新下count的值,useEffect里面的代码都会执行,这不是我们想要的效果,我们希望只在首次执行,后面不论state发不发生变化,都不会再执行。我们去看看官网的描述,

useEffect(
  () => {
  
  },
  [Dependency],
);

useEffect可以传入第二个参数,是一个数组,标志它的依赖项,只有依赖项的值发生变化才会执行里面的函数,那我们应该怎么利用呢?其实很简单,只要传一个空数组就行了,那我们修改下代码

执行下,看下对应的效果

nice!我们再点击按钮更新count值,不会再触发对应的函数了。那我们接下来如何去封装下呢,方便在其他地方使用呢?

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

于是乎,我们创建个useMount,具体代码如下

import { useEffect } from 'react';

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn?.();
  }, []);
};

export default useMount;

然后我们改造下原来的代码

import React, {useState} from 'react';
import useMount from '../hooks/useMount';

export default function IndexPage() {
  const [count, setCount] = useState(0);
  useMount(() => {
    console.log('我来了!');
  });
  return (
    <div>
      <div>欢迎来到喵爸的小作坊</div>
      <div>{count}</div>
      <button onClick={() => {
        setCount(count => count + 1);
      }}>点我下
      </button>
    </div>
  );
}

我们来看下效果

跟我们预想的是一样的。于是乎我们变简单的实现了useMount的自定义hook,以后再使用过程中,就不需要重复定义了,就是封装的魅力。当然笔者提醒下,我们的实现相对比较简单,实际如果封装的话,最好加入一些判断,useMount只有一个参数,就是传入一个function,那从封装完备性角度,我们应该判断下类型,如果不是function,应该给使用者相应的提示。

好了,今天的分享到就到这里,大家也可以思考下,我们如何再封装一个useUnmount

参考资料

  1. useEffect zh-hans.reactjs.org/docs/hooks-…
  2. 自定义hook zh-hans.reactjs.org/docs/hooks-…

大家喜欢的话可以点个关注哦,或者关注下公众号:喵爸的小作坊