Skip to content

React Hooks 详解 ​

学习一项知识,必须问自己三个重要问题:1. 它的本质是什么。2. 它的第一原则是什么。3. 它的知识结构是怎样的

大纲 ​

测试一下 Hook 的熟练程度

是什么

  • 为什么要有 Hook

    • 状态逻辑复用
  • 发展史

    • Mixins
    • HOC
  • Hook 的设计目标

    • 类组件的缺点
    • Hooks带来的好处

各个 Hooks

附录:使用规则

测试一下 Hooks 的熟练程度 ​

为什么不能在 for 循环、if 语句里使用 Hook

React.memo、React.useCallback、React.usememo 的作用

useState 中的值是个对象,改变对象中的值,组件会渲染吗?如果用 React.memo() 包裹住呢

能否口喷 Hook 的原理是什么

Hooks 的本质是什么?为什么?

React Hook,它带来了哪些便利?

说下 React Hook 实现原理

React Hook 当中的 useEffect 是如何区分生命周期钩子的

useEffect(fn, []) 和 componentDidMount 有什么差异


回答的如何?在了解一个概念前,疑惑越多,认识就越深

是什么 ​

React Hook 是 React 16.8 推出的新特性。它可以让你再不编写 class 的情况下使用 state 以及其他的 React 特性

为什么要有 Hook ​

我们一定要有个概念,即 React 的本质是什么?它的特征是 UI=f(data)、一切皆组件、声明式编程。那好,它既然是 UI=f(data),data(数据)通过 function 来驱动 UI 视图变化,之前了解 setState 时我们就说了,setState 是来管理状态的,因为在一个视图,你不能简单只展示,也要交互,交互就有状态的改变,React 是通过 setState 来改变状态。但 setState 是类组件中的 API,而每一个类组件写起来很麻烦,需要加各种生命周期,写一个组件为了是麻烦的,再加上各种优化会更麻烦。在函数式组件中,是没有状态的,一般当做渲染(无状态组件)

官网也曾经说过设计 Hooks 的动机有三:

  1. 在组件之间复用状态逻辑很难
  2. 复杂组件变得难以理解
  3. 难以理解的 class

我们先了解下状态逻辑复用的发展史

发展史 ​

Mixins 时代 ​

在我还没用 React 之前就有了,现在已经被淘汰。

Mixins(混入)是一种通过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另一个对象上,不过你可以拷贝任意多个对象的任意个方法到一个新对象上去,这是继承所不能实现的。它的出现主要就是为了解决代码复用问题

这里不对其做分析,React 官方文档在 Mixins Considered Harmful 一文中提到了 Mixins 带来的危害:

  • Mixins 可能会相互依赖,相互耦合,不利于代码维护
  • 不同的 Mixins 中的方法可能会相互冲突
  • Mixins 非常多时,组件时可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球的复杂性

HOC(高阶组件) ​

HOC 的原理其实很简单,它就是一个函数,且它接受一个组件作为参数,并返回一个新的组件,把复用的地方放在高阶组件中,你在使用的时候,只需要做不同用处

打个比方:就好像我给你一瓶水,你在渴的时候就会喝它;你在耍帅的时候拿它摆 POSE;你在别人需要的时候给他喝帮助人...

各位可以前往这里查看 demo

HOC 的用处不单单是代码复用,还可以做权限控制、打印日志等。但它有也缺陷,例如 HOC 是在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这会让调试变得非常困难;而且 HOC 可以劫持 props,在不遵守约定的情况下可能造成冲突

总结下 HOC:

  1. 创建一个函数,该函数接收一个组件作为输入,除了组件还可以传递其他的参数
  2. 基于该组件返回一个不同的组件
  3. 它是将通用的逻辑进行处理,但会因为嵌套,使得调试难度变高

Hook 的设计目标 ​

我们了解了 React 状态逻辑复用的发展史,结合类组件带来的不便。我们希望 React Hook 能这样的:

  1. 无 Class 的复杂性
  2. 无生命周期的困扰
  3. 优雅地复用
  4. 对其 Class 组件已经具备的能力

类组件的缺点 ​

  • 代码量多

  • this 指向:需要考虑 this 的指向问题

  • 组件难以维护

  • 状态逻辑难复用

    • 在组件之间复用状态逻辑很难,可能要用到 render props(渲染属性)或者 HOC(高阶组件),但无论时渲染属性还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余

Hooks 带来的好处 ​

  • 逻辑复用

  • 业务代码更聚合

  • 写法简洁

    • 这个不用多说,写个 class 组件,需要写各种生命周期,如果优化,还需要在 shouldComponentUpdate 做渲染判断,但是函数式组件的 useMemo、memo 比 shouldComponentUpdate 不知道简洁到那里去

各种 Hooks ​

useState ​

作用:让函数组件具有维持状态的能力

函数组件本身就是一个函数,不是类,因此没有构造函数 constructor(props)

任何你想定义的变量都可以单独拆分出去,独立定义,互不影响

在使用 useState 时,就会蹦出一个常见的面试题,函数式组件与类组件有何不同 ,函数式组件能捕获渲染时的值

注意事项 ​

1.不可局部更新

  • 如果 state 是一个对象,能否部分更新,不能
  • setState 不会帮我们合并属性,可使用 展开运算符(...)来解决
  • useReducer 也不会合并属性

2.地址要变

  • setState(obj) 如果 obj 地址不变,那么 React 就认为数据没有变化

