今天是星期六,因为之前休了十几天的假。所以就想着来公司补班了,星期六在公司上班也正是非常难得了。但是因为这两天把手头上的事情做得差不多了,所以今天在公司所以也没什么事干,就想着干脆把博客再续上一篇吧。

今天继续讲 React Hooks,除去之前的两篇 Hooks 博客,估计剩下的内容还可以整个一篇吧,虽然这样显得有点划水,但是这也是为每篇博客的篇幅考虑(并不)。不过写博客的作用在这两天还是显示出来了,前一段时间看的课程实际上又忘得差不多了,如果没有有意识地去记笔记或者去写博客的话,可能就忘掉了,就像我今天所要介绍的这些内容一样。

划船

useEffect - 使用「副作用」

从字面意思上来看,useEffect 就是使用「影响」,这里的「影响」也可称之为「副作用」。与平时生活中说提到的「副作用」不同,这里的「副作用」是一个中型的词语,表示的并不是有「负面效果」的影响。在这里,对环境的改变即为副作用,比如修改document.title,但是不一定要把所有「副作用」都放在 useEffect 里。

React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此称实际上之为 "afterRender" 更为恰当。在类组件中,与 "afterRender" 对应的生命周期有componentDidMountcomponentDidUpdatecomponentWillUnmount。函数组件中没有生命周期钩子,而借助 useEffect,就可以在函数组件中实现上面这三种生命周期的效果。下面是一个简单的例子,useEffect 接受一个函数作为参数,函数的内容会在每次组件更新时调用:

useEffect(() => {
    didUpdate();
});

这样 useEffect 就可以作为componentDidUpdate使用了。默认情况下,useEffect 将在每轮渲染结束后执行,但也可以让其在只有某些值改变的时候才执行。以下面这个例子为例,这里在 useEffect 添加了对窗口大小改变的监听,但实际上是并不需要每次渲染过后都重新定义监听事件,只在第一次渲染之后添加即可。如果给 useEffect 的第二个参数传递一个空数组[],这样就会类似于componentDidMount只会运行一次(仅在组件挂载和卸载时执行):

useEffect(() => {
    window.onresize = () => console.log('Hello world!');
}, []);

useEffect 的第二个参数是 useEffect 所依赖的值数组,useEffect 只会在数组中的变量产生变化的时候才会执行。如果需要用到这种渲染方式,就要确保数组中包含了所有外部作用域中会发生变化且在 useEffect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。依赖项数组不会作为参数传给 useEffect 函数。虽然从概念上来说它表现为:所有 useEffect 函数中引用的值都应该出现在依赖项数组中。

组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。在类组件中通常会使用componentWillUnmount来完成这些操作。以刚才的window.onresize为例,在组件销毁之后,显然是需要对其进行清理的。要实现这一点,useEffect 函数需返回一个清除函数。为防止内存泄漏,清除函数会在组件卸载前执行:

useEffect(() => {
    window.onresize = () => console.log('Hello world!');
    return () => {
        window.onresize = () => { };
    };
}, []);

还有一点需要注意的是,一个函数组件中可以调用多次 useEffect,多个 useEffect 中的内容会按照调用顺序执行。

useLayoutEffect - 布局副作用

如果要模拟componentWillMountcomponentWillUpdate这些在组件渲染前触发的生命周期钩子,React 为此提供了另一个 Hook: useLayoutEffect。它和 useEffect 的结构相同,区别只是调用时机不同。useLayoutEffect 通常执行的是与布局相关的副作用,因为是在组件渲染前执行,可以使得一些变化对用户而言不那么突兀。

因为 useLayoutEffect 在浏览器渲染组件之前调用,所以 useLayoutEffect 总是比 useEffect 先执行,并不受调用顺序的影响。由于 useLayoutEffect 中的内容会占用组件渲染的时间,而且大部分时候并不需要直接改变 DOM,所以 useEffect 比 useLayoutEffect更为常用,也更被推荐使用。

useMemo - 使用「记忆化」

useMemo 中的 "Memo" 指的是 "memoized",也就是记忆化。要理解 useMemo,就要先理解 React.memo。在 React 16.6.0 中更新了 React.memo,使用 React.memo 可以封装不必要重复渲染的组件,如果该组件的 props 没有变化,就不会再次渲染,这样可以减少多余的 render。比如:

<Child2 data={m} />
const Child = React.memo(Child2);

