历时整整一个月,我终于从家里回到公司。我上一篇博客当时在我准备回家的时候写的,可以看到真是放了一个月的假,虽然我也不算是工作狂的类型,但是一直在家闷着也怪无聊的,倒不如来上班找点事做。在家期间,我又刷了一些课程, 这段时间如果不忙的话,就争取把这些之前做的笔记整理一下吧。

这次博客的主题是 Hooks。说实话,在来公司之前,我没有用过 Hooks,平时工作上使用 Hooks 也是在工作中现学现用,所以我确实没有底气说自己就掌握了 Hooks。而是到这时自己实在闲得没办法的时候才想起要学一下,也是相当惭愧了。这次博客就大致把笔记整理一遍,希望能起到复盘的效果。

钩子

Hooks 的中文意思是「钩子」。在以前学 Vue 的时候,都学过一个词,叫「生命周期钩子」,虽然感觉很奇怪,但是想必这个「钩子」和 Hooks 的「钩子」是类似的。在以前,React 的函数组件没有生命周期和状态(state)。在 React 16.8,Hooks 被加入了,可以借此让函数组件实现之前只有类组件能够做到的一些事情。这篇博客主要讲讲 Hooks 中 useState 的相关内容。

useState(使用状态)的实现原理

说起 Hooks,大家用得最多的也就是 useState 了。直接来看一下一个简单的 useState 实现吧:

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

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

对于函数组件Example来说,count就相当于是 state,setCount就相当于 setState。页面首次渲染Example组件,会调用Example函数,得到虚拟的 div,并由此创建真是的 div。用户点击 button,调用setCount,此时会触发再次渲染Example组件,并根据 DOM diff 更新 div。需要注意的是,每次调用Example函数,都重新运行useState(0),所以setCount并不会改变count,因为函数重新调用了,生成了新的countExample组件每次重新渲染,useState(0)得到的count值都是不相同的。

那么,useState 是怎么实现每次重新调用都生成新的count的效果呢?如果自己试着实现一下 useState,应该怎么写?下面就是一种简单的实现:

let _state;
const myState = initialValue => {
  _state = _state === undefined ? initialValue : _state;
  const setState = newValue => {
    _state = newValue;
    render();
  }
  return [_state, setState];
}
const render = () => {
  ReactDOM.render(<App />, rootElement);
};

在这个例子中,myState接受一个初始值initialValue,并且需要借助闭包的形式存储 state 的值。如果当前的 state 没有设定任何值,就将 state 赋值为initialValue。而后在myState中声明了一个用于改变状态的函数setState,将state的值改变为新的值,并重新触发rendermyState返回一个数组,第一个值用于读取状态,第二个值用于修改状态。

这个例子已经可以简单地实现 useState 的状态存储和组件更新的效果,只不过因为过于简单,存在的问题也不少。在平时的工作中,常常会有在一个函数组件中使用两个 useState 的情况,但是这个例子是办不到的,因为所有的数据都放在_state里,第二次调用myState会把之前的值给覆盖掉。这时候就需要对例子进行改造,让其可以变得可以在一个函数组件中多次使用。

首先,_state不能是普通对象,虽然状态可以分别存放在各个属性值里,但是setState没法知道状态在对象中对应的属性名。所以_state应该使用数组的形式,根据调用myState的顺序将值随访在数组的对应位置,并借助闭包的形式存放已经调用myState的次数。每次组件重新渲染后,重置_state。例子代码如下所示:

let _state = [];
let _index = 0;
const myState = initialValue => {
  let index = _index;
  _state[index] = _state[index] === undefined ? initialValue : _state[index];
  const setState = newValue => {
    _state[index] = newValue;
    render();
  }
  _index++;
  return [_state[index], setState];
}
const render = () => {
  _index = 0;
  ReactDOM.render(<App />, rootElement);
};

不过这种方法的缺点也很明显,如果多次调用 render,每次调用myState的顺序就必须一致,不然就会出现读写混乱的情况。这就是为什么,React Hooks 中的 useState 不能放在 if - else 语句里面调用的原因,因为这样会打乱数据存放的位置。

当然,现在的例子中的代码仍然存在着问题。这里的_state_index都是以一种类似于全局变量的方式存放的,把变量放在全局作用域可能会重名,如果一个组件的myState把这两个变量占用了,那么其他组件用什么呢?事实上,每个函数组件都会将 useState 需要的_state_index放在组件对应的虚拟 DOM 节点上,保证了多个组件之间的状态互不干扰,相对独立。关于 Hooks 中的 useState 的实现思想,大致可以总结如下:

  • 每个函数组件对应一个 React 节点;
  • 每个节点保存着 state 和 index;
  • useState 操作的是 state[index];
  • index 由 useState 在函数组件中的调用顺序决定;
  • setState 会修改 state,并触发更新。

使用 useState 需要注意的地方

1、state 不支持局部更新

在类组件中,state 是以对象的形式存在的,useState 中的 state 当然也可以是对象。比如:

const [myState, setMyState] = React.useState({
  name: 'guangyu',
  age: 18
})

但是和类组件中的 setState 不同,setState会「帮忙」合并旧属性,而这里的myState不支持「局部更新」。如果setMyState传入的新对象不包含旧值的部分属性,mystate上的其他属性就会丢失。比如:

setMyState({ age: 19 }) // 其他属性丢失
setMyState({ ...myState, age: 19 }) // 新的 state 合并了旧属性

2、state 的对象需要改变地址才能触发更新

以上面的例子为例,如果改为这样写,是不会触发组件渲染的,因为对象的地址没有改变, React 认为数据没有变化:

myState.age = 19;
setMyState(myState) // 不会触发更新

3、useState 其实可以接受函数

useState 接受的参数可以是 state 的初始值,也可以是一个函数,此时这个函数的返回值会被作为 state 的初始值。比如:

const [myState, setMyState] = React.useState(() => ({
  name: 'guangyu',
  age: 18
}))

这样做的好处是,可以减少函数的计算过程,不会每次渲染都执行一遍初始值的获取过程。如果初始值是一个比较复杂的对象,这样做就可以只让函数组件在第一次渲染的时候才执行这个函数获取初始值。

4、setState 也可以接受函数

在前文提到过,在 useState 中,setState 不能改变 state,每次重新渲染都会产生新的 state 和 setState。如果像这样连续调用多次 setState,state 的新值会以最后一次 setState 的值为准。比如:

const [n, setN] = React.useState(0)
setN(n + 1)
setN(n + 1) // 重新渲染得到的 n 的值是 1

但是如果让 setState 接受函数的话,情况就变得不同了。setState还可以接受函数为参数,这个函数接受 state 为参数,返回 state 的新值。比如:

const [n, setN] = React.useState(0)
setN(i => i + 1)
setN(i => i + 1) // 重新渲染得到的 n 的值是 2

这种方式 setState 的方式看起来更为直观,需要对 state 多次操作的话,可以使用这种方式。如果可以接受这种形式的 setState,应该优先使用。

以上就是本篇博客中有与 useState 相关的所有内容。本来今天是打算把 Hooks 大概过一遍的,没想到 useState 就够我划水划一篇了,感觉还挺不错。正好前一段时间看的课程又要忘得差不多了,打了这么多字倒是加深了不少影响。最近回到公司可能接下来的时候会更忙了,也不知道我下篇博客得等到啥时候,先这样吧。

晚上的路上