前言

react hooks 是 React 16.8 的新增特性。 它可以让我们在函数组件中使用 state 、生命周期以及其他 react特性,而不仅限于 class 组件。react hooks 的出现,标示着 react中不会在存在无状态组件了,只有类组件和函数组件。具体可查看官网。

优势:

  1. 函数组件不能使用state,遇到交互更改状态等复杂逻辑时不能更好地支持,hooks让函数组件更靠近class组件,拥抱函数式编程。
  2. 解决副作⽤问题,hooks出现可以处理数据获取、订阅、定时执行任务、手动修改 ReactDOM这些⾏为副作用,进行副作用逻辑。比如useEffect。
  3. 更好写出有状态的逻辑重用组件。
  4. 让复杂逻辑简单化,比如状态管理:useReducer、useContext。
  5. 函数式组件比class组件简洁,开发的体验更好,效率更⾼,性能更好。
  6. 更容易发现无用的状态和函数。

useState介绍

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

返回一个 state,以及更新 state 的函数setState。

  • state是要设置的状态 。
  • setState是更新state的方法。setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
  • initState是初始的state,可以是随意的数据类型,也可以是回调函数,但是函数必须是有返回值(惰性state)。

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

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

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState(newState);
这里的值不会立即变化

使用useState钩子提供的更新程序的状态更新也是异步的,并且不会立即反映和更新,但会触发重新呈现。在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。

useState原理

我们需要写一个 useState 方法,会返回当前状态和设置状态的方法,每当状态改变之后,方法中会调用刷新视图的 render 方法。状态我们需要放在最外面,方便下次执行函数时可以重新取值。初始状态只会在函数第一次执行的时候赋值。

let memoizedState;
function useState (initialState) {
  memoizedState = memoizedState || initialState
  function setState (newState) {
    memoizedState = newState
    render()
  }
  return [memoizedState, setState]
}

这样确实是可以正常使用,但是当多个 useState 存在的时候就有问题了,只能变一个状态了。现在我们需要优化我们的 Hooks ,解决多个 useState 同时使用的问题,当多个状态存在的时候,我们需要使用数组保存状态。

let memoizedStates = []
let index = 0
function useState (initialState) {
  memoizedStates[index] = memoizedStates[index] || initialState
  let currentIndex = index
  function setState (newState) {
    memoizedStates[currentIndex] = newState
    render()
  }
  return [memoizedStates[index++], setState]
}

useState使用

例子:

function App () {
    const [ count, setCount ] = useState(0)
    return (
      <div>
        点击次数: { count } 
        <button onClick={() => { setCount(count + 1)}}>点我</button>
      </div>
      )
  }

相同值,当我们在使用 useState 时,修改值时传入同样的值,我们的组件是不会渲染的。

function App () {
    const [ count, setCount ] = useState(0)
    console.log('我就看你渲染不')
    return (
      <div>
        点击次数: { count } 
        <button onClick={() => { setCount(count)}}>点我</button>
      </div>
      )
  }

useState注意点

  1. useState最好写到函数的起始位置, 主要是便于阅读
  2. useState严禁出现在代码块(判断和循环等)中
  3. useState返回的函数(数组的第二项), 这个函数的引用是不会变化的(优化性能)
  4. 如果使用函数改变数据, 若数据和之前的数据完全相等(使用Object.is), 则不会重新渲染, 由于Object.is是浅比较, 所以如果状态是一个对象的时候要小心操作了
  5. 如果使用函数改变数据, 传入的值不会和原来的数据进行合并而是直接进行替换(跟setState完全不一样), 所以在修改对象的时候, 我们要先将之前的对象保存下来
  6. 不要直接去改变state的值
  7. 果要实现强制刷新组件的情况: 如果是类组件我们都会使用forceUpdate, 在函数组件中, 我们可以用useState来实现, 使用useState的改变state的函数传入一个空对象, 因为每次传入一个空对象的地址不一样所以一定会刷新。使用const [, forceUpdate] = useState({});forceUpdate({})
  8. 和类组件一样, 函数组件的状态更改在某些时候是一步的(dom事件下), 如果是异步的更改, 则多个状态的更改会合并, 此时不能信任之前的状态, 而应该使用回调函数的方式改变状态
  9. 如果某些状态之间没有必然的联系, 应该分化为不同的状态而非合并成一个对象。

useState更新延迟问题

看过上面的解释,相信大家知道这里要说了什么了

const [current, setCurrent] = useState(0)
  const click = () => {
     setCurrent(current+ 1)
     console.log(current)  //0
  }

解决方案:

  • 用useRef代替(类似沙箱,它还可以“跨渲染周期”保存数据)
  • 直接用变量存储或用函数包裹(组件渲染重新刷新变量)
  • 配合useEffect处理

具体情况具体处理

const current = useRef(0); 
  const click = () => {
     current.current += 1
     console.log(current.current) //1
  }
const [name, setName] = useState('1');
  const handleTest = () => {
    console.log(name) //1
    setName('2')
    console.log(name) //1
  }
  
  useEffect(() => {
    console.log(name) //2
  },[name])