React Hooks的基本理解

731 阅读6分钟

什么是hooks ?

Hooks是React16.8中新增的功能,它们让你无需编写类即可使用状态和其他 React 功能

src=http _pic2.zhimg.com_v2-8b48d28670a1568e4cf08000725abb81_1440w.jpg&refer=http _pic2.zhimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg.jpg

Hooks是函数,它有多个种类,每个hook都是为function Component提供使用React状态和声明周期特性的通道。Hooks不能在class Component中使用,React中提供了一些预定义好的Hooks供我们使用,下面我们来了解一下:

useState

咱们来看一段代码

import React, { useState } from 'react';

function Example() {
  // 定义一个 State 变量,变量值可以通过 setCount 来改变
  const [count, setCount] = useState(0);

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

可以看到useState的入参只有一个,就是state的初始值。这个初始值可以是一个数字、字符串、数组或对象,甚至可以是一个函数,当入参是一个函数的时候,这个函数只会在这个组件初始化渲染的时候执行:

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

当需要根据之前的状态值来计算出当前状态值的时候,就需要传入函数,这跟class Component 中的setState有点像,另外跟class Component的setState很像的一点是,当新传入的值跟之前的值一样时(使用object.is比较),不会触发更新。

useEffect

解释useEffect之前先理解一下什么是副作用。网络请求,订阅某个模块或DOM操作都是副作用的例子,useEffect专门用来处理副作用的。正常情况下,在function Component的函数中,是不建议写副作用代码的,容易出bug。

下面的class Component例子中,副作用代码写在了componentDidMountcomponentDidUpdate中:

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

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

可以看到componentDidMountcomponentDidUpdate中的代码一样,而使用函数组件中的useEffect来写就不会出现这样的问题:

import React, {useState, useEffect} from "react";

function FriendStatus(props) {

    const [count,setCount] = useState(0);
    
    useEffect(()=>{
        document.title = `You clicked $(count) times`
    });
    
    return (
        <div>
            <p>You clicked {count} times</p>
            <button @click={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    )
}

useEffect会在每次 DOM 渲染后执行,不会阻塞页面渲染,它同时具备componentDidMountcomponentDidUpdateomponentWillUnmount三个声明周期函数的执行时机。

此外还有一些副作用需要组件卸载的时候做一些额外的清理工作,如订阅某个功能:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

componentDidMount订阅后,需要在componentWillUnmount取消订阅,接下来我们使用函数组件中的useEffect

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    
    // 返回一个函数来进行额外的清理工作:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

useEffect返回一个函数时,React会在下一次执行这个副作用之前执行一次清理工作,整个组件的声明周期流程可以如下理解:

组件挂载 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 组件卸载

上文提到useEffect会在每次渲染后执行,但有的时候我们只希望在stateprops改变才执行

如下class Component中的使用:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

使用useEffect时,我们只需要传入第二个参数:

useEffect(() => {
 document.title = `You clicked ${count} times`;
}, [count]);     // 只有在 count 改变的时候才执行 Effect

第二个参数是个数组时,可以传入多个值,一般会将用到的所有props和state都传进去

当副作用只需要在组件挂载的时候和卸载的时候执行,第二个参数都可以传入一个空数组[],实现效果优点类似于componentDidMountcomponentWillUNmount的组合

useLayoutEffect

useLayoutEffect的用法和useEffect的用法完全一样,都可以执行副作用和清理操作,它们之间唯一的区别就是执行的时机不同

useEffect不会阻塞浏览器的绘制任务,它在页面更新后才会执行。

useLayoutEffectcomponentDidMountcomponentDidUpdate的执行时机一样,会阻塞页面渲染,如果在里面执行耗时任务的话,会出现页面卡顿。

在大多数情况下,useEffect是非常好的选择。唯一例外的就是需要根据新的 UI 来进行 DOM 操作的场景,useLayoutEffect会保证在页面渲染前执行,也就是页面渲染出来的是最终的效果。如果使用useEffect,页面很有肯能因为渲染了2次而出现的页面抖动。

useContext

useContext可以帮助我们跨组件层级直接传递变量,实现共享

以下代码就相当于把count变量允许跨层级实现传递和使用了(也就是实现了上下文),当父组件的count发生改变时子组件也会发生改变:

import React, { useState , createContext } from 'react';

const CountContext = createContext()

function Example(){

    const [ count , setCount ] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={()=>{setCount(count+1)}}>click me</button>
    
            <CountContext.Provider value={count}>
            </CountContext.Provider>

        </div>
    )
}
export default Example;

接下来我们看看一个hook组件是如何接收到这个变量的

import React, {useState , createContext , useContext} from 'react';

function Counter(){
    const count = useContext(createContext)    //一行代码就可以拿到count
    
    return ( <h1>{count}</h1> )
}

得到后就可以显示出来了,但要记得在<CountContext.Provider>的闭合标签中,如下:

<CountContext.Provider value={count}>
    <Counter />
</CountContext.Provider>

useMemo

下面我们先看一个没有使用useMemo的例子:

import React from 'react'

export default function withoutMemo() {
    const [count , setCount] = useState(1);
    const [val , setValue] = useState('');
    
    function expensive() {
        let sum = 0;
        for (let i = 0; i < count * 100; i++){
            sum += i;
        }
        return sum;
    }
    
    return (
        <div>
            <h3>{count} - {val} - {expensive()}</h3>
            <div>
                <button @click={() => setCount(count + 1)} > +c1 </button>
                <input value={val} onChange={event => setValue(event.target.value)} />
            </div>
        </div>
    )
}

这里创建了两个state,然后通过 expensive 函数,执行一次昂贵的计算,拿到count对应的某个值,我们可以看到:无论是修改count还是val,由组件的重新渲染,都会触发 expensive 的执行,但是这里的计算只依赖于count的值,在val修改的时候,是没有必要再次进行计算的,这种情况我们可以使用useMemo 如下:

import React from 'react'

export default function withoutMemo() {
    const [count , setCount] = useState(1);
    const [val , setValue] = useState('');
    const expensive = useMemo(() => {
        let sum = 0;
        for (let i = 0; i < count * 100; i++){
            sum += i;
        }
        return sum;
    },[count]);
    
    return (
        <div>
            <h3>{count} - {expensive()}</h3>
            {val}
            <div>
                <button @click={() => setCount(count + 1)} > +c1 </button>
                <input value={val} onChange={event => setValue(event.target.value)} />
            </div>
        </div>
    )
}

上面我们可以看到,使用useMemo来执行昂贵的计算,然后将计算值返回,并且将count作为依赖值传递进去。这样,就只会在count改变的时候触发expensive执行,在修改val的时候,返回上一次缓存的值。

useCallback

讲完了useMemo,接下来是useCallback。useCallback跟useMemo比较类似,但它返回的是缓存的函数。我们看一下最简单的用法:

const fnA = useCallback(fnB, [a])

上面的useCallback会将我们传递给它的函数fnB返回,并且将这个结果缓存;当依赖a变更时,会返回新的函数。既然返回的是函数,我们无法很好的判断返回的函数是否变更,所以我们可以借助ES6新增的数据类型Set来判断,具体如下:

import React, { useState , useCallback } from 'react'

const set = new set();

export default function Callback() {
    const [count , setCount] = useState(1);
    const [val , setValue] = useState('');
    
    const callback = useCallback(() => {
        console.log(count)
    },[count]);
    
    set.add(callback)
    
    return (
        <div>
            <h3>{count}</h3>
            <h3>{set.size}</h3>
            <div>
                <button @click={() => setCount(count + 1)} > +c1 </button>
                <input value={val} onChange={event => setValue(event.target.value)} />
            </div>
        </div>
    )
}

我们可以看到,每次修改count,set.size就会+1,这说明useCallback依赖变量count,count变更时会返回新的函数;而val变更时,set.size不会变,说明返回的是缓存的旧版本函数。

知道useCallback有什么样的特点,那有什么作用呢?

使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

import React, { useState , useCallback , useEffect } from 'react'

function Parent() {
    const [count , setCount] = useState(1);
    const [val , setValue] = useState('');
    
    const callback = useCallback(() => {
        return count;
    },[count]);
    
    set.add(callback)
    
    return (
        <div>
            <h3>{count}</h3>
            <Child callback={callback} />
            <div>
                <button @click={() => setCount(count + 1)} > +c1 </button>
                <input value={val} onChange={event => setValue(event.target.value)} />
            </div>
        </div>
    )
}

function Child({ callback }) {
    const [count , setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    },[callback]);
    
    return <div> {count} </div>
}

不仅是上面的例子,所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都是useCallback的应用场景。

useRef

useRef返回一个普通 JS 对象,可以将任意数据存到current属性里面,就像使用实例化对象的this一样。

function Counter() {
  const [count, setCount] = useState(0);

  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count;
  });
  const prevCount = prevCountRef.current;

  return <h1>Now: {count}, before: {prevCount}</h1>;
}

自定义Hooks

自定义 Hook 其实就是一个普通的函数定义,以use开头来命名也只是为了方便静态代码检测,不以它开头也完全不影响使用。

总结

到此为止,Hooks 相关的内容基本介绍完了,想要彻底理解 Hooks 的设计是需要投入相当精力的,希望本文可以为你学习这一新特性提供一些帮助。

src=http _img.adoutu.com_picture_1538472313820.jpg&refer=http _img.adoutu.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg.jpg