Skip to content

S13-06 React-Hooks

[TOC]

API

React

  • useState(initialState?)返回:[state, setState],用于在函数组件中管理状态(state)

    • 参数
    • initialState?any,状态的初始值,不设置则为 undefined
    • 返回
    • state:``,当前状态值
    • setState:``,用于更新状态值的函数。当使用这个函数设置新的状态时,React 会根据新的状态重新渲染组件。
  • useEffect(callback, dependencies?)返回:,用于在函数组件中执行副作用操作,例如访问 API、订阅事件、设置定时器等,可以模拟生命周期

    • 参数
    • callback() => void | callbackFn,要执行的副作用代码,可以是异步的或同步的
      • 返回
      • EffectCallback,在useEffect()函数中返回一个函数,这个函数将在组件卸载时执行。
    • dependencies?[],包含了与回调函数有关的依赖项,当这些依赖项发生变化时,才会重新执行回调函数。如果该数组为空,则仅在组件挂载和卸载时运行一次副作用代码。
  • useContext(MyContext)返回:value,可以实现组件之间的数据共享。让你在组件树中传递数据,而不需要手动的将 props 一级一级地传递下去。

    • 参数
    • MyContextContext,一个 Context 对象,它可以通过 React.createContext()来创建。
    • 返回
    • value:``,返回该 Context 对象的当前值。
  • useReducer(reducer, initialState)返回:[state, dispatch],提供了一种可预测且可测试的方式来处理状态更新逻辑。

    • 参数
    • reducer(state, action) => void,用于根据当前状态和传递的 action 返回新的状态
    • initialStateany,状态的初始值
    • 返回
    • state:``,当前状态
    • dispatch(action) => void,用来分发 action 来触发状态更新
  • useCallback(callback, deps)返回:memorizedCallback,用于返回一个 memorized(记忆化)的回调函数。可用于提高性能

    • 参数
    • callbackFunction,要记忆化的回调函数
    • depsArray,是一个依赖数组,当依赖数组中的任意一个值发生变化时,callback就会重新生成。
    • 返回
    • memorizedCallbackFunction,memoized(记忆化)的回调函数
  • useMemo(callback, deps)返回:memoizedValue,用于缓存计算结果,以便在依赖项未更改时避免不必要的重新计算。

    • 参数
    • callbackFunction,计算逻辑的回调函数
    • depsArray,是一个依赖数组,当依赖项数组中的任何一个值发生变化时,useMemo()会重新计算并返回新的计算结果。如果依赖项数组为空,则该函数仅在组件首次渲染时被计算。
    • 返回
    • memoizedValueFunction,memoized(记忆化)的计算值
  • useRef(initialValue?)返回:refContainer,用于创建一个可变的引用,它返回一个对象,对象中有一个 current 属性,该属性的值可以在组件的整个生命周期中保持不变,并可以被读取和修改。

    • 参数
    • initialValue?any,表示 refContainer.current 的初始值
    • 返回
    • refContainer{current},refContainer 对象中有一个 current 属性,该属性的值可以在组件的整个生命周期中保持不变,并可以被读取和修改。
  • useImperativeHandle(ref, createHandle, deps?)返回:,用于向父组件暴露子组件的实例方法

    • 参数
    • refObject | (cpn) => void,如果 ref 参数是函数,则函数的第一个参数为子组件的实例对象,用于设置 ref 的值。
    • createHandle() => {method,...},用于创建一个对象,该对象包含子组件需要向父组件暴露的方法。
    • deps?array,包含所有影响 createHandle 函数的值,当这些值发生变化时,会重新调用 createHandle 函数。
  • useLayoutEffect(effect, deps)返回:,与 useEffect() 类似,但是它会在浏览器 layout 和 paint 之前执行,因此可以在渲染前同步读取 DOM 布局和触发重渲染。

    • 参数
    • effect() => void | Function,用于执行副作用操作,可以返回一个清除函数。
    • depsarray,包含所有影响 effect 函数的值,当这些值发生变化时,会重新调用 effect 函数。
  • useId(prefix?)返回:id,用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook

    • 参数
    • prefixstring,用于在生成的 id 前添加前缀。如果不传入 prefix,则默认为“id”。
    • 返回
    • idstring,生成的唯一标识符
  • useTransition({timeoutMs})返回:[isPending, startTransition],用于在渲染期间对异步更新进行控制。告诉 react 对于某部分任务的更新优先级较低,可以稍后进行更新。

    • 参数
    • timeoutMsnumber,表示等待异步更新的最长时间,超过这个时间后,异步更新将被强制中止。
    • 返回
    • isPendingboolean,,表示当前是否处于异步更新的等待中。
    • startTransition(callback) => void,用于触发异步更新
  • useDeferredValue(value, config)返回:deferredValue,接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后

    • 参数

    • value:``,当前状态的值

    • config:``,配置项

      • timeoutMs:``,定义延迟多少毫秒后开始更新状态的时间,默认为 500 毫秒。
      • equals:``,定义判断两个值相等的回调函数,如果该函数返回 true,React 会认为两个值相等,不再更新状态。
    • 返回

    • deferredValue:``,是一个延迟更新的值

React Redux

  • useSelector(selectorFn, equalityFn?)返回:selectedState,用于从 Redux store 中获取 state。
    • 参数
    • selectorFn(state) => selectedState,接收整个 Redux store 的 state 作为参数,返回需要获取的 state。
    • equalityFn?Function,比较返回的两个对象是否相等来决定是否组件重新渲染
    • 返回
    • selectedStateState,Redux store 中的 state
  • useDispatch()返回:dispatch,用于获取 dispatch 函数,从而可以向 Redux store 发送 action。
    • 返回
    • dispatch(action) => void,dispatch 函数,可以用来发送 action
  • useStore()返回:store,用于获取 Redux store 对象。它返回整个 Redux store 对象,可以用来获取、设置 state,以及订阅 state 的变化。
    • 返回
    • storeObject,整个 Redux store 对象

认识 Hooks

为什么需要 Hook?

Hook 是 React 16.8 的新增特性,它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性(比如生命周期)。

我们先来思考一下class 组件相对于函数式组件有什么优势?比较常见的是下面的优势:

  • class 组件可以定义自己的state,用来保存组件自己内部的状态

    函数式组件不可以,因为函数每次调用都会产生新的临时变量;

  • class 组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;比如在 componentDidMount 中发送网络请求,并且该生命周期函数只会执行一次

    函数式组件在学习hooks 之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求

  • class 组件可以在状态改变时只会重新执行 render 函数以及我们希望重新调用的生命周期函数componentDidUpdate等;

    函数式组件在重新渲染时整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;

所以,在 Hook 出现之前,对于上面这些情况我们通常都会编写 class 组件。

类组件修改状态

js
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      count: 100
    }
  }
  render() {
    const { count } = this.state
    return (
      <div>
        <div>App Counter: {count}</div>
+        <button onClick={e => this.setState({ count: count + 1 })}> +1 </button>
      </div>
    )
  }
}

函数组件的缺点

  • 组件不会被重新渲染:修改 message 后,组件不知道要重新渲染
  • 如果页面重新渲染:函数会被重新执行,第二次执行时,会重新给 message 赋值为 'Hello World'
  • 也没有生命周期函数
js
const App = memo(() => {
+  let message = 'Hello World'
  return (
    <div>
      <div>App Counter: {message}</div>
+      <button onClick={e => message = '你好,世界'}> 修改msg </button>
    </div>
  )
})

说明:点击“修改 msg” 并不能重新渲染页面

Class 组件存在的问题

1、复杂组件变得难以理解:

  • 我们在最初编写一个 class 组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的 class 组件会变得越来越复杂;

  • 比如 componentDidMount 中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount 中移除);

  • 而对于这样的 class 实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;

2、难以理解的 class:

  • 很多人发现学习 ES6 的 class 是学习 React 的一个障碍。

  • 比如在 class 中,我们必须搞清楚this 的指向到底是谁,所以需要花很多的精力去学习 this;

  • 虽然我认为前端开发人员必须掌握 this,但是依然处理起来非常麻烦;

3、组件复用状态很难

  • 在前面为了一些状态的复用我们需要通过高阶组件;

  • 像我们之前学习的 redux 中 connect 或者 react-router 中的 withRouter,这些高阶组件设计的目的就是为了状态的复用;

  • 或者类似于 Provider、Consumer 来共享一些状态,但是多次使用 Consumer 时,我们的代码就会存在很多嵌套

  • 这些代码让我们不管是编写和设计上来说,都变得非常困难;

Hook 的出现

Hook 的出现,可以解决上面提到的这些问题;

简单总结一下 hooks:

  • 它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性

  • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;

Hook 的使用场景:

  • Hook 的出现基本可以代替我们之前所有使用 class 组件的地方

  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为 Hooks,因为它完全向下兼容,你可以渐进式的来使用它;

  • Hook只能函数组件中使用,不能类组件,或者函数组件之外的地方使用;

在我们继续之前,请记住 Hook 是:

  • 完全可选的**:**你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。

  • 100% 向后兼容的**:**Hook 不包含任何破坏性改动。

  • 现在可用**:**Hook 已发布于 v16.8.0。

Class 组件和 Functional 组件对比

image-20230404115236252

计数器案例对比

我们通过一个计数器案例,来对比一下 class 组件和函数式组件结合 hooks 的对比:

类组件

js
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      count: 100
    }
  }
  render() {
    const { count } = this.state
    return (
      <div>
+        <div>App Counter: {count}</div>
+        <button onClick={e => this.setState({ count: count + 1 })}> +1 </button>
      </div>
    )
  }
}

函数组件

js
const App = memo(() => {
  // 1. 通过useState定义count, setCount
+  const [count, setCount] = useState(100)

  return (
    <div>
      {/* 2. 显示count */}
+      <div>App Counter: {count}</div>
      {/* 3. 修改count */}
+      <button onClick={e => setCount(count + 1)}> +1 </button>
      <button></button>
    </div>
  )
})

你会发现上面的代码差异非常大:

  • 函数式组件结合 hooks 让整个代码变得非常简洁

  • 并且再也不用考虑 this 相关的问题 ;

内置 Hook

useState()

  • useState(initialState?)返回:[state, setState],用于在函数组件中管理状态(state)
    • 参数
    • initialState?any,状态的初始值,不设置则为 undefined
    • 返回
    • state:``,当前状态值,第一次调用为初始化值
    • setStateFunction,用于更新状态值的函数。当使用这个函数设置新的状态时,React 会根据新的状态重新渲染组件。

useState 解析

那么我们来研究一下核心的一段代码代表什么意思:

  • useState 来自 react,需要从 react 中导入,它是一个 hook;

    • 参数:初始化值,如果不设置为 undefined;

    • 返回值:数组,包含两个元素;

      • 元素一:当前状态的值(第一次调用为初始化值);
      • 元素二:设置状态值的函数;
  • 点击 button 按钮后,会完成两件事情:

    • 调用 setCount设置一个新的值
    • 组件重新渲染,并且根据新的值返回 DOM 结构;

相信通过上面的一个简单案例,你已经会喜欢上 Hook 的使用了。

  • Hook 就是 JavaScript 函数,这个函数可以帮助你 钩入(hook into) React State 以及生命周期等特性;

但是使用它们会有两个额外的规则

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

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

image-20230418143429904

Tip:

  • Hook 指的类似于 useState、useEffect 这样的函数
  • Hooks 是对这类函数的统称;

认识 useState

State Hook的 API 就是 useState,我们在前面已经进行了学习:

  • useState会帮助我们定义一个 state 变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。

    • 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
  • useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为 undefined)。

  • useState的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。

FAQ:为什么叫 useState 而不叫 createState?

  • “create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。

  • 在下一次重新渲染时,useState 返回给我们当前的 state。

  • 如果每次都创建新的变量,它就不是 “state”了。

  • 这也是 Hook 的名字总是以 use 开头的一个原因。

当然,我们也可以在一个组件中定义多个变量和复杂变量(数组、对象)

useEffect()

  • useEffect(callback, dependencies?)返回:,用于在函数组件中执行副作用操作,例如访问 API、订阅事件、设置定时器等,可以模拟生命周期
    • 参数
    • callback() => void | callbackFn,要执行的副作用代码,可以是异步的或同步的
      • 返回
      • EffectCallback,在useEffect()函数中返回一个函数,这个函数将在组件卸载时执行。
    • dependencies?[],包含了与回调函数有关的依赖项,当这些依赖项发生变化时,才会重新执行回调函数。如果该数组为空,则仅在组件挂载和卸载时运行一次副作用代码。

基本使用

目前我们已经通过 hook 在函数式组件中定义 state,那么类似于生命周期这些呢?

  • Effect Hook 可以让你来完成一些类似于 class 中生命周期的功能

  • 事实上,类似于网络请求手动更新 DOM、一些事件的监听,都是 React 更新 DOM 的一些副作用(Side Effects);

  • 所以对于完成这些功能的 Hook 被称之为 Effect Hook;

假如我们现在有一个需求:页面的 title 总是显示 counter 的数字,分别使用 class 组件和 Hook 实现:

类组件

js
export class App extends PureComponent {
  componentDidMount() {
    // 1. 首次渲染时在title显示
+    document.title = this.state.count
  }
  componentDidUpdate() {
    // 2. 每次更新时在title显示
+    document.title = this.state.count
  }
  constructor() {
    super()
    this.state = {
      count: 100
    }
  }
  render() {
    const { count } = this.state
    return (
      <div>
        <div>Counter: {count}</div>
        <button onClick={e => this.setState({ count: count + 1 })}> +1 </button>
      </div>
    )
  }
}

Hook

js
const App = memo(() => {
  const [ count, setCount ] = useState(200)

  // 1. 在title显示count
  // 当前传入的函数会在组件被渲染后自动执行
+  useEffect(() => {
+    document.title = count
+  },[count])

  return (
    <div>
      <div>计数:{count}</div>
      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

useEffect 的解析:

  • 通过 useEffect 的 Hook,可以告诉 React 需要在渲染后执行某些操作

  • useEffect 要求我们传入一个回调函数,在 React 执行完更新 DOM 操作之后,就会回调这个函数

  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;

清除机制-返回回调函数

在 class 组件的编写过程中,某些副作用的代码,我们需要在 componentWillUnmount 中进行清除

  • 比如我们之前的事件总线或 Redux 中手动调用subscribe

  • 都需要在 componentWillUnmount 有对应的取消订阅;

  • Effect Hook 通过什么方式来模拟 componentWillUnmount 呢?

useEffect 传入的回调函数 A 本身可以有一个返回值,这个返回值是另外一个回调函数 B:

js
type EffectCallback = () => void | (() => void | undefined)

为什么要在 effect 中返回一个函数?

  • 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;

  • 如此可以将添加和移除订阅的逻辑放在一起

  • 它们都属于 effect 的一部分;

React 何时清除 effect?

  • React 会在组件更新卸载的时候执行清除操作

  • 正如之前学到的,effect 在每次渲染的时候都会执行;

js
// useEffect-清除机制
const App = memo(() => {
  const [ count, setCount ] = useState(100)

  useEffect(() => {
    function clickHandle() {
      console.log('触发点击事件~')
    }
+    document.addEventListener('click', clickHandle)

+    return () => {
+      document.removeEventListener('click', clickHandle)
+    }
  })
  return (
    <div>
      <div>App</div>
      <button onClick={e => setCount(count + 1)}>监听点击</button>
    </div>
  )
})

逻辑分离-多个 Effect 使用

使用 Hook 的其中一个目的就是解决 class 中生命周期经常将很多的逻辑放在一起的问题:

  • 比如网络请求、事件监听、手动修改 DOM,这些往往都会放在 componentDidMount 中;

使用 Effect Hook,我们可以将它们分离到不同的 useEffect 中:

  • 代码不再给出

Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:

  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;

注意:

  • useEffect()函数的执行是异步的,它不会阻塞组件的渲染。
  • 如果有多个useEffect()函数,它们的执行顺序是不确定的,因此应该避免在多个useEffect()函数中使用相同的依赖项,以免造成不可预期的结果。
js
const App = memo(() => {
  const [count, setCount] =
    useState(100) +
    // 执行多个useEffect
    useEffect(() => {
      document.title = count
      console.log('执行:title显示count')
    }) +
    useEffect(() => {
      console.log('执行:监听redux store')
    }) +
    useEffect(() => {
      console.log('执行:监听事件监听')
    })

  return (
    <div>
      <div>Counter: {count}</div>
      <button onClick={(e) => setCount(count + 1)}> +1 </button>
    </div>
  )
})

性能优化

默认情况下,useEffect 的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

  • 某些代码我们只是希望执行一次即可,类似于 componentDidMount 和 componentWillUnmount 中完成的事情;(比如网络请求、订阅和取消订阅);

  • 另外,多次执行也会导致一定的性能问题;

我们如何决定 useEffect 在什么时候应该执行和什么时候不应该执行呢?

  • useEffect 实际上有两个参数:

  • 参数一:执行的回调函数;

  • 参数二:该 useEffect 在哪些 state 发生变化时,才重新执行;(受谁的影响)

但是,如果一个函数我们希望依赖任何的内容时,也可以传入一个空的数组[]

  • 那么这里的两个回调函数分别对应的就是componentDidMountcomponentWillUnmount生命周期函数了;

示例:性能优化

  • 传入空数组,useEffect 只在组件挂载和卸载时执行
  • 传入依赖的变量,useEffect 会在变量改变时重新执行
  • 不传入第二个参数,useEffect 会在组件每次更新时重新执行
js
const App = memo(() => {
  const [ count, setCount ] = useState(100)
  const [ msg, setMsg ] = useState('Hi')

  // 1. 不传入第二个参数,useEffect会在组件每次更新时重新执行
  useEffect(() => {
    console.log('不传入第二个参数')
+  })

  // 2. 传入空数组,useeffect会在组件挂载和卸载时执行
  useEffect(() => {
    console.log('传入空数组,useeffect会在组件挂载和卸载时执行')
 +  },[])

  // 3. 传入依赖变量,useEffect会在变量改变时重新执行
  useEffect(() => {
    console.log('传入依赖变量,useEffect会在变量改变时重新执行')
 + },[count])

  return (
    <div>
      <div>计数:{count}</div>
      <button onClick={e => setCount(count + 1)}> +1 </button>
      <button onClick={e => setMsg('你好')}> 修改msg </button>
    </div>
  )
})

useContext()

useContext(MyContext)返回:value,可以实现组件之间的数据共享。让你在组件树中传递数据,而不需要手动的将 props 一级一级地传递下去。

  • 参数
  • MyContextContext,一个 Context 对象,它可以通过 React.createContext()来创建。
  • 返回
  • value:``,返回该 Context 对象的当前值。

在之前的开发中,我们要在组件中使用共享的 Context 有两种方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取this.context

  • 多个 Context 或者在函数式组件中通过 MyContext.Consumer 方式共享 context;

但是多个 Context 共享时的方式会存在大量的嵌套

  • Context Hook 允许我们通过 Hook 来直接获取某个 Context 的值;

image-20230404115713865

注意事项:

  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。

基本使用

1、创建 Context

js
import { createContext } from 'react'
const UserContext = createContext()
export default UserContext

2、提供待共享的 value

js
const App = memo(() => {
  return (
    <div>
      <h3>App</h3>+{' '}
      <UserContext.Provider value={{ name: 'Tom', age: 20, gender: 'male' }}>
        + <Home />+{' '}
      </UserContext.Provider>
    </div>
  )
})

3、在子孙组件通过 useContext 获取 Context 值

js
const HomeBanner = memo(() => {
+  const user = useContext(UserContext)
  return (
    <div>
      <h3>HomeBanner</h3>
      <div className="show">姓名:{user.name}</div>
      <div className="show">年龄:{user.age}</div>
      <div className="show">性别:{user.gender}</div>
    </div>
  )
})

PS、之前获取 Context 的方法

  • 通过 <XxxContext.Consumer> 获取 Context 值

    js
    const HomeProduct = memo(() => {
      return (
        <div>
          <h3>HomeProduct</h3>
    +      <UserContext.Consumer>
            {
    +          value => {
                return (
                  <>
                    <div>名称:{value.name}</div>
                    <div>性别:{value.gender}</div>
                    <div>年龄:{value.age}</div>
                  </>
                )
              }
            }
    +      </UserContext.Consumer>
        </div>
      )
    })
  • 通过 ClassName.contextType = XxxContext 获取 Context 值

    js
      export class HomeList extends PureComponent {
        render() {
    +      const user = this.context
          console.log(user)
          return (
            <>
              <h3>HomeList</h3>
              <div>名称:{user.name}</div>
              <div>性别:{user.gender}</div>
              <div>年龄:{user.age}</div>
            </>
          )
        }
      }
    +  HomeList.contextType = UserContext

useReducer()

  • useReducer(reducer, initialState)返回:[state, dispatch],提供了一种可预测且可测试的方式来处理状态更新逻辑。
    • 参数
    • reducer(state, action) => void,用于根据当前状态和传递的 action 返回新的状态
    • initialStateany,状态的初始值
    • 返回
    • state:``,当前状态
    • dispatch(action) => void,用来分发 action 来触发状态更新

很多人看到 useReducer 的第一反应应该是 redux 的某个替代品,其实并不是。

useReducer仅仅是useState 的一种替代方案

  • 在某些场景下,如果 state 的处理逻辑比较复杂,我们可以通过 useReducer 来对其进行拆分;

    image-20230418172938713

  • 或者这次修改的 state 需要依赖之前的 state 时,也可以使用;

示例:基础使用

js
const App = memo(() => {
+  function counterReducer(state, action) {
    switch(action.type) {
      case 'add_number':
        return { ...state, counter: state.counter + action.payload }
      case 'sub_number':
        return { ...state, counter: state.counter - action.payload }
      case 'add_friend':
        return { ...state, friend: [ ...state.friend, action.payload ] }
      default:
        return state
    }
  }

+  const [state, dispatch] = useReducer(counterReducer, { counter: 100, friend: ['李雷', '韩梅梅'] })
  return (
    <div>
      <h3>App</h3>
      <div className="show">
        <div>Counter: {state.counter}</div>
+        <button onClick={e => dispatch({type: 'add_number', payload: 10})}> +10 </button>
+        <button onClick={e => dispatch({type: 'sub_number', payload: 10})}> -10 </button>
        <br />
        <div>朋友:</div>
        <ul>
          {
            state.friend.map((item, index) => {
              return <li key={index}>{item}</li>
            })
          }
        </ul>
+        <button onClick={e => dispatch({type: 'add_friend', payload: '张飞'})}>添加朋友</button>
      </div>
    </div>
  )
})

数据是不会共享的,它们只是使用了相同的 counterReducer 的函数而已。

所以,useReducer 只是 useState 的一种替代品,并不能替代 Redux。

useCallback()

  • useCallback(callback, deps?)返回:memoizedCallback,用于返回一个 memoized(记忆化)的回调函数。可用于性能优化
    • 参数
    • callbackfunction,要记忆化的回调函数
    • deps?array,是一个依赖数组,当依赖数组中的任意一个值发生变化时,callback就会重新生成。
    • 返回
    • memoizedCallbackfunction,memoized (记忆化)的回调函数

useCallback 实际的目的是为了进行性能的优化。

如何进行性能的优化呢?

  • useCallback 会返回一个函数的 memoized (记忆的) 值;

  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

image-20230404115815740

案例

  • 案例一:使用 useCallback 和不使用 useCallback 定义一个函数是否会带来性能的优化;
  • 案例二:使用 useCallback 和不使用 useCallback 定义一个函数传递给子组件是否会带来性能的优化;

通常使用 useCallback 的目的不希望子组件进行多次渲染,并不是为了函数进行缓存

useCallback 性能优化的点:

  • 1、当需要将一个函数传递给子组件时,最好用 useCallback 进行优化,将优化之后的函数,传递给子组件

闭包陷阱

说明:bar1,bar2 是由函数 foo 内部返回的函数,该函数中 count 的值是在定义的时刻就固定的

js
// 演示闭包陷阱
function foo(count) {
  return function () {
    console.log(count + 1)
  }
}
const bar1 = foo(100)
const bar2 = foo(200) + bar1() // 101
bar2() // 201

示例:防止子组件被重复渲染

  • 普通函数:子组件被重新渲染
  • 缓存函数-不传参数:子组件被重新渲染
  • 缓存函数-传递空数组:子组件没有被重新渲染
  • 缓存函数-传递依赖变量:子组件被重新渲染
  • 缓存函数-传递依赖变量,但修改另一个变量:子组件没有被重新渲染
js
const App = memo(() => {
  const [count, setCount] = useState(100)
  const [msg, setMsg] = useState('Hi')

  // 1. 普通函数:子组件被重新渲染
  const increment = () => {
    setCount(count + 1)
    console.log('执行普通increment函数~')
  }

  // 2. 缓存函数-不传参数:子组件被重新渲染
  const increment = useCallback(() => {
    setCount(count + 1)
    console.log('执行-不传参数的-缓存increment函数~~')
  })

  // 3. 缓存函数-传递空数组:子组件没有被重新渲染
  const increment = useCallback(() => {
    setCount(count + 1)
    console.log('执行-传递空数组-缓存increment函数~~')
  }, [])

  // 4. 缓存函数-传递依赖变量:子组件被重新渲染
  const increment = useCallback(() => {
    setCount(count + 1)
    console.log('执行-传递依赖变量-缓存increment函数~~')
  }, [count])

  // 5. 缓存函数-传递依赖变量,但修改另一个变量:子组件没有被重新渲染
  const increment = useCallback(() => {
    setCount(count + 1)
    console.log('执行-传递依赖变量-缓存increment函数~~')
  }, [count])

  return (
    <div>
      <h3>App Counter: {count}</h3>
      <h3>App Msg: {msg}</h3>
      <button onClick={increment}> +1 </button>
      <button onClick={(e) => setMsg('你好')}> 修改msg </button>

      <Home increment={increment} />
    </div>
  )
})

示例:性能优化-count 值变化时子组件依然不会重新渲染

说明:

  • 通过传递 useCallback 的 deps 参数空数组[],保证 foo 函数每次渲染都是同一个函数
  • 既然 foo 函数每次父组件渲染时保持不变,那么子组件就不会被重新渲染
  • 通过对象变量 countRef 保存依赖的数据,保证在一个固定的函数中每次可以取到 count 的最新值,因为对象是引用值
js
const App = memo(() => {
  const [count, setCount] = useState(100)
  const countRef = useRef()
  countRef.current = count
  const increment = useCallback(function foo() {
    setCount(countRef.current + 1)
    console.log('执行increment函数~')
  }, [])

  return (
    <div>
      <h3>App Counter: {count}</h3>
      <button onClick={increment}> +1 </button>

      <Home increment={increment} />
    </div>
  )
})

useMemo()

  • useMemo(callback, deps)返回:memoizedValue,用于缓存计算结果,以便在依赖项未更改时避免不必要的重新计算。
    • 参数
    • callbackFunction,计算逻辑的回调函数
    • depsArray,是一个依赖数组,当依赖项数组中的任何一个值发生变化时,useMemo()会重新计算并返回新的计算结果。如果依赖项数组为空,则该函数仅在组件首次渲染时被计算。
    • 返回
    • memoizedValueany,memoized(记忆化)的计算值

useMemo 实际的目的也是为了进行性能的优化

如何进行性能的优化呢?

  • useMemo 返回的也是一个 memoized(记忆的) 值;

  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

image-20230404115845968

案例:

  • 案例一:进行大量的计算操作,无须每次渲染时都重新计算;

  • 案例二:对子组件传递相同内容的对象时,使用 useMemo 进行性能的优化

useMemo 和 useCallback 的对比

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

示例:进行大量的计算操作,无须每次渲染时都重新计算

js
const App = memo(() => {
  const [count, setCount] = useState(100)

  function calNum(num) {
    console.log('执行计算calNum')
    let total = 0
    for(let i=1; i<=num; i++) {
      total += i
    }
    return total
  }

  // 1. 普通函数:每次重新渲染,都执行一次计算
  const res = calNum(count)

  // 2. 缓存计算结果:依赖count变化时重新执行计算
+  const res = useMemo(() => {
+    return calNum(count)
+  },[count])

  return (
    <div>
      <h3>App res1: {res} - {count}</h3>

      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

示例:对子组件传递相同内容的对象时,使用 useMemo 对子组件渲染进行优化

对子组件传递相同内容的对象时,父组件重新渲染时,子组件也会重新渲染

原因: 父组件重新渲染时,每次都会定义一个新的对象,2 次定义的对象不是同一个对象

*优化:*使用 useMemo 进行性能的优化

js
const App = memo(() => {
  const [ count, setCount ] = useState(100)


  // 1. 传递一个值类型到子组件,value不变时子组件不会重新渲染
  const value = 100

  // 2. 传递一个引用类型到子组件,info每次都会创建一个新对象,子组件会被重新渲染
  const info = { name: 'Jack', age: 20 }

  // 3. 通过useMemo返回一个不变的对象,子组件不会被重新渲染
+  const obj = useMemo(() => ({ name:'Tom', age: 33 }), [])

  return (
    <div>
      <h3>App</h3>
      <button onClick={e => setCount(count + 1)}> +1 </button>
      <hr />
      <Home value={value}/>
      <Profile info={info}/>
+      <About obj={obj} />
    </div>
  )
})

useRef()

  • useRef(initialValue?)返回:refContainer,用于创建一个可变的引用,它返回一个对象,对象中有一个 current 属性,该属性的值可以在组件的整个生命周期中保持不变,并可以被读取和修改。
    • 参数
    • initialValue?any,表示 refContainer.current 的初始值
    • 返回
    • refContainer{current},refContainer 对象中有一个 current 属性,该属性的值可以在组件的整个生命周期中保持不变,并可以被读取和修改。

useRef 返回一个 ref 对象,返回的 ref 对象在组件的整个生命周期保持不变。

image-20230404115906565

ref 的 2 种用法:

用法一绑定 DOM(或者组件,但是需要是 class 组件)元素;

示例

js
const App = memo(() => {
+  const titleRef = useRef()

  const getDOM = () => {
    console.log(titleRef) // {current: h3}
+    console.log(titleRef.current) // <h3>App</h3>
    console.log(titleRef.current.innerHTML) // App
  }

  return (
    <div>
+      <h3 ref={titleRef}>App</h3>
      <button onClick={getDOM}>获取DOM</button>
    </div>
  )
})

用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;

验证 useRef 的返回值在组件重新渲染时始终是同一个对象

js
let obj = null
const App = memo(() => {
  const [count, setCount] = useState(100)

  // 判断2次渲染时的ref对象是否是同一个
+  const nameRef = useRef('Tom')
+  console.log('obj === nameRef? ', obj === nameRef) // obj === nameRef?  true
+  obj = nameRef

  return (
    <div>
      <h3>App</h3>
      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

useImperativeHandle()

  • useImperativeHandle(ref, createHandle, deps?)返回:,用于向父组件暴露子组件的实例方法
    • 参数
    • refObject | (cpn) => void,如果 ref 参数是函数,则函数的第一个参数为子组件的实例对象,用于设置 ref 的值。
    • createHandle() => {method,...},用于创建一个对象,该对象包含子组件需要向父组件暴露的方法。
    • deps?Array,包含所有影响 createHandle 函数的值,当这些值发生变化时,会重新调用 createHandle 函数。

useImperativeHandle 并不是特别好理解,我们一点点来学习。

我们先来回顾一下 ref 和 forwardRef 结合使用:

  • 通过 forwardRef 可以将 ref 转发到子组件;

  • 子组件拿到父组件中创建的 ref,绑定到自己的某一个元素中;

示例:ref 绑定子组件中的某个元素

父组件

js
const App = memo(() => {
+  const homeRef = useRef()

  const getCpnElement = () => {
+    console.log(homeRef.current)
+    homeRef.current.focus()
+    homeRef.current.value = '输入姓名'
  }
  return (
    <div>
      <h3>App</h3>
      <button onClick={e => getCpnElement()}>获取子组件元素</button>
      <hr />
+      <Home ref={homeRef}/>
    </div>
  )
})

子组件

js
+ const Home = memo(forwardRef((props, ref) => {
  return (
    <div>
      <div className="title">Home</div>
+      <input type="text" ref={ref}/>
    </div>
  )
}))

forwardRef 的做法本身没有什么问题,但是我们是将子组件的 DOM 直接暴露给了父组件:

  • 直接暴露给父组件带来的问题是某些情况的不可控;

  • 父组件可以拿到 DOM 后进行任意的操作;

  • 但是,事实上在上面的案例中,我们只是希望父组件可以操作的 focus,其他并不希望它随意操作

通过 useImperativeHandle 可以值暴露固定的操作:

  • 通过 useImperativeHandle 的 Hook,将传入的 ref 和 useImperativeHandle 第二个参数返回的对象绑定到了一起;

  • 所以在父组件中,使用 inputRef.current 时,实际上使用的是返回的对象;

  • 比如我调用了 focus 函数,甚至可以调用 printHello 函数;

示例:使用 useImperativeHandle 限制父组件只能操作子组件暴露的指定方法

父组件

image-20230419172535367

子组件

  • 父组件传递的 ref 别绑定到了 useImperativeHandle 上
  • 子组件中通过 useImperativeHandle 定义要向父组件暴露的函数
  • 在子组件中另外定义 inputRef 获取 DOM 元素
js
const Profile = memo(forwardRef((props, ref) => {
+  const inputRef = useRef()
  // 定义要向父组件暴露的函数
+  useImperativeHandle(ref, () => {
+    return {
+      focus() {
+        inputRef.current.focus()
+      }
+    }
+  })
  return (
    <div>
      <h3>Profile</h3>
+      <input type="text" ref={inputRef}/>
    </div>
  )
}))

useLayoutEffect()

  • useLayoutEffect(effect, deps)返回:,与 useEffect() 类似,但是它会在浏览器 layout 和 paint 之前执行,因此可以在渲染前同步读取 DOM 布局和触发重渲染。
    • 参数
    • effect() => void | Function,用于执行副作用操作,可以返回一个清除函数。
    • depsArray,包含所有影响 effect 函数的值,当这些值发生变化时,会重新调用 effect 函数。

useLayoutEffect 看起来和 useEffect 非常的相似,事实上他们也只有一点区别而已:

  • useEffect会在渲染的内容更新到 DOM 上之后执行,不会阻塞DOM 的更新;

  • useLayoutEffect会在渲染的内容更新到 DOM 上之前执行,会阻塞DOM 的更新;

如果我们希望在某些操作发生之后再更新 DOM,那么应该将这个操作放到useLayoutEffect

image-20230404120023929

官方更推荐使用useEffect而不是 useLayoutEffect。

示例: useEffect 和 useLayoutEffect 的对比

useEffect: 先显示了 0 后,再在 useEffect 中修改为随机值,会有闪烁现象

js
const App = memo(() => {
  const [count, setCount] = useState(100)

+  useEffect(() => {
+    if(count > 100) {
+      setCount(1)
+    }
+  },[count])
   return (
    <div>
      <h3>App Counter: {count}</h3>
      <button onClick={e => setCount(count + 1)}> +1 </button>
    </div>
  )
})

useLayoutEffect: 没有闪烁现象

js
const App = memo(() => {
  const [count, setCount] = useState(100)

+  useLayoutEffect(() => {
+    if(count > 100) {
+      setCount(1)
+    }
+  }, [count])

  return (
    <div>
      <h3>App Counter: {count}</h3>
      <button onClick={(e) => setCount(count + 1)}> +1 </button>
    </div>
  )
})

自定义 Hooks

自定义 Hook

自定义 Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算 React 的特性。

自定义 Hook 必须以use开头

需求:所有的组件在创建和销毁时都进行打印

  • 组件被创建:打印“组件被创建了";

  • 组件被销毁:打印"组件被销毁了";

image-20230404120039955

抽取代码

1、定义 Hook

js
import { useEffect } from 'react'

function useLifeLog(cpnName) {
  useEffect(() => {
    console.log(cpnName + ' 组件被创建~')

    return () => {
      console.log(cpnName + ' 组件被销毁~')
    }
  }, [])
}

export default useLifeLog

2、使用自定义 Hook

js
// Home.jsx
const Home = memo(() => {
  // 使用自定义Hook
  ;+useLifeLog('Home')

  return <div>Home</div>
})

自定义 Hook 练习

需求一:Context 的共享

1、创建 Context

js
import { createContext } from 'react'

const UserContext = createContext()
const TokenContext = createContext()

export { UserContext, TokenContext }

2、挂载 Context 到组件树

js
<UserContext.Provider value={{ name: 'Tom', age: 19 }}>
  <TokenContext.Provider value={{ token: 'token~' }}>
    <Home />
  </TokenContext.Provider>
</UserContext.Provider>

3、自定义 Hook

js
import { useContext } from 'react'
import { TokenContext, UserContext } from '../context'

function useUserInfo() {
  const user = useContext(UserContext)
  const token = useContext(TokenContext)

  return [user, token]
}

export default useUserInfo

4、使用 hook

js
const HomeBanner = memo(() => {
+  const [ user, token ] = useUserInfo()
  return (
    <div>
      <div>HomeBanner</div>
      <div>姓名:{user.name}</div>
      <div>年龄:{user.age}</div>
      <div>Token:{token.token}</div>
    </div>
  )
})

需求二:获取滚动位置

js
import { useEffect, useState } from 'react'

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 })

  useEffect(() => {
    // 事件监听函数
    function scrollHandler() {
      setScrollPosition({
        x: window.scrollX,
        y: window.scrollY
      })
    }
    // 监听滚动事件
    document.addEventListener('scroll', scrollHandler)
    return () => {
      // 取消监听滚动事件
      document.removeEventListener('scroll', scrollHandler)
    }
  }, [])
  return scrollPosition
}

export default useScrollPosition

需求三:localStorage 数据存储

1、定义 hook

通过 key,直接从 localStorage 中获取一个数据

js
import { useEffect, useState } from 'react'

function useLocalStorage(key) {
  const [data, setData] = useState(() => {
    return JSON.parse(localStorage.getItem(key))
  })

  // 保存到本地存储是副作用
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data))
  }, [key, data])

  return [data, setData]
}

export default useLocalStorage

2、使用 hook

js
const App = memo(() => {
+  const [token, setToken] = useLocalStorage('token')
+  const [user, setUser] = useLocalStorage('user')
  return (
    <div>
+      <div>token: {token}</div>
+      <button onClick={e => setToken('令牌')}>设置token</button>

      <hr />
+      <div>name: {user?.name}, age: {user?.age}</div>
+      <button onClick={e => setUser({ name: '张飞', age: 48 })}>设置user</button>

    </div>
  )
})

redux hooks

  • useSelector(selectorFn, equalityFn?)返回:selectedState,用于从 Redux store 中获取 state。
    • 参数
    • selectorFn(state) => selectedState,接收整个 Redux store 的 state 作为参数,返回需要获取的 state。
    • equalityFn?Function,比较返回的两个对象是否相等来决定是否组件重新渲染
    • 返回
    • selectedStateState,Redux store 中的 state
  • useDispatch()返回:dispatch,用于获取 dispatch 函数,从而可以向 Redux store 发送 action。
    • 返回
    • dispatch(action) => void,dispatch 函数,可以用来发送 action
  • useStore()返回:store,用于获取 Redux store 对象。它返回整个 Redux store 对象,可以用来获取、设置 state,以及订阅 state 的变化。
    • 返回
    • storeObject,整个 Redux store 对象

之前的 redux 开发中,为了让组件和 redux 结合起来,我们使用了 react-redux 中的connect

  • 但是这种方式必须使用高阶函数结合返回的高阶组件;

  • 并且必须编写:mapStateToProps 和 mapDispatchToProps 映射的函数;

Redux7.1开始,提供了Hook的方式,我们再也不需要编写 connect 以及对应的映射函数了

useSelector作用将 state 映射到组件中:**

  • 参数一:将 state 映射到需要的数据中;

  • 参数二:可以进行比较来决定是否组件重新渲染;(后续讲解)

useSelector 默认会比较我们返回的两个对象是否相等;

  • 如何比较呢? const refEquality = (a, b) => a === b;

  • 也就是我们必须返回两个完全相等的对象才可以不引起重新渲染;

useDispatch非常简单,就是直接获取 dispatch 函数,之后在组件中直接使用即可;**

我们还可以通过useStore获取当前的 store 对象

示例:useSelector(),useDispatch()

1、创建 reducer 片段

js
const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    count: 100,
    msg: 'hi'
  },
  reducers: {
    addNumber(state, { payload }) {
      state.count = state.count + payload
    },
    subNumber(state, { payload }) {
      state.count = state.count - payload
    },
    changeMsg(state, { payload }) {
      state.msg = payload
    }
  }
})

