React Hooks

171 阅读8分钟
React组件的核心是**状态管理**和**生命周期**

React Hooks是作为一种改变组件状态、处理组件副作用的方式。

可以实现函数组件不通过class的方式也可以获取到组件状态,并且修改状态。

Hooks的主要卖点之一是可以避免复杂的Class组件和高阶组件。

Hook 规则

Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则。我们提供了一个 linter 插件来强制执行这些规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。

只在 React 函数中调用 Hook

**不要在普通的 JavaScript 函数中调用 Hook。**你可以:

  • 在 React 的函数组件中调用 Hook
  • 在自定义 Hook 中调用其他 Hook 

useState的调用顺序

那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联

因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:

// ------------
// 首次渲染
// ------------
useState('Mary')           // 1\. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm)     // 2\. 添加 effect 以保存 form 操作
useState('Poppins')        // 3\. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle)     // 4\. 添加 effect 以更新标题

// -------------
// 二次渲染
// -------------
useState('Mary')           // 1\. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm)     // 2\. 替换保存 form 的 effect
useState('Poppins')        // 3\. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle)     // 4\. 替换更新标题的 effect

// ...

但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?

 // 在条件语句中使用 Hook 违反第一条规则
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

在第一次渲染中 name !== '' 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:

useState('Mary')           // 1\. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm)  // 此 Hook 被忽略!
useState('Poppins')        //  2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle)     // 3 (之前为 4)。替换更新标题的 effect 失败

React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。

1、 useState 的使用

useState,就是在函数中使用 state。大多数使用方法如下:

const [state, setState] = useState(defaultState)
1\. setState(newState)
2\. setState(oldState => ({...oldState, newKey: value}))
3\. return (<div>{state}</div>)

useState的实现:

// Demo
const MyReact = (function() {
  let _val // 在函数作用域内保存_val
  return {
    render(Component) {
      const Comp = Component()
      Comp.render()
      return Comp
    },
    useState(initialValue) {
      _val = _val || initialValue // 每次执行都会赋值
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState]
    }
  }
})()

使用:

// 继续 Demo 
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  return {
    click: () => setCount(count + 1),
    render: () => console.log('render:', { count })
  }
}
let App
App = MyReact.render(Counter) // render: { count: 0 }
App.click()
App = MyReact.render(Counter) // render: { count: 1 }

2、useEffect 的使用

**useEffect** 相对应的是生命周期

语法:

useEffect(fun, [dependencies]);

//(1)fun:回调函数,当依赖项改变执行,回调函数执行, 
//fun可以有return语句, 当设置return语句代表当卸载组件或者页面时触发执行的函数,
//相当于是组件 destroy 时调用的方法。所以可以当做以前的componentWillUnmount来使用

//(2)dependencies:依赖项, 当依赖改变之后才会执行对应的回调函数;
//当组件或者页面首次加载时,effect 都会执行一次。
//dependencies可以为[], 当为[]的时候useEffect只会执行一次(也就是上面的首次加载执行的一次),
//可以当做componentDdimount来使用
//dependencies若不为空, 则代表当数组中某个元素改变之后,就会触发回调函数。

注意:第二个参数不能为引用类型,引用类型比较不出来数据的变化,会造成死循环。

useEffect 的第二个参数,有三种情况

  1. 什么都不传,组件每次 render 之后 useEffect 都会调用,相当于 componentDidMountcomponentDidUpdate
  2. 传入一个空数组 [], 只会调用一次,相当于 componentDidMount
  3. 传入一个数组,其中包括变量,只有这些变量变动时,useEffect 才会执行

大多数使用场景如下:

1\. useEffect(() => { console.log('hooks') }, [dep1, dep2, ...deps])
2\. useEffect(() => { console.log('hooks') }, [])
3\. useEffect(() => { console.log('hooks') }) // 不传依赖
4\. useEffect(() => { return () => { console.log('hooks') }}, [dep1, dep2, ...deps])
5\. useEffect(() => { return () => { console.log('hooks') }}, [])
首先 `useEffect` **是在页面加载完毕之后才执行的内容**,也就是说它并不是在调用函数 render 时直接执行,但 **React 会按每次的执行顺序**记录下来,在页面加载完毕后按顺序执行 effect。

(1)使用useEffect实现componentDidMount

根据useEffect的第一个参数来实现。设置依赖项为空数组,则可以保证useEffect的回调只会在首次加载的时候执行一次。

function Page1(props) {
    const [state, setState] = useState('test')
    useEffect(() => {
        console.log(state) // 'test'
    }, [])
}

(2)使用useEffect实现componentWillUnMount

根据传入useEffect的回调函数的return语句可以实现。可以在return语句的代码里卸载定时器等。以前的componentWillUnMount的工作,可以放到这里实现。

useEffect(() => { // 卸载时也能拿到最新的 state
        return () => { console.log(state) } // 'efg',在组件卸载时候调用
}, [state])

(3)使用useEffect实现component更新

根据useEffect第二个参数:依赖项数组,可以实现。

