Hooks 之 useState

485 阅读5分钟

正在学习 React 的 Hooks,打算根据学习笔记,写下总结,这样印象和理解回加深一下吧!

对于 React 的学习还处于初级阶段,会结合大佬的博客以及官网自己整理,我能做好的就是尽可能的去提出疑问,然后寻找答案,写的博客也算是一个整合吧(笑)。首先可以明确的是:Hook 是一个特殊的函数,它可以让你“钩入” React 的特性

开始学习得时候,资料里面将组件分为状态组件(类组件)和无状态组件(函数组件),因为类式组件拥有状态,生命周期等,可以处理相关逻辑,实现需求。而函数组件通常在主要负责渲染的时候,性能会比较高,这点在 react-redux 的设计中体现得尤为明显:类组件充当容器,和redux连接,负责逻辑交互,而函数组件充当UI组件,通过 props 与容器组件联系,主要负责渲染。

一、基本使用

但是现在,函数也能有自己的组件了,函数组件不能实例化,没有this,那要怎样拥有自己的状态 state 呢?答案就是 useState,React 官网给出的示例代码如下:

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

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

useState 的参数就是状态的初始值,在左边使用的是数组结构赋值count 是状态变量,setCount 是更新状态的方法。刚开始的时候还是需要从习惯类组件的思维转换过来。count 可以比作类组件 state 对象,我们还可以多次调用 useState,下一次的状态就可以取其他名字,可以是基本数据类型,也可以是引用类型。这样一看是不是就和state很像了呢!当然有状态,就要有改变状态的方法,这里每个状态都有自己的改变状态的方法,并且类组件里面的 setState 是合并,这里是替换

如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用,这是惰性初始 state

二、一些疑惑

上边的小 demo 实现记录点击按钮的次数,但是细心的朋友会发现好些个问题:首先,一般函数运行完成后,变量会销毁的。

1.如何记录状态

Hook 使用了 JavaScript 的闭包机制,useState为什么能记住上一次执行的状态,应该就是全局变量+闭包useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props),这也是闭包的作用。

let _val // 将我们的状态保持在模块作用域中
const MyReact = (function() {
    return {
      render(Component) {
        const Comp = Component()
        Comp.render()
        return Comp
      },
      useState(initialValue) {
        _val = _val || initialValue // 每次运行都重新赋值
        function setState(newVal) {
          _val = newVal
        }
        return [_val, setState]
      }
    }
  })()

2.useState 是否每次渲染都执行

从上面这段模拟 useState 原理的代码可以看出,useState 完全可以每次都执行,使用了或运算符 ||

3.如何实现多个状态

就是利用数组,初次渲染,数组为空,使用 useState 的参数,后面的渲染,数组值不为空,使用数组值

let _state = [];// 利用全局数组来保存状态,就能解释为什么可以记录上一次的状态。
let index = 0;//各个状态值的数组下标

function myUseState(initialValue) {
  const currentIndex = index;
  index += 1;
     //判断是否是第一次的步骤,是第一次的话,_state[currentIndex]为null,将 initialValue赋给它,不是第一次就保持原状。这里就解决了每一次useState都会执行,但不是默认值的问题
  _state[currentIndex] = _state[currentIndex] || initialValue;
  const setState = newState => {
    _state[currentIndex] = newState;//更新状态,注意细节了是替换,而项类组件里边setState是合并
    render();// 触发页面重新渲染
  };
  return [_state[currentIndex], setState];
}

三、给 setSatate 传函数

setState 有两种传参的方法,一种就是传递 count+1,也就是将count 替换成 count+1,第二种就是传递一个函数,该函数将接收先前的 state,并返回一个更新后的值 。需要用到其他状态,使用函数会更加方便。

但是,还有一个更加重要的特性,需要注意。我们知道 useEffect 的第二个参数,能决定 effect 依赖哪些数据,根据这些数据的变化决定在一次重新渲染中是否执行。

我们实现一个数字自增通常会这样写

  useEffect(() => {
    console.log('执行effect')//自增到几,这句话就会打印多少次
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);

初次渲染,setCount 改变 count,同时 count 改变会引发页面的重新渲染,而 effect 对比一下,发现依赖 count 改变了,于是执行,又会执行 setCount 一直这样下去,实现了定时自增。

同时,我们还可以移除依赖:

  useEffect(() => {
    console.log('执行effect')//就打印一次,因为第二个参数传的是空数组,但是自增的功能不会有影响
    const id = setInterval(() => {
      setCount(c => c+1)
    }, 1000);
    return () => clearInterval(id);
  },[]);

尽管effect只运行了一次,第一次渲染中的定时器回调函数可以完美地在每次触发的时候给React发送c => c + 1更新指令。它不再需要知道当前的count值。因为React已经知道了。

React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 setState

四、Hook 的使用规则

这个官网上边讲的比较清楚了:

  1. 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook, React 靠的是 Hook 调用的顺序来区分哪个 state 对应哪个 useState
  2. 只在 React 函数(函数组件和其他hook)中调用 Hook,不要在普通的 JavaScript 函数中调用 Hook,这也是为了保证调用顺序,避免出错

其实就是因为,使用了数组来保存状态,数组值都是靠渲染的顺序决定,上边两条规定都是为了保证每次渲染的时候 hook 函数的调用顺序是一样的。

参考博客

30分钟精通React Hooks

useState 原理

深入理解:React hooks是如何工作的?