Hooks

2,250 阅读7分钟

useState

用法

const [state, setState] = useState(initialState)

更新方法

  1. 函数式更新

    setState((prevValue) => PrevValue + 1) 参数为前一个值

  2. 普通更新

    setState(value)

初始化方法

  1. 普通的初始化方法

    useState(initialState)

  2. 函数式的初始化方法

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

    ⚠️ 使用这种方式的好处是someExpensiveComputation函数值会在初始化的时候,调用一次。

    我们其实也可以这么做。

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

    这样的缺点就是,每次函数组件渲染的时候,都会调用一次``someExpensiveComputation`,造成不必要的性能浪费。

首先useState是一个函数,每次运行useState,它的第一个参数都是修改后的状态值,这个值应该是存在固定的指针下, useState只是去同一个地方取值而已。

需要注意的地方

每次运行useState返回的setState 指向同一个函数。这也就是为什么不需要在useEffectuseCallback中指定依赖。

import React, { useState, useEffect } from 'react';
function Hello () {
    const [top, setScrollTop] = useState(1);
    useEffect(() => {
     	window.addEventListener('scroll', (e) => {
        setScrollTop(e.target.scrollTop);
      }, false)
    }, [])
  
  return <h1>top: {top}</h1>
}

假设我们有这样一个场景,需要监听滚动,并且把滚动的距离输出。 如果每次返回的setScrollTop不是指向同一个函数,而是随着top进行更新的话,那我们就需要在每次组件更新的时候,去重新绑定滚动事件。

代码会是这样:

import React, { useState, useEffect } from 'react';
function Hello () {
    const [top, setScrollTop] = useState(1);
    useEffect(() => {
     	window.addEventListener('scroll', (e) => {
        setScrollTop(e.target.scrollTop);
      }, false)
    }, [setScrollTop])
  
  return <h1>top: {top}</h1>
}

如果在useEffect中调用设置状态的方法,会不会导致组件无限刷新,考虑下面这样的代码

function Hello () {
    const [top, setScrollTop] = useState(1);
    useEffect(() => {
      console.log('do it');
    	setScrollTop(10)
    })
  
  return <h1>top: {top}</h1>
}

结果是输出两次do it。并不会无限刷新组件。 在组件第一次挂载的时候,会执行useEffect输出do it, 此时执行setScrollTop, top会被设置成10。 此时react会对比原值和当前值即1!==10, 此时刷新组件。 当dom渲染完成之后,再次执行useEffect, 输出do it, 此时react会对比原值和当前值即10 === 10, 不再更新组件。

useEffect

用法

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});

可以用它来代替react类组件中的生命周期,componentDidMount, componentDidMount, componentWillUnmount。一般用来执行一些有副作用的操作。

需要注意的地方

  1. 组件在更新的时候,也会执行清除函数。

  2. useEffect的执行时机是在浏览器完成布局与绘制之后。

    所以可以在useEffect获取渲染完成之后的dom节点。类似``componentDidMount`

如何使用useEffect来实现类组件中的生命周期呢?

