# react-hooks 的新特性

# 概念

我们知道,在react里面只有类组件才可以使用state,很多时候我们在编写函数组件的时候突然意识到需要state才可以完成特定需求的时候,最后不得不将函数组件转化为class组件的写法,而hooks就是让我们可以在函数组件当中使用state的新特性

# 注意事项

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。

  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用

# useState

# 初体验

我们先来看下react-hooks中的第一个简单用法, 函数组件中可以通过useState来给组件添加state,但是和class组件不一样的地方在于他不会把新的state和旧的state进行合并,而每次渲染的时候都是一个独立的state,我们先来看下基本用法来理解一下他




 






function Counter1() {
    const [number, setNumber] = useState(0)
    return (
        <>
            <p>{ number }</p>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </>
    )
}

还是这个简单计数器例子,我们通过useState(0)创建一个state,并且初始值为0,而返回值是一个数组,至于为什么是一个数组我们放在下一章节手写一个react-hooks中来解释,这个数组的第一项就是我们想要的state中的值,第二项就是一个更新这个值的方法,它接收一个新的 state 值并将组件的一次重新渲染加入队。

# 每次渲染都是独立的闭包

怎么理解这句话呢,我们先写一个例子




 












function Counter2(){
  const [number,setNumber] = useState(0);
  function alertNumber(){
    setTimeout(()=>{
      alert(number);
    },3000);
  }
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
          <button onClick={alertNumber}>alertNumber</button>
      </>
  )
}

我们会发现,当我们点击第一个按钮两次,在点击第二个按钮一次,最后点击第一个按钮两次,最后alert的值会是2而不是我们想象中的4

  • 为什么会出现这个原因呢?

我们可以这么理解,当我们点击alertNumber的时候,调用了state中number的值,我们可以理解为相当于一个闭包,state里面的值一直更给引用,所有获取到的是点击时候当前的值(虽然正确的解释不是这样的~~)

# 函数式更新

还是老规矩我们先来看一个例子




 












function Counter3() {
    const [number, setNumber] = useState(0)
    function lazy1() { 
        setTimeout(() =>{
            setNumber(number+1)
        }, 3000)
    }
    return (
        <>
            <p>{ number }</p>
            <button onClick={() => setNumber(number + 1)}>+</button>
            <button onClick={lazy1}>lazy1 number+</button>
        </>
    )
}

TIP

当我们点击第一个按钮两次,在点击第二个按钮一次,最后点击第一个按钮两次的时候,页面最终显示的结果是啥

我们看下效果图

An image

结果最后由4变成3吧,至于为什么,其实和上一个小节一样,我们只需记住每一次渲染都是独立的,获取的都是当前状态的值,这里在点击第二个按钮的时候,此时的numner的值为2,所以三秒后的页面上的值为 2 + 1

但是问题来了,我们如果想实现每次点击第二个按钮,获取到的都是最新的一个值,那我们可以通过setNumber(fn)的写法来实现




 












function Counter3() {
    const [number, setNumber] = useState(0)
    function lazy2() {
        setTimeout(() =>{
            setNumber(number =>number + 1)
        }, 3000)
    }
    return (
        <>
            <p>{ number }</p>
            <button onClick={() => setNumber(number + 1)}>+</button>
            <button onClick={lazy2}>lazy2 number+</button>
        </>
    )
}

我们来看下效果

An image

是不是就可以实现我们的效果了,其中 setNumber(number =>number + 1) 中的形参number表示上一个状态的number,这也是我们这节所说的函数式更新,是不是和 this.setState很像啊

# 惰性初始 state

在我们日常开发中,有时候我们需要给组件传入一些状态值作为组件的初始化state来使用吧,我们来看下hooks是怎么应对这种情况的

function Counter4(props) {
    function initState() {
        return {
            number:props.number
        }
    }

    const [counter, setCounter] = useState(initState)
    return (
        <>
            <p>{ counter.number }</p>
            <button onClick={() => setCounter({number : counter.number + 1})}>+</button>
        </>
    )
}

ReactDOM.render(<Counter4 number={5}/>, window.root)

从代码中我们可以看出,组件Counter4传入了一个number属性,而我们的useState的用法是一种useState(fn)的用法,这样的话就可以实现上述的功能了。

需要注意的是,fn只会在组件一开始第一个渲染的时候执行,只执行一次,后序渲染会直接忽略。

TIP

为什么叫 惰性初始 state ?

为什么叫惰性初始化呢,其实和我们的路由懒加载差不多,首先它是一个函数,只有在我们使用到counter的时候他才会执行这个函数,使用则不执行

# 性能优化

其实hooks里面集成了很多性能优化,这里我们就挑2个地方来简单的说一下吧

# 每次修改之后state没有发生变化,则页面不重新渲染




 









function Counter5(props) {
    console.log('render')
    // 如果当状态发生变化的时候,状态的值没有改变的话 不会重新渲染组件
    const [number, setCounter] = useState(0)
    return (
        <>
            <p>{ number }</p>
            <button onClick={() => setCounter(number + 1)}>+</button>
            <button onClick={() => setCounter(number)}>+</button>
        </>
    )
}

执行上述代码的时候我们发现,点击第一个按钮,state的值发生了改变,所以控制台打印了'render', 但是当我们点击第二个按钮的时候,因为state的值没有改变,我们发现控制台并没有输出东西,这也是hooks内部做的一个小优化,当调用setXXX()来修改state的值,如果值没有发生变化,组件不会重新渲染

# 使用 useMomo、useCallback 减少渲染次数

我们先来看一个比较简单的例子




 



















function Counter6(props) {
    console.log('Counter6 render')
    const [number, setNumber] = useState(0)
    const [counter, setCounter] = useState(0)
    const data = {
        counter,
    }
    const addClick = () => setCounter(counter + 1)
    return (
        <>
            <input value={number} onChange={(e) => {setNumber(e.target.value)}} />
            <SubCounter data={data} onClick={addClick} />
        </>
    )
}

function SubCounter(props) {
    console.log('SubCounter render')
    return (
        <button onClick={props.onClick}>{props.data.counter}</button>
    )
}

当我们在输入框中修改输入框的值,实际上是只修改了state里面的number的值,没有修改counter的值,但是页面上出现了2个console,这表明了两个组件Counter6SubCounter 都重新渲染了,其实这也是正常现象,这是因为我们hooks每次更新都是独立的,也就是每次拿到的data等值都是最新的,所以触发更新

但是啊,hooks给我们提供了2个可以减少这类情况渲染次数的方法,我们力求于修改state中的number的时候,组件SubCounter没有重新渲染




 


















SubCounter1 = memo(SubCounter1) // new
function Counter7(props) {
    console.log('Counter7 render')
    const [name, setName] = useState('CY')
    const [counter, setCounter] = useState(0)
    const data = useMemo(() => ({ counter }), [counter]) // new
    const addClick = useCallback(() => setCounter(counter + 1), [counter])// new
    return ( 
        <>
            <input value={name} onChange={(e) => {setName(e.target.value)}} />
            <SubCounter1 data={data} onClick={addClick} />
        </>
    )
}

function SubCounter1(props) {
    console.log('SubCounter1 render')
    return (
        <button onClick={props.onClick}>{props.data.counter}</button>
    )
}

在代码中我们可以发现使用了memo(Component)将我们的子组件包裹起来,使用useMemo()useCallback()将我们的数据和方法包裹起来,其中useMemo()useCallback()的第二个参数是依赖项的意思,表示只有在依赖项发生改变的时候才会重新计算值或者执行函数。这也就是说只有当counter改变的时候才会重新计算或者执行函数,配合memo(Component)使用的时候,我们单纯的修改state里面的number的时候,是不会触发更新的

# useReducer

看到名字我们就知道是使用redux了,useReducer内部帮我们集成了redux的整套流程,就不需要我们在组件内部绑定数据,添加订阅啥的,我们可以很优雅的在函数组件里面使用redux了,我们直接看用法吧




 























import React, { useReducer } from 'react'
import ReactDOM from 'react-dom'

function reducer(state, action) { //创建reducer
    switch (action.type) {
        case 'ADD':
            return {
                number: state.number + 1
            }
            default:
            break;
    }
}

