【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)

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

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

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

在这里插入图片描述

注:

  • 在函数组件一次执行上下文中,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]; // 数组里的第二个值

useReducer

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

useReducer语法:

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

参数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>
}

在这里插入图片描述

注:

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

2.2 状态派生与保存型Hooks

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

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

什么是 memoization?

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

Hooks逻辑

  • 几乎所有有依赖数组的 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

React.memo高阶组件

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

React.memo语法:

const MemoComponent = React.memo(component, Func)

参数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]);

第一个参数 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>

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

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

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])

把回调函数及依赖项数组作为参数传入 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使用变量的依赖

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

想要在组件挂载后执行一次副作用代码并且不依赖于任何状态变化时,使用 useEffect(() => {}, []);。而如果你想要在每次组件更新后执行副作用代码,无论状态是否改变,可以使用 useEffect(() => {});,不传递依赖数组。

useLayoutEffect

首先要说明的是,useLayoutEffect和useEffect很像,函数签名也是一样。唯一的不同点就是useEffect是异步执行,而useLayoutEffect是同步执行的。
当函数组件刷新(渲染)时,包含useEffect的组件整个运行过程如下:

  • 触发组件重新渲染(通过改变组件state或者组件的父组件重新渲染,导致子节点渲染)。
  • 组件函数执行。
  • 组件渲染后呈现到屏幕上。
  • useEffect hook执行。

当函数组件刷新(渲染)时,包含useLayoutEffect的组件整个运行过程如下:

  • 触发组件重新渲染(通过改变组件state或者组件的父组件重新渲染,导致子组件渲染)。
  • 组件函数执行。
  • useLayoutEffect hook执行, React等待useLayoutEffect的函数执行完毕。
  • 组件渲染后呈现到屏幕上。

useEffect异步执行的优点是,react渲染组件不必等待useEffect函数执行完毕,造成阻塞。

百分之99的情况,使用useEffect就可以了,唯一需要用到useLayoutEffect的情况就是,在使用useEffect的情况下,我们的屏幕会出现闪烁的情况(组件在很短的时间内渲染了两次)。

2.4 状态获取与传递型Hooks

useRef

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

import { useRef } from ‘react’

const myComponent = () => {
  const refObj = useRef(initialValue)
  console.lgo(refObj)//{current:initialValue}
  return (
  //…
  )
}

在上面的代码片段中,我们有一个对象 refObj,我们想在应用程序中引用它,为了访问这个值或更新它的值,我们可以像这样调用 current 属性:

// 在函数内部
const handleRefUpdate = () => {
    // 访问被引用对象的值
    const value = refObj.current

    // 更新被引用对象的值
   refObj.current = newValue
}
  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。
  • useRef 创建的是一个普通JS对象,而且会在每次渲染时返回同一个 ref 对象,被引用对象的值在重新渲染之间保持不变。
  • 当我们变化它的 current 属性的时候,操作的都是同一个对象,更新被引用对象的值不会触发重新渲染。
  • 可以保存任何类型的值:dom、对象等任何可变值ref对象与自建一个{current:‘’}对象的区别是:useRef会在每次渲染时返回同一个ref对象,即返回的ref对象在组件的整个生命周期内保持不变。自建对象每次渲染时都建立一个新的。
  • useRef返回的ref对象在重新渲染之间保持不变,改变ref对象的.current属性时组件不会重新渲染

useRef 获取 DOM 元素,在 React Native 中虽然没有 DOM 元素,但是也能够获取组件的节点信息( fiber 信息 )。

const DemoUseRef = ()=>{
    const dom= useRef(null)
    const handerSubmit = ()=>{
        /*  <div >表单组件</div>  dom 节点 */
        console.log(dom.current)
    }
    return <div>
        {/* ref 标记当前dom节点 */}
        <div ref={dom} >表单组件</div>
        <button onClick={()=>handerSubmit()} >提交</button> 
    </div>
}

如上通过 useRef 来获取 DOM 节点。

useRef 保存状态, 可以利用 useRef 返回的 ref 对象来保存状态,只要当前组件不被销毁,那么状态就会一直存在。

const status = useRef(false)
/* 改变状态 */
const handleChangeStatus = () => {
  status.current = true
}

useContext

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

 const contextValue = useContext(context) 

useContext 接受一个参数,一般都是 context 对象,返回值为 context 对象内部保存的 value 值,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分发数据
            <MyContext.Provider value={num}>
                <Item num={num} />
            </MyContext.Provider>
        </h1>
    )
}
function Item() {
    //接收contex对象,并返回该context的值
    //该值由上层组件中距离当前组件最近的<MyContext.Provider>的value prop决定
    const num = useContext(MyContext)//useContext的参数必须是context对象本身:
    //调用了useContext的组件总会在context值变化时重新渲染,上层数据发生改变,肯定会触发重新渲染
    return <div>子组件  {num}</div>
}

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;

我们希望 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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React 18全家桶React.js的最新版本,它引入了很多新特性和改进,使得开发更加方便和高效。其中,Hooks是React 16.8版本引入的一项重要特性,它允许我们在无需编写类组件的情况下,在函数式组件中使用状态和其他React功能。 React 18全家桶Hooks项目实战网盘是一个基于React 18全家桶的实际开发项目,旨在通过实际开发来学习和掌握React 18全家桶Hooks的使用。 在这个项目中,我们可以通过网盘将文件上传、下载和分享给其他用户。首先,我们可以使用React 18全家桶的新特性来创建一个界面,例如使用React函数组件和Hooks来管理界面的状态、处理用户输入以及渲染文件列表等。我们可以使用React Router来管理不同页面之间的导航,并使用React Context来共享全局的状态和数据。 对于文件的上传和下载功能,我们可以利用React 18全家桶提供的新API,例如使用React Concurrent Mode来提高文件上传和下载的性能。同时,我们可以使用React Query来管理文件的后台数据请求和状态更新,以及使用React Hook Form来处理表单的数据验证和提交。 在项目中,我们可以使用React 18全家桶的新特性来实现一些高级的功能,例如使用React Server Components来实现服务器端渲染和实时数据更新,或使用React Fast Refresh来提高开发和调试的效率。 通过参与React 18全家桶Hooks项目实战网盘,我们可以深入了解并熟练掌握React 18全家桶Hooks的使用。这将有助于我们在实际的React项目开发中提高开发效率和代码质量。同时,我们也可以通过参与项目来拓展我们的React技术栈,并与其他开发者共同学习和交流。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值