但是 React.memo 也是存在缺点的,如果为组件传入对象的 props,由于每次渲染都会重新定义新的对象,造成每次传入的 props 都变化了,React.memo 没有起到作用。因此需要将这些对象 props 进行缓存,使得每次 render 都可以保持对象不发生变化。这时候就需要用到 useMemo 了:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo 接受一个计算函数和依赖项数组作为参数,计算函数被用于计算需要记忆的值,而且只有在某个依赖项改变时才重新计算这个值。这种优化有助于避免在每次渲染时都进行高开销的计算,在依赖项没有改变的情况下,就会使用这个变量的缓存值。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。如果提供的是空数组,那么这个值就不会被重新计算。

useMemo 可用于记忆函数或其他各种值。如果需要记忆的值是函数,那么就要将计算函数写成返回函数的函数,比如:

const memoizedValue = useMemo(() => () => console.log('Hello world!'), []);

传入 useMemo 的函数会在渲染期间执行,因此不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

useCallback - 记忆回调函数

与 useMemo 作用相同,useCallback 专门用于记忆函数。类似于 useMemo 的语法糖,下面两种写法在功能上是等价的:

const memoizedValue1 = useMemo(() => () => console.log('Hello world!'), []);
const memoizedValue2 = useCallback(() => console.log('Hello world!'), []);

useRef - 使用 ref(转发)

ref 是访问 DOM 节点的主要方式。当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。ref 的值根据节点的类型而有所不同:

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
  • 不能将 ref 属性用于函数组件上,因为函数组件并没有实例(instance)

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。下面是使用 useRef 的一个简单例子:

import React, { useRef } from 'react';
function Content() {
    // 创建ref
    const fileInputEl = useRef(null);

    // 在你的元素或者组件上面挂载ref
    <input ref={fileInputEl} type='file' />
    // 使用ref
    // 当点击这个div的时候触发input的点击事件
    <div onClick={() => fileInputEl.current.click()}>
        上传文件
    </div>
}

返回的 ref 对象在组件的整个生命周期内保持不变。与 useState、useReducer 中的 state 不同,useRef 是观察始终的,如果需要一个值,在组件 render 时保持不变,就可以使用 useRef。useRef 不仅可以用于类组件或 DOM 元素,还可以用于存放任意数据。ref 值(current)的改变不会触发 render,可以借助 useState 的特性,手动让组件在 ref 值变化时 render。

forwardRef(并不是 Hook)

上面提到,函数组件不能接受 ref 属性(props 无法传递 ref 属性),此时需要对函数组件进行封装:

const Button = (props, ref) => {
    return <button ref={ref}>{props.txt}</button>;
};
export default Button2 = React.forwarfRef(Button);

这样,父组件就可以通过 ref 操作子组件中的button。此时 Button 中的 ref 依然不是在 props 中的,而是在函数组件的第二个参数。如果函数组件希望接受到父组件以 props 形式传递来的 ref 属性,就必须使用 forwardRef 进行封装。forwardRef 可以再多层函数组件之间传递 ref。

useImperativeHandle - 使用「重要的」处理器

咋一看 useImperativeHandle 的名称感觉非常奇怪,完全看不出来这是用做什么的。事实上,useImperativeHandle 叫做 "setRef" 更为恰当。useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。例如:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);

这样FancyInput的父组件,在FancyInput的使用获取的 ref 值就是带有focus方法的一个对象。此处的inputRef是真实的 ref,自定义后暴露给父级组件的 ref 是封装之后的 ref。

自定义 Hook

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。虽然Hooks 不能在 if-else 语句中调用,但是并非不能在函数中调用。与组件中一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。例如:

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。事实上自定义 Hook 只是讲函数组件中使用 Hooks 的部分提取出来做一个新的函数,工作方式与直接在函数组件中使用其他 Hooks 没有区别。

自定义 Hook 必须以 “use” 开头。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。

在两个组件中使用相同的 Hook 不会共享 state。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。


以上就是本篇博客中关于 Hooks 的全部内容。虽然课程也是早就看过的了,在工作中使用 Hooks 也是很久的事情了,但是知道今天为止我才对 Hooks 有比较清楚的认识,真就博客推动学习,工作推不动了。非常惭愧。看来手里这些课程存货有空还是要及时消化的,不然在工作中可能就会因为对一些经常在用的知识不够了解而闹笑话了。

书架

贴一下 React 官方文档中关于 Hooks 的介绍,这篇博客同样是边看文档边学编写的,继续加油吧: