• 【React】React全家桶(五)React Hooks


    1 Hooks简介

    1.1 什么是Hooks?

    • Hooks 直译是 “钩子”,它并不仅是 react,甚至不仅是前端界的专用术语,而是整个行业所熟知的用语。 一般指:系统运行到某一时期时,会调用被注册到该时机的回调函数。比如: windows 系统的钩子能监听到系统的各种事件,浏览器提供的 onloadaddEventListener 能注册在浏览器各种时机被调用的方法。

    • React中,Hooks 是 React 16.8 新增的特性,是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的特殊JS函数,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。 简言之:Hooks是一系列方法,提供了在函数式组件中完成开发工作的能力。

    • 之前使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有 , Hooks 的出现可以让你在函数组件直接使用state等功能

    • React Hooks官方文档

    1.2 Hooks的优势

    • hooks 之间的状态是独立的,有自己独立的上下文,不会出现混淆状态的情况
    • 让函数组件有了状态管理
    • 解决了组件树不直观、类组件难维护、逻辑不易复用的问题
    • 避免函数重复执行的副作用

    1.3 Hooks使用场景

    • 利用 hooks 取代生命周期函数
    • 让函数组件有了状态
    • 组件辅助函数
    • 处理发送请求
    • 存取数据做好性能优化

    1.4 Hooks使用注意事项

    • 假设任何以 use 开头并紧跟着一个大写字母的函数就是一个 Hook

    • 只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用

    • 只能在 React 的函数组件中调用 Hook,不要在普通JS函数中调用

    2 Hooks API

    2.1 数据驱动更新型Hooks

    useState

    useState(数据驱动更新):给函数组件添加state状态,并进行状态的读写操作 ,函数组件通过 useState 可以让组件重新渲染,更新视图。

    useState语法:

    import React,{ useState } from 'react'
    const [xxx, setXxx] = useState(initState)
    
    • 1
    • 2

    参数initState: 第一种情况是非函数,将作为 xxx 初始化的值。 第二种情况是函数,函数的返回值作为 useState 初始化的值。

    返回值[xxx, setXxx]: 包含2个元素的数组 ,xxx是当前状态值,setXxx是修改状态值的函数

    • xxx: 提供给 UI ,作为渲染视图的数据源
    • setXxx: 改变 xxx 的异步函数 ,推动函数组件渲染的渲染函数, 要在下次重新渲染才能获取新值 ,下面是setXxx的两种写法:
      • setXxx(newValue): 参数为非函数值, 直接指定新的状态值来覆盖原来的状态值
      • setXxx((value) => {newValue}): 参数为函数, 接收原来的状态值, 返回新的状态值进行覆盖,若newValue没有使用value可以不写

    useState基础用法:

    import React, { useState } from 'react'
    const Demo = () => {
      let [number, setNumber] = useState(0)
      console.log(number, 'number')/* 这里的number能够即时更新的,这里是在重新渲染后调用的  */
      return (
        <div>
          <span>{number}</span>
          <button
            type="primary"
            onClick={() => {
              setNumber(number + 1)
              console.log(number,'number-=') /* 这里number不能够即时更新的,setNumber是异步更新  */
            }}
          >
            +1按钮
          </button>
        </div>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    注:

    • 在函数组件一次执行上下文中,state 的值是固定不变的, 当触发setXxx在当前执行上下文中获取不到最新的 state,,只有在下一次组件渲染中才能获取到 (setXxx是异步函数)
    • 如果两次setXxx 传入相同的 state 值,那么组件就不会更新

    补充:方括号的作用

    [ ]语法叫数组解构,它意味着我们同时创建了 fruit 和 setFruit 两个变量,fruit 的值为 useState 返回的第一个值,setFruit是返回的第二个值

      const [fruit, setFruit] = useState('banana');
      //等价于
      var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组
      var fruit = fruitStateVariable[0]; // 数组里的第一个值
      var setFruit = fruitStateVariable[1]; // 数组里的第二个值
    
    • 1
    • 2
    • 3
    • 4
    • 5

    useReducer

    useReducer(订阅状态,更新视图)useState的替代方案,对于复杂的state操作逻辑,嵌套的state的对象, 或者下一个 state 依赖于之前的 state 等 ,推荐使用useReducer。

    useReducer语法:

    const [xxx, setXxx] = useReducer(reducer, initState);
    
    • 1

    参数reducer:类似于 redux 中的 reducer 函数,(state, action) => newState,接收当前应用的state和触发的动作action,计算并返回最新的state ,如果返回的 state 和之前的 state ,内存指向相同,那么组件将不会更新。

    参数initState: 第一种情况是非函数,将作为state初始化的值。 第二种情况是函数,函数的返回值作为 useReducer 初始化的值。

    返回值[xxx, setXxx]: 包含2个元素的数组 ,xxx是当前状态值,setXxx是修改状态值的函数

    • xxx: 提供给 UI ,作为渲染视图的数据源
    • setXxx: 改变 xxx 的异步函数 ,用来触发reducer函数,计算对应的state,其余和useState类似

    useReducer基础用法:

    //demo.js
    import React, { useReducer } from 'react'
    import MyChildren from './children'
    const Demo = () => {
      /* number为更新后的state值,  dispatchNumbner 为当前的派发函数 */
      const [number, dispatchNumber] = useReducer((state, action) => {
        const { payload, name } = action
        /* return的值为新的state */
        switch (name) {
          case 'add':
            return state + 1
          case 'sub':
            return state - 1
          case 'reset':
            return payload
        }
        return state
      }, 0)
      return (
        <div>
          当前值:{number}
          {/* 派发更新 */}
          <Button onClick={() => dispatchNumber({ name: 'add' })}>增加</Button>
          <Button onClick={() => dispatchNumber({ name: 'sub' })}>减少</Button>
          <Button onClick={() => dispatchNumber({ name: 'reset', payload: 666 })}>
            赋值
          </Button>
          {/* 把dispatch 和 state 传递给子组件  */}
          <MyChildren dispatch={dispatchNumber} state={number} />
        </div>
      )
    }
    
    //children.js
    const MyChildren = props => {
      const { dispatch, state } = props
      return <div>MyChildren中值:{state}</div>
    }
    
    • 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

    在这里插入图片描述

    注:

    • 如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。

    2.2 状态派生与保存型Hooks

    任何刚开始写 react 组件的工程师应该都能发现,组件会一直重新渲染。比如我们在使用React开发过程中经常会遇到父组件引入子组件的情况,往往会造成子组件不必要的重复渲染,造成主线程阻塞,页面渲染卡顿,带来性能问题。 缓存组件可以优化性能 ,能够很好解决这一问题。

    memoization 含义、Hooks 逻辑、React浅比较

    什么是 memoization?

    • memoization 是一种用空间换时间的优化方法。
    • 把计算结果缓存起来,取用前需通过校验,取出后实现复用。
    • 保持返回值的引用相等。

    Hooks逻辑

    • 几乎所有有依赖数组的 hooks 都共享同一套基础逻辑。
    • 组件初次渲染时,执行一次。
    • 组件重新渲染时,通过浅比较检查依赖数组有没有变化。如果没有,不重复执行。

    React中的浅比较:

    • 浅比较使用的是 Object.is 函数,只比较对象第一层的属性和值,不是使用严格相等 === 运算符;
    • 通过浅比较,空对象和空数组是等价的;
    • 通过浅比较,以数组索引为 key 和数组值为value的对象是等价的,比如:{ 0: 2, 1: 3 } 等价于 [2, 3]
    • 由于通过Object.is比较的+0-0Number.NaNNaN是不相等的,所以在复杂结构中比较时,这也是适用的;
    • 虽然{ } 和 [ ] 浅比较是相等的,但是嵌套在对象中对象是不相等的,比如:{ someKey: {} }{ someKey: [] } 是不相等的。
    shallowCompare({}, {}) // => true
    shallowCompare({ a: 1, b: 2 }, { a: 1, b: 2 }) // => true
    shallowCompare({ a: 1, b: 2 }, { a: 1, c: 2 }) // => false
    shallowCompare({ a: 1, b: { 2 }}, { a: 1, b: { 2 }}) // => false
    
    • 1
    • 2
    • 3
    • 4

    React.memo高阶组件

    React.memo:将组件的渲染结果缓存,并有效复用,减少主线程的阻塞。

    React.memo语法:

    const MemoComponent = React.memo(component, Func)
    
    • 1

    参数component:自定义组件

    参数Func:一个函数,用来判断组件需不需要重新渲染。如果省略第二个参数,默认会对该组件的props进行浅比较,当 props 没有改变时,组件就不需要重新渲染

    如果 props 中存在回调函数或者多层嵌套的复杂对象,或每次父组件更新时子组件的props都会生成新的内存地址,这样浅比较无效,仅使用react.memo是无法达到目的的,需要自己写比较函数,或者搭配 useCallback 使用。

    何时使用 React.memo

    1. 检查组件是否是 Pure 的,即相同输入,相同输出。
    2. 检查组件是否经常被相同的 props 重复渲染,且导致了性能问题。
    3. 如果 props 中有回调函数,可以考虑搭配使用 useCallback 使用。

    useMemo

    useMemo(派生新状态): 可以在函数组件 render 上下文中同步执行一个函数逻辑,这个函数的返回值可以作为一个新的状态缓存起来, 同时保证了返回值的引用地址不变 。 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算缓存值 ,让组件中的函数跟随状态更新,用于进行高开销、复杂场景的计算 。

    useMemo语法:

    const memoizedValue = useMemo(create,deps)
    //useMemo(() => computeExpensiveValue(a, b), [a, b]);
    
    • 1
    • 2

    第一个参数 create:创建函数,函数的返回值作为缓存值

    第二个参数deps:依赖项数组,为当前 useMemo 的依赖项,在函数组件下一次执行的时候, 通过浅比较对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。 没有提供依赖项数组,useMemo在每次渲染时都会计算新的值 。 一般来说,所有create函数中引用的值都应该出现在依赖项数组deps中

    返回值memoizedValue:执行 create 的缓存值 。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。

    useMemo 何时使用:

    1. create 导致了页面的卡顿
    2. create 需要跟随组件渲染执行。
    3. create 本身有优化空间

    useMemo基础用法:

    • 派生新状态:
    function Scope() {
        const keeper = useKeep()
        const { cacheDispatch, cacheList, hasAliveStatus } = keeper
       
        /* 通过 useMemo 得到派生出来的新状态 contextValue  */
        const contextValue = useMemo(() => {
            return {
                cacheDispatch: cacheDispatch.bind(keeper),
                hasAliveStatus: hasAliveStatus.bind(keeper),
                cacheDestory: (payload) => cacheDispatch.call(keeper, { type: ACTION_DESTORY, payload })
            }
          
        }, [keeper])
        return <KeepaliveContext.Provider value={contextValue}>
        </KeepaliveContext.Provider>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如上通过 useMemo 得到派生出来的新状态 contextValue ,只有 keeper 变化的时候,才改变 Provider 的 value 。

    • 缓存计算结果:
    function Scope(){
        const style = useMemo(()=>{
          let computedStyle = {}
          // 经过大量的计算
          return computedStyle
        },[])
        return <div style={style} ></div>
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 缓存组件,减少子组件 rerender 次数:
    function Scope ({ children }){
       const renderChild = useMemo(()=>{ children()  },[ children ])
       return <div>{ renderChild } </div>
    }
    
    • 1
    • 2
    • 3
    • 4

    useCallback

    useCallback(保存状态)useCallback 与useMemo类似,它们接收的参数一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于useCallback 缓存的是函数本身以及它的引用地址,而不是返回值

    useCallback语法:

    const memoizedCallback = useCallback(
      () => {
        doSomething(a, b);
      },
      [a, b],
    );
    
    // 不使用 useCallback
    const handleLog = (message) => console.log(message)
    // 使用 useCallback
    const handleLogMessage = useCallback(handleLog, [message])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    把回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的缓存版本,该缓存版本的回调函数仅在某个依赖项改变时才会更新 ,跟随状态更新执行。

    注意:

    • useCallback返回的是一个函数,不再是值
    • useMemo第一次渲染时执行,缓存变量,之后只有在依赖项改变时才会更新缓存,如果依赖不更新,返回的永远是缓存的那个变量
    • useCallback 第一次渲染时执行,缓存函数,之后只有在依赖项改变时才会更新缓存 ,如果依赖不更新,返回的永远是缓存的那个函数
    • 给子组件中传递 props 的时候,如果当前组件不更新,不会触发子组件的重新渲染(最重要的用途)

    使用 useCallback 要对依赖数组做浅比较,对性能带来的负面影响,同时又提升了代码的复杂度。如果使用不当,很可能得不偿失。

    2.3 执行副作用型Hooks

    useEffect

    useEffect(异步执行副作): 在函数组件中执行副作用操作 (用于模拟类组件中的生命周期钩子) , 在执行 DOM 更新之后调用 。

    可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

    • componentDidMount: 组件挂载完成 (开启监听, 发送ajax请求)
    • componentDidUpdate:组件更新完成
    • componentWillUnmount:组件即将卸载(收尾工作, 如: 清理定时器)

    副作用:指那些没有发生在数据向视图转换过程中的逻辑,如 发送ajax请求获取数据、 设置订阅 / 启动定时器及手动修改DOM 。

    在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的

    不需要清除的effect: 只想在 React 更新 DOM 之后运行一些额外的代码,如发送网络请求,手动变更 DOM,记录日志

    需要清除的 effec: 如订阅外部数据源 ,清除工作可以防止引起内存泄露

    默认情况下,React 会在每次渲染后都会执行useEffect,通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。 将 useEffect放在组件内部让我们可以在 effect 中直接访问 state 变量(或其他 props),我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中

    useEffect(() => { 
        
      // 执行副作用操作【挂载+更新】
        
      return () => { //返回清除副作用的函数(可选),如订阅,定时器,在组件卸载的时候执行清除操作
          
      }
    }, [stateValue]) // 仅在stateValue更改时执行更新
    //如果某些特定值在两次重渲染之间没有发生变化,你可以通知React跳过对useEffect的调用,只要传递数组作为 useEffect 的第二个可选参数即可
    //如果想执行只运行一次的useEffect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数,这样effect内部的props和state就会一直拥有其初始值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.4 状态获取与传递型Hooks

    useRef

    useRef 可以用来获取元素,缓存状态,保存标签对象,接受一个状态 initValue 作为初始值,返回一个 ref 对象 ,ref上有一个 current 属性就是 ref 对象需要获取的内容。

    const refContainer = useRef(initValue);
    console.log(refContainer.current)
    
    • 1
    • 2
    • useRef返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

    • useRef可以在函数组件中存储/查找组件内的标签或任意其它数据,相当于创建一个额外的容器来存储数据,我们可以在外部拿到这个值

    • 重新赋值 ref.current 不会触发重新渲染

    案例

    function TextInputWithFocusButton() {
      const inputEl = useRef(null);
      const onButtonClick = () => {
        // `current` 指向已挂载到 DOM 上的文本输入元素
        inputEl.current.focus();
      };
      return (
        <>
          <input ref={inputEl} type="text" />
          <button onClick={onButtonClick}>Focus the input</button>
        </>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    useContext

    useContext用来获取父级组件传递过来的 context 值,这个当前值就是最近的父级组件 Provider 设置的 value 值,useContext 参数一般是由 createContext 方式创建的,也可以父级上下文 context 传递的 ( 参数为 context )。useContext 可以代替 context.Consumer 来获取 Provider 中保存的 value 值。

     const contextValue = useContext(context) 
    
    • 1

    useContext 接受一个参数,一般都是 context 对象,返回值为 context 对象内部保存的 value 值。

    import React, { useContext, createContext } from 'react'
    //创建context对象
    const MyContext = React.createContext()
    export default function Hook() {
        const [num, setNum] = React.useState(1)
        return (
            <h1>
                 //Provider确定数据共享范围,value分发数据
                <Context.Provider value={num}>
                    <Item num={num} />
                </Context.Provider>
            </h1>
        )
    }
    function Item() {
        //接收contex对象,并返回该context的值
        //该值由上层组件中距离当前组件最近的的value prop决定
        const num = useContext(MyContext)//useContext的参数必须是context对象本身:
        //调用了useContext的组件总会在context值变化时重新渲染,上层数据发生改变,肯定会触发重新渲染
        return <div>子组件  {num}</div>
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.5 自定义型 Hooks

    自定义 Hook 是一个函数,其名称以 use开头,是 React Hooks 聚合产物,其内部有一个或者多个 React Hooks 组成,用于解决一些复杂逻辑。 通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

    自定义 hooks的步骤:

    1. 引入 react 和自己需要的 hook
    2. 创建自己的hook函数
    3. 返回一个数组,数组中第一个内容是数据,第二个是修改数据的函数
    4. 暴露自定义 hook 函数出去
    5. 引入自己的业务组件

    例如:模拟数据请求的 Hooks

    import React, { useState, useEffect } from "react";
    function useLoadData() {
      const [num, setNum] = useState(1);
      useEffect(() => {
        setTimeout(() => {
          setNum(2);
        }, 1000);
      }, []);
      return [num, setNum];
    }
    export default useLoadData;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们希望 reducer 能让每个组件来使用,我们自己写一个 hooks,自定义一个自己的 LocalReducer

    import React, { useReducer } from "react";
    const store = { num: 1210 };
    const reducer = (state, action) => {
      switch (action.type) {
        case "num":
          return { ...state, num: action.num };
        default:
          return { ...state };
      }
    };
    function useLocalReducer() {
      const [state, dispatch] = useReducer(reducer, store);
      return [state, dispatch];
    }
    export default useLocalReducer;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    由浅入深Dubbo网络通信协议大全
    Python 列表推导式:计算不同运动组合的热量列表及最大最小热量消耗情况
    创建vue3项目并引用elementui
    C++中的继承
    计算机毕业设计之java+springcloud基于vue的智慧养老平台-老人信息管理-敬老院管理系统
    Linux图形栈入门概念
    层次遍历二叉树
    16、XDC引脚约束
    第 18章 安全架构设计理论与实践
    【Java代码规范】阿里编码规约 VS CheckStyle
  • 原文地址:https://blog.csdn.net/Better_Xing/article/details/126244248