export const { addNumber, subNumber, changeMsg } = counterSlice.actions
export default counterSlice.reducer

2、定义 Redux store

js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter'

const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})

export default store

3、通过 Provider 提供 store

js
root.render(
  +(
    <Provider store={store}>
      <App />
    </Provider>
  )
)

4、获取 store 数据

*之前:*在组件中通过connect()获取 store 数据

image-20230420102555276

image-20230420102344030

优化: 使用 Hook 获取,修改 store 数据

在组件中通过 useSelector() 获取 store 数据

在组件中通过 useDispatch() 获取 dispatch,再通过 dispatch 派发 action 修改 store 数据

js
const App = memo(() => {
    // 获取store数据
+  const { count, msg } = useSelector((state) => ({
+    count: state.counter.count,
+    msg: state.counter.msg
+  }))
    // 获取dispatch
+  const dispatch = useDispatch()

  return (
    <div>
+      <div>App Counter: {count}</div>
+      <button onClick={e => dispatch(addNumber(1))}> +1 </button>
      <hr />
+      <div>msg: {msg}</div>
+      <button onClick={e => dispatch(changeMsg('你好'))}> 修改msg </button>
    </div>
  )
})

优化 2: 全等比较(浅层)和更新,让组件不用监听整个 store 的变化,只监听本组件用到的 store 数据是否发生变化

