React16.8+版本的发布,hooks函数是新增的一大功能,其实useState函数是让函数组件有了状态,那么我们就来详细了解一下useState这个函数。

用法:

const [state, setState] = useState(initialState);

因为react是函数式编程,所以上面的代码就很好理解了,useState函数接收一个初始化参数initialState,其返回值用数组解构俩个参数,state是初始化的state,以及更新state的函数setState. 在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

setState(newState);

在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。

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

 函数式更新

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

“+” 和 “-” 按钮采用函数式形式,因为被更新的 state 需要基于之前的 state。但是“重置”按钮则采用普通形式,因为它总是把 count 设置回初始值。

如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。

注意

与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

 useReducer 是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。

惰性初始 state

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

跳过 state 更新 

调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

以上部分是官方文档给出的内容。。。

下面,我们来解析useState的一些相关问题:

1、当组件中出现多个状态的时候该怎么办?

组件中出现多个状态这是太正常不过了。那也就是意味着useState函数会被调用多次.

import React, { useState } from 'react'
function Example () {
    const [name, setName] = useState('cat')
    const [age, setAge] = useState(0)
    const [gender, setGender] = useState('male') 
}

useState()函数接收的参数并没有规定接收什么类型的参数,它只是被作为初始值。需要注意的是,我们之前调用setState方发,是将现在的数据与之前的state数据进行合并然后返回一个新状态,而hooks调用setState方法是将之前的state数据进行替换,返回一个新状态,当然hooks也提供了useReducer()函数,以redux的方式来管理state.

在开发中我们可以发现,无论useState()函数被调用了多少次,他们之前都是互相不影响,都是相互独立的。这一点很重要。那么这是如何做到的呢?

react会根据useState的顺序来规定的。在react实现的内部声明了一个数组,按照其useState()函数调用的顺序,来保存其数据,所以,一定要在函数的外层调用useState(),不要在if...else,for循环,子函数中调用useState(),所以,因为react函数共享一个保存数据的数组,那么也会对其他的函数组件有影响。Capture Value” 特性的产生是在于每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

来实现一个简单的useState函数:

let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}

下面来看看react是如何实现的:

(1)React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。

type Hooks = {
  memoizedState: any, // 指向当前渲染节点 Fiber
  baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
  baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
  queue: UpdateQueue<any> | null,// UpdateQueue 通过
  next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
}

memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?

我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

useState完结。。。