useEffect有一个可选参数,通过控制第二个参数,可以实现componentDidMountcomponentDidUpdate生命周期。 第二个参数表示``useEffect要在哪些值发生变化后执行, 作用很相当于vue中的watch`

  1. 实现componentDidMount, 第二个参数传入空数组,表示不依赖任何值,那么只会在组件初次渲染的时候的执行。

    useEffect(() =>{}, [])
    
  2. 实现componentDidUpdate, 并且比componentDidUpdate更细致,例如一个常见的需求,当id发生变化时,去重新请求数据。

    // hooks版本
    function Hello ({id, age}) {
      const [list, setList] = useState([]);
      useEffect(async () => {
        const data = await api.query(id);
        setList(data);
      }, [id]) // 当id发生变化的时候去请求数据
      return (<div> {list.map(i => (<span>{i}</span>))} </div>)
    }
    
    // 类版本
    async componentDidUpdate (prevProps) {
      // 需要自己判断id是否相等,去执行请求的数据, 尤其是依赖多个条件的时候,还需要挨个判断,
      if (prevProps.id !== props.id) {
           const data = await api.query(id);
           setList(data);
        }
     }
    
  3. 实现componentDidUpdate生命周期, 只需要在传入的函数中返回一个卸载时需要执行的函数即可

    useEffect(() =>{
      return function clear () {}
    })
    

⚠️ 使用useEffect最重要的一点是它可以把相关逻辑放到一起,例如我们经常会在组件挂载或者卸载的使用进行一些操作。例如常见的设置事件监听和移除事件监听。

// 类写法
handleScroll () {}
componentDidMount () {
  window.addEventListener('scroll', this.handleScroll, false);
}
componentWillUnmount () {
 window.addEventListener('scroll', this.handleScroll, false);
}

function handleScroll () {}
// hooks写法, 相关逻辑被放到了一起
useEffect(() =>{
  window.addEventListener('scroll', handleScroll, false);
  return window.removeEventListener('scroll', handleScroll);
}, [])


useContext

订阅context的变化,感觉就是对于获取context的值换了一种写法而已。相对于之前的写法,在函数组件中添加context更加简单。

用法

const context = React.createContext({})
const { Provider, Consumer  } = context; 
// hooks的写法
class App extends React.Component {
     return (
                <Provider
                    value={{
                            name: 'li'
                        }}
                > 
                    <Hello/>
                </Provider>
            </div>
}
  
function Hello () {
    const value = useContext(context); 
    return <h1>value: {value.name}</h1>
}
  
// 原本的写法
function Hello (props) {
    function render ({name}) {
      return <h1>value: {value.name}</h1>
    }
  
    return (
      <Consumer>
        {render}
      </Consumer>
    )
}
  

useReducer

类似于redux那样的状态更新方案。

使用场景(基本上就是redux的应用场景)

  1. 管理的状态值是对象,并且键值较多。
  2. state每个key修改的逻辑比较复杂,需要单独放到一个文件里面管理。

用法

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

useCallback

仅在指定的依赖项发生变化时,会返回一个新的函数引用,函数体并木有发生变化。

用法

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

这样使用的好处

  1. 不会在每次组件render的时候,重新生成一个函数,节省开销。例如

    function f () {
      const cacheCallback = useCallback(
          () => {
        			doSomething(a, b);
      			},
      			[a, b],
      )
      // 和下面这样的形式相比, 每次组件渲染的时候,都会重新创建一个doSometing函数
      function doSometing (a,b) {}
    }
    
  2. 可以保持函数的引用保持不变。我们都知道在类组件,事件处理函数基本上都是通过this.method的方式绑定的,这样做的方式有一个好处,对方法的引用一直保持不变。 那么在函数组件就可以通过使用useCallback来实现。

  3. 可以实现在子组件把该回调作为依赖处理。

    function Parent ({a, b}) {
      const cacheCallback = useCallback(
          () => {
            doSometing(a, b);
          },
          [a, b]
      )
      return <Child handler={cacheCallback}/>
    }
    
    function Child ({ handler }) {
      useEffect(() => {
        handler();
      }, [handler])
    }
    

useMeno

类似于vuecomputed,在依赖发生变化的时候重新计算缓存值。其实自己实现起来也很容易,和vue的计算属性不同的是,vue的计算属性是自动收集依赖的,而使用useMeno需要手动在数组种传入依赖项。

用法

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useRef

故名思义,该hook主要是用来获取组件实例或者或者dom节点。 但是它更有用的地方,是可以返回一个在组件生命周期内,引用不变的对象。

用法

function f () {
  const elRef = uesRef(null);
  return <div ref={elRef}></div>
}

用来存储数据的话,考虑下面的场景。

let handler = () => {}; // 事件处理函数
// 不使用useRef, 可以使用函数外部的一个变量来存储数据
function f () {
  useEffect(() => {
    window.addEventListener('scroll', handler)
  }, [])
  
  const moveScroll = useCallback(
    () => {
      window.removeEventListener('scorll', handler)
    },
    []
  )
  
  return <div onClick={moveScroll} ref={elRef}>移除scroll监听</div>
}


// 使用useRef的版本,可以使代码更加内聚。但是前提是必须要理解useRef这个hooks。
function f () {
  
  const handler = useRef(null);
  handler.current = () => {}  // 事件处理
  
  useEffect(() => {
    window.addEventListener('scroll', handler.current)
  }, [])
  
  const moveScroll = useCallback(
    () => {
      window.removeEventListener('scorll', handler.current)
    },
    []
  )
  
  return <div onClick={moveScroll} ref={elRef}>移除scroll监听</div>
}

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。

用法

const Fancy = React.forwardRef((props, ref) => {
    return <div>
        <input type="text" ref={ref}/>
    </div> 
})

function Hello () {
    const ref = useRef(null); 

    useEffect(() => {
        console.log('current', ref); // { current: Input }
    }, [])

    return <Fancy ref={ref}/>
}

useLayoutEffect

函数签名和useEffect是一样的, 可以使用它来读取 DOM 布局并同步触发重渲染。

useDebugValue

用来给hooks添加上打印信息。