const initState = 0  // 创建初始值
function Counter() {
    const [state, dispatch] = useReducer(reducer, initState, () => ({number : initState}))
    return (
        <>
            <p>{ state.number }</p>
            <button onClick={() => (dispatch({type: 'ADD'}))}>+</button>
        </>
    )
}

ReactDOM.render(<Counter/>, window.root)

TIP

其实useState内部是靠useReducer来实现的

useReducer的第一个参数是我们的reducer了,第二个参数是我们state的初始值,第三个参数是我们的函数式更新的写法,可以在初次渲染的时候动态设置我们的初始值,然后我们就可以很简单的拿到 state, dispatch啦

# useContext

useContext的用法我们就不举例子说了,很简单就可以让我们使用上下文,而不同于以前函数组件使用上下文的时候需要使用ContextConsumer包裹起来




 







// 子组件
function SubCounter() {
    const {xxx, YYY} = useContext(Context) // 传入context 就可以获取到上下文中的值了
    return (
        <>
            <p>{state.number}</p>
            <button onClick={() => {dispatch({type: 'ADD'})}}>+</button>
        </>
    )
}

# useEffect

# 初体验

useEffect是我认为react-hooks里面较为新的东西,他的目的没有和useContext这些一样是为了简化某些操作,或者为函数组件提供某些操作,useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。我们来看一个在以前只有类组件才可以实现的例子




 
























class Counter extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        number: 0
      };
    }

    componentDidMount() {
        document.title = this.state.number;
    }

    componentDidUpdate() {
        document.title = this.state.number;
    }

    render() {
      return (
        <div>
          <p>{this.state.number}</p>
          <button onClick={() => this.setState({ number: this.state.number + 1 })}>
            +
          </button>
        </div>
      );
    }
  }

该例子的大概意思是,当我们点击按钮修改state的时候,我们需要同步的修改浏览器title的值,这个在函数组件是无法实现的,但是我们在使用hooks之后函数组件也拥有这个能力




 














import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
    const [number,setNumber] = useState(0);
    // 相当于 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        // 使用浏览器的 API 更新页面标题
        document.title = number;
    });
    return (
        <>
            <p>{number}</p>
            <button onClick={()=>setNumber(number+1)}>+</button>
        </>
    )
}
ReactDOM.render(<Counter />, document.getElementById('root'));

这就是 useEffect的大致用法了,我们只需要知道,函数组件也可以和class组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途

TIP

每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染

# 清除副作用

但是我们的useEffect在某一些特定的情况下是有害的,如




 











function Counter(){
  const [number,setNumber] = useState(0);
  useEffect(() => {
     console.log('开启一个新的定时器')
     const $timer = setInterval(()=>{
      setNumber(number=>number+1);
     },1000);
  });
  return (
      <>
          <p>{number}</p>
      </>
  )
}

如上述代码,我们希望页面上的数据每一秒加一,但是实际的情况确实页面上的数据几乎是成倍的增加,出现了内存泄漏,这是因为我们一直提到,hooks的每一次渲染都是独立的,上述代码相当于每一次渲染都增加了一个计时器,所以计时器叠加导致出现了内存泄漏,那么我们如何避免这种情况呢




 
















function Counter(){
  const [number,setNumber] = useState(0);
  // 相当于componentDidMount 和 componentDidUpdate
  useEffect(() => {
     console.log('开启一个新的定时器')
     const $timer = setInterval(()=>{
      setNumber(number=>number+1);
     },1000);
      return ()=>{
        console.log('销毁老的定时器');
        clearInterval($timer);
     } 
  });
  return (
      <>
          <p>{number}</p>
      </>
  )
}

我们只需要在useEffect(fn)的fn里面返回一个函数就好了,这个函数会在下一个渲染一开始的时候执行,这个时候我们可以清楚页面上的定时器

我们还有一种方法




 











function Counter(){
  const [number,setNumber] = useState(0);
  useEffect(() => {
     console.log('开启一个新的定时器')
     const $timer = setInterval(()=>{
      setNumber(number=>number+1);
     },1000);
  },[]);
  return (
      <>
          <p>{number}</p>
      </>
  )
}

useEffect(fn, [xx]) 接受第二个参数依赖项,意思是fn的执行依赖于依赖项,当我们传入空数组[]的时候意味着fn只执行一次,所以就不会出现内存泄漏的情况了