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

文章目录

  • 1 Hooks简介
    • 1.1 什么是Hooks?
    • 1.2 Hooks的优势
    • 1.3 Hooks使用场景
    • 1.4 Hooks使用注意事项
  • 2 Hooks API
    • 2.1 数据驱动更新型Hooks
      • useState
      • useReducer
    • 2.2 状态派生与保存型Hooks
      • memoization 含义、Hooks 逻辑、React浅比较
      • React.memo高阶组件
      • useMemo
      • useCallback
    • 2.3 执行副作用型Hooks
      • useEffect
    • 2.4 状态获取与传递型Hooks
      • useRef
      • useContext
    • 2.5 自定义型 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可以不写

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

【React】React全家桶(五)React Hooks_第1张图片

注:

  • 在函数组件一次执行上下文中,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>
}

【React】React全家桶(五)React Hooks_第2张图片

注:

  • 如果 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

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

2.4 状态获取与传递型Hooks

useRef

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

const refContainer = useRef(initValue);
console.log(refContainer.current)
  • 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>
    </>
  );
}

useContext

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

 const contextValue = useContext(context) 

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

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;

你可能感兴趣的:(React全家桶,react.js,前端框架,javascript,前端)