React-Hooks

什么是Hooks?

Hooks是一类特殊的函数,适用于React的函数组件,可以让我们在不编写class的情况下使用state及其他的React特性,比如副作用处理及生命周期等。

为什么要Hooks?

Hooks主要可以解决以下几类问题:

  1. 在无需修改组件结构的情况下复用状态逻辑。原来的类组件如果需要封装一段可重用的逻辑,需要使用到高阶组件,不仅增加代码复杂度,同时会增加额外的组件节点。但是采用hooks的话,会更加的简单方便。
  2. 复杂逻辑分离,针对一些副作用进行了集中的管理,让相关的业务逻辑更加聚合。
  3. 使得函数组件可以进行组件的状态管理,能够满足绝大多数类组件的使用,减少对class的使用,从而避免class的一些痛点问题(this的指向等)

怎么用Hooks?

  1. 只能在react的函数组件或自定义的Hook中调用。
  2. 只能在函数的最外层调用Hook。不要在循环、条件判断或子函数中调用,这样是为了确保hook都能被执行,react是根据hook的执行顺序来实现多次渲染之间hooks的状态对比。
  3. React提供了一个检查hooks是否使用正确的插件:eslint-plugin-react-hooks
//安装
npm install eslint-plugin-react-hooks --save-dev

//eslint配置文件
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    // 检查 Hooks 的使用规则
    "react-hooks/rules-of-hooks": "error", 
    // 检查依赖项的声明
    "react-hooks/exhaustive-deps": "warn"
  }
}

备注:react通过调用hooks的调用顺序,来确认state对应那个useState,如果使用了条件判断语句,可能导致前后的调用顺序不一致,就会导致bug的出现

什么时候用Hooks?

  • 需要向函数组件中添加state的时候
  • 想要在函数组件中使用一些副作用的时候
  • 想要实现props广播的时候

常见Hooks

1.useState

useState能够给函数组件引入state来实现函数组件的 状态管理,组件内部可以通过setState来修改state的值。state的变化同样会触发组件的重新渲染。

// 引入
import React, { useState } from 'react'
...
// 数组解构,stateInit只会在初始创建state时有效,后续重复渲染时,会采用已存在的state
const [state, setState] = useState(initialState)

// 设置state
setState(state)
...
  1. 根据实际场景来声明state,state的内容应该尽可能在逻辑上独立和解耦,方便后期如果存在逻辑抽离的改造。
  2. useState的入参允许是一个函数,当我们的值依赖于旧的state时,就可以采取函数作为入参的方式。
  3. initialState允许是一个函数,方便应对需要经过复杂处理得到初始值的情况。
  4. useState不会自动合并更新对象,可以采取展开运算符来达到合并对象的效果。
  5. 调用state的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。
  6. setState是一个异步的处理机制,state值发生变化后会触发重新渲染。
  7. state中永远不要保存可以通过计算得到的值,这种可以通过声明变量或者利用一些cache的机制保存。

2. useEffect

useEffect在渲染后判断依赖并调用,通过useEffect可以实现在函数组件中执行副作用(数据获取、设置订阅、记录日志以及手动更改React组件中的DOM)操作。useEffect支持通过返回一个清除函数的方式,来实现组件卸载的时候执行必要的一些清除操作。

// 引入
import React, { useEffect }  from 'react'
...
useEffect(() => {
    document.title = `You clicked ${count} times`;
});

// useEffect
useEffect(()=>{
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
})

// useEffect支持传入第二个参数,以此实现按需重新执行
useEffect(()=>{
    console.log(count)
}, [count])
  1. 默认情况下在首次渲染和每次更新后都会执行,每次渲染都会生成新的effect函数,并在每次执行新的effect之前,都会对旧的effect进行清理(相对于class组件省去了还需要在componentDidUpdate关注清理逻辑的情况)。
  2. useEffect允许声明多个,以此方便不同功能的逻辑分离,react将按照顺序调用。
  3. 依赖数组中的变量应该是会随着外部变化并且会在effect中使用的变量,如果不存在需要重复执行的情况,则可以传递一个[]。
  4. 如果effect中存在函数调用,要注意函数中是否存在需要依赖的props和state,如果有,建议将函数放到effect中去声明或者在依赖数组中加上依赖项
  5. 当useEffect没有依赖项时,每次render后都会重新执行。当useEffect的依赖项时空数组[]时,则只会在首次执行时触发,对应到class组件就是componentDidMount。
  6. react的会采用浅比较来判断依赖项是否发生变化,所以特别要注意数组或者对象类型。

