Winter

宏愿纵未了 奋斗总不太晚

0%

React Hooks

React Hooks 是 React 16.8 的新增特性,它可以让我们在不编写 类组件(class component) 的情况下使用 state 以及其他的 React 特性(例如生命周期),所以在 React Hooks 之前,主要是编写类组件。

类组件存在的问题

  1. 逻辑复杂的组件难以开发和维护,组件的生命周期函数中可能包含各种互不想关的逻辑,或者同样的逻辑需要在不同的生命周期函数中实现,例如:在 componentDidMount 中请求数据,在 componentDidUpdate 中判断状态变化请求数据。
  2. 组件状态复用,在 React Hooks 之前,逻辑复用基本是用高阶组件来实现,这种方式会有嵌套地狱 的问题
1
2
3
4
5
6
7
8
9
export default withA(
  withB (
    withC (
      withD (
        Component
      )
    )
  )
)
  1. this 指向问题,在类组件中要搞清楚 this 的指向问题,增加学习成本
    React Hooks 基本能解决上面的问题,而且同一个业务用函数组件的代码量比类组件要少很多,有一些特性用类组件很难实现,所以现在 React 基本都是编写函数组件。

注意:React Hooks 只能在函数组件中使用,不能在类组件、函数组件之外的地方使用。

useState

useState 可以在组件内添加一个状态变量

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

参数

  • initialState:初始化值,不设置为 undefined ,**只在第一次渲染会用到!**

返回值

  • state:当前状态的值(第一次渲染为初始化值)
  • setState:设置状态值的函数,调用之后组件重新渲染,并且根据新的值返回DOM结构
    useState 写一个计数器的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    import { memo, useState } from "react";

    function Counter(props) {

      const [counter, setCounter] = useState(0)

      return (

        <div>
          <h2>当前计数: {counter}</h2>
          <button onClick={e => setCounter(counter+1)}>+1</button>
          <button onClick={e => setCounter(counter-1)}>-1</button>
        </div>

      )

    }

    如果初始化值需要写一些业务代码的话:
    1
    2
    3
    const [counter, setCounter] = useState(() => {
        return 0
    })

useEffect

useEffect 可以让你完成一些类似类组件生命周期的功能,通常用来执行一些副作用的代码。
副作用是指一个 function 做了和本身运算返回值(渲染)无关的事,类似于网络请求、修改DOM、事件监听等

1
useEffect(setup, dependencies?)

参数

  • setup:是一个回调函数,当组件被添加到 DOM 中会被执行,在这里执行副作用的代码,这个回调函数可以返回另一个回调函数 cleanup(清除函数),当组件在 DOM 中被移除会执行 cleanup
  • dependencies:可选,是一个数组,表示该 useEffect 受哪些 state 的影响,如果有 state 被修改的话,会执行 cleanup,然后再执行 setup。如果不传那么每一次重新渲染就会执行 Effect,如果传空数组 [] 表示不依赖任何 state ,适合只执行一次的操作

可以写多个 useEffect 把不同的逻辑分离,调用顺序是每一个 effect 的顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

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

const App = memo(() => {

  const [count, setCount] = useState(0)
  const [message, setMessage] = useState("Hello World")

  useEffect(() => {
    console.log("count:", count)
  }, [count])
 
  useEffect(() => {
    console.log("message:", message)
  }, [message])

  return (

    <div>
      <button onClick={e => setCount(count+1)}>+1({count})</button>
      <button onClick={e => setMessage("Hello")}>修改message({message})</button>
    </div>

  )
})

export default App

根据 useEffect 的机制如果监听某一个属性被改变也可以用

useContext

在 React Hooks 之前如果组件中需要多个 Context 数据时,会有大量的嵌套问题,推荐用 useContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from "./context"

const App = memo(() => {

  // 使用 Context
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)
 
  return (
    <div>
      <h2>User: {user.name}-{user.age}</h2>
      <h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2>
    </div>
  )
 
})

