浅入深出地了解useState
在我不算很长的React函数式编程中来看,从上手到现在也使用了不下千次的useState,可以说useState是我的代码的重要的构成部分,可能不了解本质也能进行日常的业务开发,但是每次遇到难以解决的Bug时我日常怀疑人生,因此我迫不及待的想要了解useState到底做了什么。
useState诞生背景
什么是Hooks
- React一直倡导使用函数组件,但是有时候需要使用state或其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有
 - React hooks 允许你不编写class的情况下使用state以及其他的React特性
 
Hooks解决的问题
- 类组件的不足
 
- 状态逻辑难复用,在组件之后复用状态逻辑很难,可能要用到render props或者 HOC,但无论是渲染属性还是高阶组件,都会在原先的组件外包裹一层父容器,导致层级冗余
 - 趋向复杂难以维护,在声明周期函数中混杂不相干的逻辑
 - this指向问题
 
- Hooks的优势
 
- 能优化类组件的三大问题
 - 副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而 useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。
 
useState的实践
什么时候刷新
与 class 组件中的 setState 方法不同,如果你修改状态的时候,传的状态值没有变化,则不重新渲染
优化
- 默认情况下,只要父组件的状态变了(不管子组件以来不依赖该状态,子组件也会重新渲染)
 - 一般的优化,使用useMemo
 - 更深入的优化,使用useCallBack
 