3.useState 和 setState 都接受函数

  • setState(i => i + 1)

useEffect ​

作用:执行副作用

PS:什么是副作用?对环境的改变就是副作用

每一次渲染都有它自己的 props 和 state

用途:

  • 作为 componentDidMount 使用,[] 作为第二个参数
  • 作为 componentDidUpdate 使用,可指定依赖
  • 作为 componentWillUnmount 使用,通过 return

如果同时存在多个 useEffect,会按照出现次序执行

在我们讨论 effects 之前,我们需要先讨论一下渲染(rendering)

我们来看一个计数器组件 Counter

jsx
function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>Click me</button>
        </div>
    );
}

我们的组件第一次渲染的时候,从useState()拿到count的初始值0。当我们调用setCount(1),React 会再次渲染组件,这一次count是1。如此等等:

jsx
// During first render
function Counter() {
    const count = 0; // Returned by useState()
    // ...
    <p>You clicked {count} times</p>;
    // ...
}

// After a click, our function is called again
function Counter() {
    const count = 1; // Returned by useState()
    // ...
    <p>You clicked {count} times</p>;
    // ...
}

// After another click, our function is called again
function Counter() {
    const count = 2; // Returned by useState()
    // ...
    <p>You clicked {count} times</p>;
    // ...
}

当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量

每一次渲染都有它自己的事件处理函数 ​

到目前为止一切都还好。那么事件处理函数呢?

看下面的这个例子。它在三秒后会 alert 点击次数count:

jsx
function Counter() {
    const [count, setCount] = useState(0);

    function handleAlertClick() {
        setTimeout(() => {
            alert('You clicked on: ' + count);
        }, 3000);
    }

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>Click me</button>
            <button onClick={handleAlertClick}>Show alert</button>
        </div>
    );
}

如果我按照下面的步骤去操作:

  • 点击增加 counter 到 3
  • 点击一下“Show alert”
  • 点击增加 counter 到 5 并且在定时器回调触发前完成

渲染的是 3,捕获当前的值

它究竟是如何工作的呢?

我们发现 count 在每一次函数调用中都是一个常量值。值得强调的是——我们的组件函数每次渲染都会被调用,但是每一次调用中 count 值都是常量,并且它被赋予了当前渲染中的状态值

这就解释了我们的事件处理函数如何捕获点击时候的 count 值。如果我们应用相同的替换原理,每一次渲染“看到”的是它自己的 count

jsx
// During first render
function Counter() {
    const count = 0; // Returned by useState()
    // ...
    function handleAlertClick() {
        setTimeout(() => {
            alert('You clicked on: ' + count);
        }, 3000);
    }
    // ...
}

// After a click, our function is called again
function Counter() {
    const count = 1; // Returned by useState()
    // ...
    function handleAlertClick() {
        setTimeout(() => {
            alert('You clicked on: ' + count);
        }, 3000);
    }
    // ...
}

// After another click, our function is called again
function Counter() {
    const count = 2; // Returned by useState()
    // ...
    function handleAlertClick() {
        setTimeout(() => {
            alert('You clicked on: ' + count);
        }, 3000);
    }
    // ...
}

所以实际上,每次渲染都有一个“新版本”的 handleAlertClick。每一个版本的 handleAlertClick “记住”了它自己的 count

useLayoutEffect ​

名曰:布局副作用

它与 useEffect 常做对比,两者执行时机不同、性能影响不同、使用场景也不同

  • useEffect 会异步执行
    • 在 commit 阶段的 before mutation 阶段调用,但是会在 layout 阶段完成后才异步执行
    • 所以它不会阻塞浏览器的绘制
    • 在浏览器渲染完成后执行
  • useLayoutEffect 是同步执行
    • 在 commit 阶段的 layout 阶段同步执行
    • 等价于类组件中的 componentDidMount
    • 可以读取并同步修改 DOM,确保 DOM 的变化在用户看到之前完成
    • 所以它会阻塞浏览器的绘制
    • 在浏览器渲染前执行

示意图 ​

javascript
APP() --> React.element --> VDOM --> DOM --> 修改视图

useLayoutEffect 发生在 DOM 和 修改视图 阶段之间

useEffect 发生在修改 DOM 之后

总结 ​

简单来说,useLayoutEffect 总是比 useEffect 先执行

为了用户体验,优先使用useEffect(优先渲染)

useReducer ​

用来践行 Flux/Redux 的思想

看代码,公分4步走

一:创建初始值 initialState

二、创建所有操作 reducer(state, action)

三、传给 useReducer,得到读和写 API

四、调用写({ type: "操作类型"})

总的来说 useReducer 是 useState 的复杂版

useContext ​

上下文

全局变量是全局的上下文

上下文是局部的全局变量

使用方法

一、使用 const context = createContext(null) 创建上下文

二、使用 <Contet.Provider value=> 圈定作用域

三、在作用域内使用 useContext 消费上下文

附录:使用规则 ​

Hooks 的本质就是 JavaScript 函数,在使用它时需要遵守两条规则

只在最顶层使用 Hook ​

不要在循环,条件或嵌套函数中调用 Hook,确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确

只在 React 函数中调用 Hook ​

不要再普通的 JavaScript 函数中调用 Hook,你可以:

  • 在 React 的函数组件中调用 Hook
  • 在自定义 Hook 中调用其他 Hook

参考资料 ​