在 store 中增加 message

image-20230420103757142

在 Home 中展示 message,并通过打印观察是否重新渲染

image-20230420104240527

说明:由于useSelector 监听的是整个 state,当 state 中某个值改变时,就会重新渲染所在组件

在 useSelector 的第二参数中对提取的 store 数据进行浅层比较(shallowEqual)

js
  const { msg } = useSelector(state => ({
    msg: state.counter.msg
+  }), shallowEqual)

其他 Hook

useId()

  • useId(prefix?)返回:id,用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook
    • 参数
    • prefix?string,用于在生成的 id 前添加前缀。如果不传入 prefix,则默认为“id”。
    • 返回
    • idstring,生成的唯一标识符

官方的解释:useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook

概念:hydration

hydration:要想理解这个词,我们需要理解一些服务器端渲染(SSR)的概念。

概念:CSR

CSR:客户端渲染(Client Side Rendering),我们开发的 SPA 页面通常依赖的就是客户端渲染;

概念:SPA

SPA:单页面富应用(Single Page Application),所有内容都在一个单独的页面上加载,而不需要重新加载整个页面。相反,页面的内容是通过 JavaScript 动态加载的,使用户可以在不刷新整个页面的情况下浏览不同的内容。

缺点:

  • 不利于 SEO:由于所有的内容都在一个页面上,因此对于搜索引擎来说,很难对页面进行精确的索引。
  • 首屏渲染速度慢