export default App

useCallback

先了解一下 memo

memo

先简单介绍一下 memo 的机制,如果希望子组件避免没必要的重新渲染,可以用这个高阶组件,只要 props 没有发生变化,那么就不会重新渲染

1
2
3
4
5
6
7
import { memo } from 'react';

const ChildComponent = (props) => {
  // ...
}

export default memo(ChildComponent);

useCallback 运行机制

useCallback 可以在组件渲染之间保存函数定义

1
const cachedFn = useCallback(fn, dependencies)

参数

  • fn:希望被缓存的函数,第一次渲染的时候会返回这个函数(不会调用),下次渲染的时候如果依赖没有改变会返回上一次渲染的函数,如果改变会返回这次传入的函数
  • dependencies:依赖值,一般是 fn  中用到的参数

useCallback 可以做性能优化,让子组件避免没必要的重新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

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

const ChildComponent = memo(function(props) {

  const { increment } = props

  console.log("ChildComponent组件渲染")

  return (
    <div>
      <button onClick={increment}>+1</button>
    </div>
  )

})



const App = memo(function() {

  const [count, setCount] = useState(0)

  const increment = () => {
    setCount(count+1)
  }
 
  return (
    <div>
      <h2>计数: {count}</h2>
      <ChildComponent increment={increment}/>
    </div>
  )
 
})

当按钮添加的时候执行 setCount 导致 App 组件重新渲染,会重新定义一个新的 increment 函数,那么 ChildComponent 的 memo 不会生效,从而使 ChildComponent 组件渲染,但 ChildComponent 重新渲染是没必要的,如果它还有大量子组件的话会导致性能严重。

使用 useCallback 优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

const ChildComponent = memo(function(props) {

  const { increment } = props

  console.log("ChildComponent组件渲染")

  return (

    <div>
      <button onClick={increment}>+1</button>
    </div>

  )

})

const App = memo(function() {

  const [count, setCount] = useState(0)

  // 返回一个对象,下次渲染的时候也是返回相同的对象
  const ref = useRef()
  ref.current = count
 
  const increment = useCallback(() => {
    setCount(ref.current + 1)
  }, [])

  return (
 
    <div>
      <h2>计数: {count}</h2>
      <ChildComponent increment={increment}/>
    </div>
   
  )

})

useMemo

useMemo 可以在组件渲染之间缓存函数的返回值

1
const cachedValue = useMemo(calculateValue, dependencies)

参数

  • calculateValue:回调函数,这个函数的返回值就是想要缓存的值
  • dependencies:依赖

    返回值

  • cachedValue:在第一次渲染时候会调用  calculateValue ,然后返回  calculateValue  的返回值,下一次渲染的时候,如果 dependencies 没有改变,那么会返回上一次渲染的值,不需要重新计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

const App = memo(() => {

  const [count, setCount] = useState(0)
 
  // 不依赖任何的值
  const result = useMemo(() => {
      // ...
      return xxx()
  }, [])

  // 依赖 count
  const result2 = useMemo(() => {
    return xxx(count)
  }, [count])

  return (
    <div>
<A result={result}>
    </div>
  )
})

和 useCallback 的区别

以下写法是等价的:

1
2
const increment = useCallback(fn, [])
const increment2 = useMemo(() => fn, [])

useRef

useRef 返回一个对象,这个对象在组件的整个生命周期保持不变。常见的使用常见

  • 持有 DOM

  • 保存一个数据

持有 DOM 的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

import React, { memo, useRef } from 'react'

const App = memo(() => {

  const titleRef = useRef()
  const inputRef = useRef()

  function showTitleDom() {
    console.log(titleRef.current)
    inputRef.current.focus()
  }
 
  return (
    <div>
      <h2 ref={titleRef}>Hello World</h2>
      <input type="text" ref={inputRef} />
      <button onClick={showTitleDom}>查看title的DOM</button>
    </div>
  )

})

export default App

useImperativeHandle