3.useContext

该hook用来订阅上层组件中传递的context value值。当值发生变化的时候会重新的触发组件的渲染,该context值有上层组件中距离当前组件最近的xxx.Provider的value prop决定,useContext的作用相当于在全局设置了一个变量,但是相对于全局变量而言,useContext的值发生改变之后,是会触发所有用到了useContext的地方自动刷新。

import React,{useContext} from 'react'

const TextContext = React.createContext('React!')

export default function HookUseContext() {
    return (
        <div>
            <TextContext.Provider value={111}>
                <CompA />
            </TextContext.Provider>
        </div>
    )
}

function CompA(){
    return (
        <div>
            <CompB />
        </div>
    )
}

function CompB(){
    const content = useContext(TextContext)
    return (
        <div>
            <span>
                {content}
            </span>
        </div>
    )
}
  1. useContext可以用来进行祖组件和子孙组件之间的值传递,适合传递全局组件都可能会需要考虑的一些属性,比如theme,locale,language,device以及缓存数据等。
  2. useContext使得组件中的一些值依赖于外部的provider,这会导致组件的复用性降低,所以使用之时需要慎重

useContext的使用要依赖于react的一些其他API,具体如下:

  • React.createContext:创建Context对象,允许提供context的默认值,当消费组件没有匹配到Provider时,就读取默认值
  • Context.Provider:接收一个value值,允许消费组件根据context进行订阅,Provider可以被多个消费组件订阅,当value发生变化时,所有消费组件都会重新渲染。Provider支持嵌套,嵌套的时候里层的会覆盖外层的数据。

其他Hooks

1.useCallback

把内联回调函数及依赖项数组作为参数传入 useCallback,在初次渲染的时候执行,并返回该回调函数的 memoized(缓存)版本,初次渲染完成后,该回调函数仅在某个依赖项改变时才会更新。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
  1. useCallback(fn, deps) 相当于useMemo(()=>fn, deps)
  2. 适用于回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件的场景。

扩展——React.memo()

React.memo()包裹的组件(一般为函数组件,类组件可以采用PureComponent/shouldComponentUpdate进行props的比较),当props相同时,会直接复用上次的组件渲染结果,以此来提高性能。当组件中存在useState,useReducer或useContext的调用时,state或者context的变化依旧会触发组件的重新渲染。React.memo()默认只做浅层次比较,但支持传入第二个参数来实现props的深层次对比。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

useCallback & React.memo

当我们需要将函数作为prop传递给子组件时,由于每次渲染都会导致函数的重新创建,从而触发子组件的重新渲染,使用useCallback可以将函数类型的props的值进行缓存,仅当依赖项发生变化时,才会触发新的函数生成,从而减少带有函数类型prop的子组件的重新渲染。

import "./styles.css";
import { useEffect, useState, useMemo, useCallback } from "react";

export default function App() {
  const [value, setValue] = useState(1);
  let result = useMemo(() => {
    console.log(123);
  }, []);

  const handleClick = useCallback(() => {
    console.log("useMemo被执行了")
  }, [value]);

  return (
    <div className="App">
      <input onChange={(e) => setValue(2)} />
      <Button handleClick={handleClick} />
    </div>
  );
}

const Button = (props) => {
  const { handleClick } = props;
  console.log("button被渲染了");
  return <button onClick={handleClick}>点击</button>;
};


2.useMemo

该hook在首次渲染的时候会执行,并返回一个缓存的值。之后仅当依赖项数组中的内容发生变化时,才会重新执行第一个参数带入的函数,然后返回新的值。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  1. useMemo适用于每次渲染时都需要进行高开销的计算场景,仅当依赖项发生变化的时候才重新计算,能够有效避免无谓的计算。
  2. useMemo的函数会在渲染期间执行,因此不能执行副作用等操作。
  3. 当依赖数组为空时,会在每次渲染期间都执行。
  4. useMemo()可以实现局部缓存的功能。比如下面这个例子,当number未发生改变时,会直接获取旧的内容,但是文件中的其他部分将照常执行。
  5. useMemo其实可以实现useCallback的功能,在处理上其实都是类似的。