function Page1(props) {
    const [state, setState] = useState('abc')
    useEffect(() => {
        console.log(state) // 'abc'
    }, [state]); //表示只有当state改变的时候,才会执行回调
}
**注意:useEffect可以有多个**
function Page4(props) {
    const [state, setState] = useState('abc')
    // 希望页面卸载时用的是最新的 state,但是首次加载时执行 effect 的代码
    useEffect(() => { // 首次加载时执行
        setState('efg')
    }, []) 
    useEffect(() => { // 卸载时也能拿到最新的 state
        return () => { console.log(state) } // 'efg'
    }, [state]);
     useEffect(() => { // 当state更新执行
        setState(state)    }, [state]) 
}

3、useMemo 和 useCallback 的使用

这两个 hook 其实是同一个。

作用都是:Memo 全称是 Memorizer,即存储器。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

(1)useMemo

**useMemo** 可以存储任何数据包括页面节点:

**它可以把值存储起来,**使我们拿到该值的时候不需要再经过相同的计算过程。例如

const memoValue = useMemo(() => {
    return new Array(100000).fill(1).map((item, index) => index)
}, [...deps])
`useMemo` 的第二个参数跟 `useEffect` 一样都是依赖,也就是当依赖发生变化时,存储的值才会重新调用 `useMemo` 里的方法重新取值。

这样可以避免每次 render 都反复执行相同一个复杂耗时的取值方法也就提高了组件或者页面性能,这在渲染大规模数据的时候非常重要。

**使用场景:**

将一个很复杂耗时很久的计算方法, 放入这个函数中存储起来。 当依赖项改变才去计算,这样提高了组件渲染性能。

注意:

当依赖传入一个空数组时,useMemo 相当于存储了一个从加载组件到卸载为止都不会发生变化的值。

(2)useCallback

useCallback 也是同理,可以避免因为多次 render 而每次都重新声明函数的过程。这在数据多的时候也有可大可小的影响。

const handleClick = useCallback((e) => {
    // your code
}, []) //表明只在组件首次加载的时候声明一次函数 

useCallback的第二个参数依赖项,跟useMemo一样。

使用场景:

当父组件给子组件传入参数的时候。父组件由于状态改变更新, 但是父组件传给子组件的props并没有发生改变。 我们就可以优化子组件的更新(仅在props改变的时候才更新子组件), 但是由于传递给子组件的函数每次在父组件重新渲染的时候被重新声明, 导致子组件还是会改变。 所以可以将函数传入useCallback保存起来, 只有当依赖项发生改变, 这个函数才会重新创建。 这样就可以避免子组件的不必要的渲染。

4、useReducer 的使用

useReducer 常用于表单的多数据管理,来看看它的使用:

语法:

const [data, dispatch] = useReducer(reducer, defaultState);
//reducer: state的计算规则, 用来分发处理数据 
//在声明 reducer 方法的时候必须要返回一个最后处理 state 的结果值
//defaultState:默认的state
// data: 返回的新的state
// dispatch: 改变状态的方式(方法)

例:

// 1.声明默认 state
const defaultState = { 
    stateA: 'abc', 
    stateB: 'efg'
}
// 2.声明 reducer 方法
function reducer (state=defaultState, action) {
    const newState = {...state}
    const value = action.value
    switch(action.key) {
      case 'stateA': { // switch 的 case 可以加括号限定作用域
          newState.stateA = value
          return newState
      }
      case 'stateB': {
          newState.stateB = value
          return newState
      }
      default: {
          return newState // reducer 最后结果必须返回一个 state
      }
    }
}

// 3\. 在 React 中使用
function App () {
    const [data, dispatch] = useReducer(reducer, defaultState)
    const click = useCallback((e) => {
        dispatch({key: 'stateA', value: 'efg'})
        dispatch({key: 'stateB', value: 'abc'})
    }, [])
    return (
        <>
            <div>{data.stateA}-{data.stateB}</div>
            <button onClick={click}>click</button>
        </>
    )
}

useState与useReducer的关系

useState 其实是 useReducer 的一种简单实现

function useState(defaultState) {
    // reducer 方法为 null 时,相当于不对 state 进行处理直接返回 dispatch 的值
    return useReducer(null, defaultState)
}

5、useRef 

`useRef` 是**用来获取一个实际的页面节点**;与原来的ref类似。

语法:

const div = useRef(param);
//param: 传入一个任意初始值 ,

//且之后可以在 ref.current 中访问到这个值。

实例:

function App () {
    const div = useRef(null)
    useEffect(() => {
        console.log(ref.current) // <div>123</div>
    }, [])
    return (
        <div ref={div}>123</div>
    )
}

也可以用来存储数据

useRef 中的 ref 实际上是可以存储任何数据,并且你可以对 ref.current 进行任意改写,只是它不会触发组件 render:

const data = useRef({ tmp: 1 })
console.log(data.current) // {tmp: 1} 
//再次 render useRef 不会重新再声明 ref 的值 data.current = { tmp1: 1 } 
console.log(data.current) // {tmp1: 1}

参考文章:

[前端]React16.8+Hoc纯函数性能还OK的项目实践笔记(前篇)