概念:SSR

SSR:服务端渲染(Server Side Rendering),指的是页面在服务器端已经生成了完成的 HTML 页面结构,不需要浏览器通过执行 JS 代码来生成页面结构;

早期的服务端渲染包括 PHP、JSP、ASP 等方式,但是在目前前后端分离的开发模式下,前端开发人员不太可能再去学习 PHP、JSP 等技术来开发网页;

不过我们可以借助于 Node 来帮助我们执行 JavaScript 代码,提前完成页面的渲染

image-20230404120202768

SSR 同构应用

什么是同构?

  • 一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用

同构是一种 SSR 的形态,是现代 SSR 的一种表现形式。

  • 当用户发出请求时,先在服务器通过 SSR 渲染出首页的内容。

  • 但是对应的代码同样可以在客户端被执行。

  • 执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染;

image-20230404120251060

image-20230404120258297

Hydration

什么是 Hydration?这里我引入 vite-plugin-ssr 插件的官方解释。

image-20230404120311833

在进行 SSR 时,我们的页面会呈现为 HTML。

  • 但仅 HTML 不足以使页面具有交互性。例如,浏览器端 JavaScript 为零的页面不能是交互式的(没有 JavaScript 事件处理程序来响应用户操作,例如单击按钮)。

  • 为了使我们的页面具有交互性除了在 Node.js 中将页面呈现为 HTML 之外,我们的 UI 框架(Vue/React/...)还在浏览器中加载和呈现页面。(它创建页面的内部表示,然后将内部表示映射到我们在 Node.js 中呈现的 HTML 的 DOM 元素。)