useImperativeHandle 在使用 ref时可以自定义暴露给父组件的函数

1
2
3

useImperativeHandle(ref, createHandle, dependencies?)

参数

  • ref:父组件传过来的 ref 
  • createHandle :回调函数,这个函数返回一个对象,包含想要暴露给 ref 使用的函数
  • dependencies:可选参数,依赖,如果依赖的值改变了 createHandle 会重新调用

使用场景:

  • 限制某一个DOM的操作权限
  • 暴露方法给父组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

const A = memo(forwardRef((props, ref) => {

  const inputRef = useRef()
 
  useImperativeHandle(ref, () => {
 
    return {
      focus() {
        inputRef.current.focus()
      }
    }
   
  })
 
  return <input type="text" ref={inputRef}/>
 
}))

const App = memo(() => {

  const inputRef = useRef()
 
  function handleDOM() {
    inputRef.current.focus()
  }
 
  return (

    <div>
      <A ref={inputRef}/>
      <button onClick={handleDOM}>focus</button>
    </div>
   
  )

})

export default App

App 组件的 inputRef 并没有直接绑定到 A 组件的 input 元素,所以只能调用 focus 函数

useLayoutEffect

useEffect 类似,不过会在组件渲染之前执行回调函数,**会损耗性能!**
使用场景:

  • 如果渲染的内容不是想要的,可以在这里修改再进行渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

const App = memo(function() {

    const [count, setCount] = useState(0)

    useLayoutEffect(() => {
   
        if (count === 10) {
            setCount(1)
        }

    }, [count])

}

useTransition

useTransition 可以让优先级较低或渲染比较耗时的组件稍后渲染

1
const [isPending, startTransition] = useTransition()

返回值

isPending:是否在过渡状态

  • startTransition:函数,让优先级低的状态更新在这里处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54

    import React, { memo, useState, useTransition } from 'react'

    import namesArray from './namesArray'

    const App = memo(() => {

      const [ names, setNames ] = useState(namesArray)
      const [ isPending, startTransition ] = useTransition()

      function valueChangeHandle(event) {

        startTransition(() => {

          const keyword = event.target.value

          const filterNames = namesArray.filter(item => item.includes(keyword))

          setNames(filterNames)

        })

      }

      return (

        <div>

          <input type="text" onInput={valueChangeHandle}/>

          <h2>列表: {isPending && <span>loading</span>} </h2>

          <ul>

            {

              names.map((item, index) => {

                return <li key={index}>{item}</li>

              })

            }

          </ul>

        </div>

      )

    })

    export default App

    有点类似防抖(debounce)和节流(throttle),但是对比这两个有以下优势:
  • 更新协调过程是**可中断的**,渲染引擎不会长时间被阻塞,用户可以及时得到响应
  • 不需要开发人员去做额外的考虑, 整个优化过程交给 react浏览器 就行 

useDeferredValue

useDeferredValue 可以让 UI 延迟更新

1
const deferredValue = useDeferredValue(value)

参数

  • value:想要延迟更新的值

返回值

  • deferredValue:第一次渲染的时候返回 value,在下一次渲染的时候会延迟更新(返回旧值),最终拿到最新值重新渲染
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52

    import React, { memo, useState, useTransition } from 'react'
    import namesArray from './namesArray'

    const App = memo(() => {

      const [ names, setNames ] = useState(namesArray)
      const deferredNames = useDeferredValue(names)

      function valueChangeHandle(event) {

        startTransition(() => {

          const keyword = event.target.value

          const filterNames = namesArray.filter(item => item.includes(keyword))

          setNames(filterNames)

        })

      }

      return (

        <div>
       
          <input type="text" onInput={valueChangeHandle}/>
          <h2>列表: {isPending && <span>loading</span>} </h2>
         
          <ul>

            {

              deferredNames.map((item, index) => {

                return <li key={index}>{item}</li>

              })

            }

          </ul>

        </div>

      )

    })

    export default App