返回值
了解useState我们从最简单的返回值开始入手
const [data, setData] = React.useState<number>(0)
可以看到我们声明了一个数组去结构useState的返回。他的第一个返回是这个state,也就是在组件中中存储的数据,第二个返回就是更新这个数据的方法,只用通过这个方法去更新数据,才会触发组件的刷新。
那么为什么返回的是一个数组呢,可以试想如果是对象的话,那么我们就要使用重命名去解构多个useState了
const {data:numberData, setData:setNumberData} = React.useState<number>(0)
const {data:stringData, setData:setStringData} = React.useState<number>(0)
显示还是直接使用数组更方便,这是React团队在设计上的考量。
同步还是异步
在使用的时候,遇到的最常见疑惑就是useState究竟是同步的还是异步的,这直接决定了我们是否可以这个业务场景下使用它来存储数据,或者它是否能正确地重新渲染组件呈现出正确的数据。
答案是异步的。
const app = () => {
  const [data, setData] = React.useState<number>(0);
  return (
    <div onClick={() => {
      setData(data + 1);
      console.log(data)、  
      setData(data + 2);
      console.log(data)
    }} />
  )
}
// 0
// 0
同样地在onClick以外的任务下
const app = () => {
  const [data, setData] = React.useState<number>(0);
  return (
    <div onClick={() => {
      setTimeout(() => {
        setData(data + 1);
        console.log(data)
        setData(data + 2);
        console.log(data)
      }, 300)
    }} />
  )
}
// 0
// 0
可以很清楚地知道,两次打印均是0,如果在同步的情况下,那么两次的force打印出来的情况将会不一致,事实证明useState在不论什么情况下均是异步
合并
众所周知,每一次使用useState下的第二个返回值,也就是下面的setXXX,那么React的函数式组件就会重新刷新一次。
不过在一个点击事件中,如果有N个setState事件呢?显然React不会重新渲染N次吧,显然这样消耗性能的操作是违法直觉的,那么他将会在什么时候合并多个useState的值呢,这里可以同样用上面的代码举个例子。
const app = () => {
  const [data, setData] = React.useState<number>(0);
  const [string, setString] = React.useState<string>('');
  
  console.log(force, string)
  return (
    <div onClick={() => {
      setData(data + 1);
      setData(data + 2);
      setString(string + 'string')
    }} />
  )
}
// 2 'string'
可以看到上述例子中,app仅渲染一次,两个同样的setState函数仅仅只有第二个生效。
但是在setTimout或者promise等回调函数中,显然并不存在合并现象了,在一次onClick事件中刷新组件状态,可以看到多次打印出刷新值。
const app = () => {
  const [data, setData] = React.useState<number>(0);
  const [string, setString] = React.useState<string>('');
  
  return (
    <div onClick={() => {
      setTimeout(() => {
        setData(data + 1);
        setData(data + 2);
        setString(string + 'string')
      }, 300)
    }} />
  )
}
// 1 ''
// 2 ''
// 2 'string'
这个现象主要是由于React团队的操作,它们在React的jsx中对所有的onClick或者onChange事件进行了一个接管的操作,使其所有state在这些事件内有合并现象,而在其他回调中可以说是脱离了接管,因此没有任何合并的现象
时序一致
在官网的介绍中,时序一致是被提到的。何谓时序一致,就是每次渲染的时候调用useState时的顺序应该是一样的,最常见的情况就是不能在条件判断语句后加入useState
const app = () => {
  // 常规错误1.在if语句中包裹useState
  if(1 + 1 === 2) {
    const [data, setData] = React.useState<number>(0);
  }
  // 常规错误1.在useState前返回函数式组件
  if(2 + 2 === 4) {
    return 
  }
  const [string, setString] = React.useState<number>(0);
  return (
    <div onClick={() => {
      setData(data + 1);
    }} />
  )
}
为什么每次渲染函数都必须要按照同样的顺序渲染出来useState呢?理解这个问题需要从最简单的useState函数入手。
let x
function useState(initialValue) {
   x = x === undefined ? initialValue : x
  function setX(newState) {
    x = newState;
    render(); // 去渲染新组件
  }
  return [x, setX];
}
这样的useState可以简单地理解为可以做到赋值,刷新的作用。但是当在组件中调用多个useState这个方法的时候,显然一个x不够用了!
这个时候React团队采用的做法是根据每个useState在组件中创建的顺序来进行一个标记,将x值给按照相同的顺序做一个环形链表,在取出的时候按照相同的顺序拿到正确的值。
源码解析
目前 React 构架可以分为三层
import React from 'react'
import ReactDom from 'react-dom'
let firstWorkInProgressHook = {
  memoizedState: null,
  next: null
};
let workInProgressHook;
function useState(initState) {
  let currentHook = workInProgressHook.next ? workInProgressHook.next : {
    memoizedState: null,
    next: null
  };
  function setState(newState) {
    currentHook.memoizedState = newState;
    render()
  }
  // 这就是为什么 useState 书写顺序很重要的原因,因为是用链表来存储的
  if (workInProgressHook.next) {
    workInProgressHook.next
  } else {
    // 只有在初始化的时候才会进入
    workInProgressHook.next = currentHook;
    // 将 workInProgressHook 指向 {}
    workInProgressHook = currentHook;
  }
  return [currentHook.memoizedState, setState]
}
function render() {
  // 每次重新渲染的时候,都将 workInProgressHook 指向 firstWorkInProgressHook
  workInProgressHook = firstWorkInProgressHook;
  ReactDOM.render( < Counter / > , document.getElementById('root'));
}
- Scheduler(调度器) ———— 调度任务的优先级
 - Reconciler(协调器)———— 负责找出变化的组件
 - Renderer(渲染器)———— 负责将变化的组件渲染到页面上
 
我们这里模仿useState写出以下代码
function useState(initialState) {
  let hook
  // 第一次挂在函数式组件
  if(isMount) {
    // ...mount 时需要生成 hook 对象
    hook = {
      queue: {
        pending: null
      },
      memoizedState: initialState,
      next: null
    }
    
    // 将hook插入 fiber.memoizedState 链表末尾
    if(!fiber.memoizedState) {
      fiber.memoizedState = hook
    }  else {
      workInProgressHook.next = hook
    }
    // 移动 workInProgressHook 指针
    workInProgressHook = hook
  } else {
    // ...update时从workInProgressHook 中取出该useState 对应的hook
    hook = workInProgressHook;
    // 移动 workInProgressHook 指针
    workInProgressHook = workInProgressHook.next
  }
  let baseState = hook.memoizedState;
  if(hook.queue.pending) {
    // 根据 queue.pending 中保存的 update 更新 state
    let firstUpdate = hook.queue.pending.next;
    do {
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next
      // 最后一个update执行完后跳出循环
    } while (firstUpdate !== hook.queue.pending.next) {
      hook.queue.pending = null
    }
  }
  hook.memoizedState = baseState
  return [baseState, dispatchAction.bind(null, hook.queue)]
}