这个过程称为hydration

useId 的作用

我们再来看一遍:useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。

所以我们可以得出如下结论:

  • useId 是用于 react 的同构应用开发的,前端的 SPA 页面并不需要使用它;

  • useId 可以保证应用程序在客户端和服务器端生成唯一的 ID,这样可以有效的避免通过一些手段生成的 id 不一致,造成 hydration mismatch

image-20230420123258341

useTransition()

  • useTransition({timeoutMs})返回:[isPending, startTransition],用于在渲染期间对异步更新进行控制。告诉 react 对于某部分任务的更新优先级较低,可以稍后进行更新。
    • 参数
    • timeoutMsnumber,表示等待异步更新的最长时间,超过这个时间后,异步更新将被强制中止。
    • 返回
    • isPendingboolean,,表示当前是否处于异步更新的等待中。
    • startTransition(callback) => void,用于触发异步更新

官方解释:返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。

  • 事实上官方的说法,还是让人云里雾里,不知所云。

useTransition 到底是干嘛的呢?它其实在告诉 react 对于某部分任务的更新优先级较低,可以稍后进行更新。

示例: 使用 useTransition 优化带有搜索功能的超长名单列表组件

js
const App = memo(() => {
  const fakeNames = useGenerateFakeNames()
  const [names, setNames] = useState(fakeNames)
  // 1. 使用useTrasition,返回2个参数
  const [isPending, startTransition] = useTransition()

  function searchHandle(e) {
    // 2. 调用返回的startTransition函数,延迟执行传入的回调函数
    startTransition(() => {
      const kw = e.target.value
      const filteredNames = fakeNames.filter((item) => item.includes(kw))
      setNames(filteredNames)
    })
  }

  return (
    <div>
      <div className="search">
        <span>搜索:</span>
        <input type="text" onInput={(e) => searchHandle(e)} />
      </div>
      <h3>名单:{isPending ? 'Loading...' : ''}</h3>
      <ul>
        {names.map((item, index) => {
          return <li key={index}>{item}</li>
        })}
      </ul>
    </div>
  )
})