import React, { useMemo } from 'react';

export default (props = {}) => {
    console.log(`--- component re-render ---`);
    return useMemo(() => {
        console.log(`--- useMemo re-render ---`);
        return 

number is : {props.number}

}, [props.number]); }

3.useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer可以作为useState的替代方案。入参reducer类似于一个==(state, action) => newState==的reducer。该hooks返回当前的state及配套的dispatch方法。

// 指定初始化的方式
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      
      
    
  );
}

useReducer的使用场景

  • 当Reducer Hook的返回值和当前的state相同时,可以跳过子组件的渲染及副作用的执行。该特性可以减少子组件不必要的重新渲染,达到性能优化的目的。
  • useReducer可以通过第三个入参init函数,来实现惰性初始化state,这样同时有利于做state的重置处理。

4.useRef

const refContainer = useRef(initialValue);

useRef返回一个可变的Ref对象,其.current属性被初始化为传入的参数(initialValue)。返回的ref对象在组件的整个生命周期内持续存在,因此useRef适用于跨渲染的数据存储和共享

  1. 返回的ref对象的current属性可以容纳任何值,是一个通用容器。
  2. useRef会在每次渲染时返回同一个ref对象
  3. ref对象内容发生变化时,useRef并不会通知你,变更.current属性不会引发重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      
      
    
  );
}

知识关联——ref的关联方式:
1.字符串声明(不被推荐)
2.React.createRef声明,通过ref属性关联react元素
3.回调ref,将一个回调函数传递给ref,React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。这方便我们处理在绑定和卸载ref时候做相关的特殊处理。

5.useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。useImperativeHandle 应当与 forwardRef 一起使用。

import React, { useImperativeHandle, useRef, forwardRef } from 'react';

function FancyInput(props, ref) {
  useImperativeHandle(ref, () => ({
    focus: () => {
      ref.current.focus();
    },
  }));
  // inputRef属性传递
  return ;
}

FancyInput = forwardRef(FancyInput)

export default function HooksUseImperativeHandle() {
  const inputRef = useRef(null);
  console.log(inputRef)
  return (
    
); }

知识拓展——React.forwardRef
React.forwardRef 接受一个渲染函数作为入参,返回一个创建好的React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。

6.useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

  1. 必要情况下才用useLayoutEffect,不然可能造成阻塞视觉更新,推荐优先使用useEffect。
  2. 不能再服务端渲染中使用该Hook。

7.useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签

自定义Hook

hook的一大优势就在于能够封装公用逻辑,并方便复用。除了react提供的hook之外,还可以自定义hook来实现逻辑的封装。自定义hook是一个名为“use”开头的函数,函数内部调用了其他的Hook,常用来将可重用的逻辑单独抽离成一个可重用的函数。

  1. 自定义Hook中调用其他Hook需要遵循Hook使用规则,且hook名字需要始终以use开头。
  2. 自定义hook采取一种重用状态逻辑的机制,每次使用的hook时,其中的state和副作用完全隔离,这个点和useState,useEffect的重复调用机制相似。

采用自定义Hooks相对于封装工具类而言,hook的优势在于可以管理当前组件组件的state,而普通的工具类是无法修改组件的state的,也就无法在数据改变的时候触发组件的重新渲染。

典型的集中自定义hooks的场景有:

  • 抽取业务逻辑
  • 封装通用逻辑,比如异步请求
  • 监听浏览器状态,比如浏览器滚动监听
  • 拆分复杂组件,hooks的功能是封装和复用,如果只是封装的场景,其实也可以用hook来实现。

生命周期函数对应的hook

类组件存在生命周期的钩子函数,在转换到函数组件时,可以分别对应到一些hook中。比如componentDidUpdate、componentDidMount、componentWillUnmount可以对应useEffect。而部分其他的生命周期函数如getSnapshotBeforeUpdate、componentDidCatch、getDerivedStateFromError等,目前则暂时没有对用的hooks去实现。

参考资料

memo、useMemo及useCallback解析

你可能感兴趣的:(前端,React,react.js,javascript,前端)