1、生成随机名字

  • 库:faker
  • 安装:npm i @faker-js/faker -D
js
import { faker } from '@faker-js/faker'

function useGenerateFakeNames() {
  const fakeNames = []
  for (let i = 0; i < 10000; i++) {
    ;+fakeNames.push(faker.name.firstName())
  }
  return fakeNames
}

export default useGenerateFakeNames

2、展示名字列表

js
const App = memo(() => {
+  const fakeNames = useGenerateFakeNames()
+  const [ names, setNames ] = useState(fakeNames)
  return (
    <div>
      <h3>名单:</h3>
      <ul>
        {
+          names.map((item, index) => {
+            return <li key={index}>{item}</li>
+          })
        }
      </ul>
    </div>
  )
})

3、过滤包含输入字符的名字

js
const App = memo(() => {
  const fakeNames = useGenerateFakeNames()
  const [ names, setNames ] = useState(fakeNames)

+  function searchHandle(e) {
+      const kw = e.target.value
+      const filteredNames = fakeNames.filter(item => item.includes(kw))
+      setNames(filteredNames)
+  }

  return (
    <div>
      <div className="search">
        <span>搜索:</span>
+        <input type="text" onInput={e => searchHandle(e)}/>
      </div>
      <h3>名单:</h3>
      <ul>
        {
+          names.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
})

问题: 当列表数据很大时,用户操作后会有明显的延迟

4、优化: 使用useTransition将名字列表渲染的优先级降低,优先显示 input 框效果

js
const App = memo(() => {
  const fakeNames = useGenerateFakeNames()
  const [ names, setNames ] = useState(fakeNames)
  // 1. 使用useTrasition,返回2个参数
+  const [isPending, startTransition] = useTransition()

  function searchHandle(e) {
    // 2. 调用返回的startTransition函数,延迟执行传入的回调函数
+    startTransition(() => {
+      const kw = e.target.value
+      const filteredNames = fakeNames.filter(item => item.includes(kw))
+      setNames(filteredNames)
+    })
  }

  return (
    <div>
      <div className="search">
        <span>搜索:</span>
        <input type="text" onInput={e => searchHandle(e)}/>
      </div>
+      <h3>名单:{ isPending ? 'Loading...' : '' }</h3>
      <ul>
        {
          names.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
})

useDeferredValue()

  • useDeferredValue(value, config)返回:deferredValue,接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后
    • 参数
    • valueany,当前状态的值
    • configobject,配置项
      • timeoutMsnumber,定义延迟多少毫秒后开始更新状态的时间,默认为 500 毫秒。
      • equalsfunction,定义判断两个值相等的回调函数,如果该函数返回 true,React 会认为两个值相等,不再更新状态。
    • 返回
    • deferredValueany,是一个延迟更新的值

官方解释:useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后

在明白了 useTransition 之后,我们就会发现 useDeferredValue 的作用是一样的效果,可以让我们的更新延迟。

js
const App = memo(() => {
  const fakeNames = useGenerateFakeNames()
  const [ names, setNames ] = useState(fakeNames)
+  const deferedNames = useDeferredValue(names)

+  function searchHandle(e) {
+    const kw = e.target.value
+    const filteredNames = fakeNames.filter(item => item.includes(kw))
+    setNames(filteredNames)
+  }

  return (
    <div>
      <div className="search">
        <span>搜索:</span>
        <input type="text" onInput={e => searchHandle(e)}/>
      </div>
      <h3>名单:</h3>
      <ul>
        {
+          deferedNames.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
})