进阶篇

PlumliilAbout 9 min

进阶篇

Hook

useState

先放上 React 官方类型定义

type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
/**
 * Returns a stateful value, and a function to update it.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#usestate
 */
function useState<S>(
  initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];

我们在使用 useState 的时候会给 useState 传递一个参数,而这个参数的返回值是一个数组,

数组中有两个元素,第一个元素为我们设置的值,第二个为更新 第一个值的方法

在类型定义上看,S 为一个泛型,用来指定初始值的类型,从括号中看,传入参数可以为一个值或者一个函数,这个函数返回值是一个数组,第一个值是我们的初始值,第二个参数为 dispatch,dispatch 本质是一个函数,接收一个参数这个参数可以是具体的值也可以是一个函数,如果是一个函数,那么这个函数返回的参数就是上一次设置的值

import { useState } from "react";
const Index = () => {
  const [count, setCount] = useState(() => 0);
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount((prevCount) => prevCount + 2)}>+2</button>
    </div>
  );
};

如果我们将 button 的点击事件抽离出来,并且在点击事件中连续执行四次 setCount,那么它执行的结果是什么呢,它只会+10

const Index = () => {
  const [count, setCount] = useState(() => 0);
  const btnClick = () => {
    setCount(count + 10);
    setCount(count + 10);
    setCount(count + 10);
    setCount(count + 10);
  };
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount((prevCount) => prevCount + 2)}>+2</button>
      <button onClick={btnClick}>+40</button>
    </div>
  );
};

这个函数内的 setCount 只会执行一次,但是当我们用另一种方法时,他就会执行我们想要执行的次数

const btnClick = () => {
  setCount((prevCount) => prevCount + 10);
  setCount((prevCount) => prevCount + 10);
  setCount((prevCount) => prevCount + 10);
  setCount((prevCount) => prevCount + 10);
};

useState 这个 Hook 是用来管理 state 的,它可以让函数组件具有维持状态的能力。即在一个函数组件的多次渲染之间,这个 state 是共享的。

useEffect

effect 意为副作用 通过 Effect Hook 可以来获得一些类似与 class 中生命周期的功能 事实上,类似于网络请求,手动更新 Dom,事件监听,都是 React 更新 Dom 的一些副作用,所以对于完成这些功能的 Hook 被称为 Effect Hook

从定义来看

type DependencyList = ReadonlyArray<unknown>;
/**
 * Accepts a function that contains imperative, possibly effectful code.
 *
 * @param effect Imperative function that can return a cleanup function
 * @param deps If present, effect will only activate if the values in the list change.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#useeffect
 */
function useEffect(effect: EffectCallback, deps?: DependencyList): void;

useEffect 函数第二个参数是一个数组,该数组储存函数依赖,

  • 当第二个参数不传时,只要逐渐重新渲染,那么 useEffect 函数都会重新执行一遍
  • 当第二个参数为空数组[]时,只会在组件渲染时执行一次
  • 当第二个参数的数组中有参数时,对应的 useEffect 会在该参数变化时进行执行 通过 useEffect 的 Hook,可以告诉 React 需要在渲染后执行某些操作,useEffect 要求我们传入一个回调函数,在 React 执行更新完 Dom 操作后,就会执行这个回调函数,默认情况下,无论是第一次选然后还是每次更新之后都会执行这个回调函数

在 React 组件中,useEffect 不止能定义一个,在组件中我们不可避免的在生命周期中做一些操作,我们可以定义多个 useEffect 来区分操作,useEffect 执行的顺序就是定义的顺序如:

const Index = () => {
  const [count, setCount] = useState(() => 0);
  // 网络请求
  useEffect(() => {
    console.log("网络请求");
  }, []);
  // 修改Dom
  useEffect(() => {
    console.log("修改Dom:button内容改变", count);
  }, [count]);
  // 定义事件
  useEffect(() => {
    console.log("订阅一些事件");
    return () => {
      console.log("取消订阅某些事件");
    };
  }, []);
  return (
    <div>
      <h1>useEffect Hook</h1>
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </div>
  );
};

useContext

Context Hook 是可以让子组件给后代组件共享数据

接受一个上下文对象(从' React.createContext '返回的值)并返回当前* context 值,该值由最近的给定上下文提供程序提供。

/**
 * Accepts a context object (the value returned from `React.createContext`) and returns the current
 * context value, as given by the nearest context provider for the given context.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#usecontext
 */
function useContext<T>(
  context: Context<T> /*, (not public API) observedBits?: number|boolean */
): T;

子孙组件

import { useContext } from "react";
import { FirContext, SecContext } from ".";
export const MyComp = () => {
  const fir = useContext(FirContext);
  const sec = useContext(SecContext);
  return (
    <div>
      <h1>MyComp</h1>
      <h3>
        {fir.title}:{fir.content}
      </h3>
      <h3>
        {sec.title}:{sec.content}
      </h3>
    </div>
  );
};

父组件

import { createContext } from "react";

export const FirContext = createContext();
export const SecContext = createContext();

const Index = () => {
  return (
    <div>
      <h1>Context Hook</h1>
      <FirContext.Provider
        value={{
          title: "FirContext",
          content: "Docs Content FirContext.Provider",
        }}
      >
        <SecContext.Provider
          value={{
            title: "SecContext",
            content: "Docs Content SecContext.Provider",
          }}
        >
          <MyComp />
        </SecContext.Provider>
      </FirContext.Provider>
    </div>
  );
};

useReducer

在某些场景下,如果 state 处理的逻辑较为复杂,可以使用 useReducer 来对其进行拆分,或者这次修改的 state 需要依赖之前的 state 时也可以使用 看看官方怎么说:

/**
 * An alternative to `useState`.
 *
 * `useReducer` is usually preferable to `useState` when you have complex state logic that involves
 * multiple sub-values. It also lets you optimize performance for components that trigger deep
 * updates because you can pass `dispatch` down instead of callbacks.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#usereducer
 */
// overload where dispatch could accept 0 arguments.
function useReducer<R extends ReducerWithoutAction<any>, I>(
  reducer: R,
  initializerArg: I,
  initializer: (arg: I) => ReducerStateWithoutAction<R>
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];

从上述代码可以看出,useReducer 是 useState 的另一种选择,当我们要实现的状态逻辑较为复杂时,useReducer 优先级更高,还可以优化触发深度更新组件的性能,因为 useReduce 可以向下出传递 dispatch 而不是回调

useReducer 支持我们传递三个参数, 第一个参数是 reducer 纯函数 第二个参数是我们要使用的初始化值 第三个参数是一个函数,它支持我们对 useReducer 进行初始化操作

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return state + 1;
    // return { ...state, count:count+1 };
    case "decrement":
      return state - 1;
    // return { ...state, count: count - 1 };
    default:
      return state;
  }
};

const Index = () => {
  // const [count, setCount] = useState(0);
  const [state, dispatch] = useReducer(reducer, 1);
  // const [state, dispatch] = useReducer(reducer, { count: 1 });
  return (
    <>
      <h1>useReducer</h1>
      <h2>当前计数:{state}</h2>
      {/* <h2>当前计数:{state.count}</h2> */}
      <button onClick={() => dispatch({ type: "increment" })}>+1</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-1</button>
    </>
  );
};

从上面代码可以看出我们可以传递两种不同形式的参数,数字和对象,对应的使用方法也需要改变,同时我们可以对 reducer 进行抽离来复用,改变的数据不会共享

useRef

useRef 在 react 中有两个作用

  • 用来存储数据
  • 使用 Ref 访问 DOM 元素

在第一个作用中 useRef 创建一个对象来存储数据,和 useState 不同的是,改变 useRef 创造出来的对象的值组件不会重新渲染,需要等到下次渲染才会显示到页面上

在函数组件中进行获取 Dom 元素时,有时会结合forwardRef来使用,forwardRef可以用来转发 ref,使用forwardRef包裹的组件接收第二个参数ref,这个ref为父组件传入的ref,从而可以对子组件中的 Dom 进行操作.

import React, { forwardRef, useRef } from "react";

const Foo = forwardRef((props, iptRef: any) => {
  const clickHandler = () => {
    console.log("iptRef", iptRef);
    iptRef.current.focus();
  };
  return (
    <div>
      <input type="text" ref={iptRef} />
      <button onClick={clickHandler}>聚焦</button>
    </div>
  );
});

const Demo = () => {
  const fooRef = useRef<any>();
  const clickHandler = () => {
    console.log("fooRef", fooRef);
    fooRef.current.focus();
  };
  return (
    <div>
      <Foo ref={fooRef} />
      <button onClick={clickHandler}>FOO REF</button>
    </div>
  );
};

export default Demo;

上述代码两个按钮均可实现输入框聚焦功能,当点击按钮时控制带打印如下

index.tsx:5 iptRef {current: input}
index.tsx:19 fooRef {current: input}

这两个 ref 操作的是同一个 Dom 节点

useCallback

useCallback 的实际目的是进行性能优化,useCallback 会返回一个函数的 memoized(记忆值);在依赖不变的情况下,多次定义的时候,返回的值是相同的 useCallback 接收两个参数,第一个参数是要完成的回调,第二个参数是回调依赖值

// @flow
import * as React from "react";
const Foo = React.memo((props: any) => {
  console.log("Foo render");
  return (
    <div>
      <ul>{props.render()}</ul>
    </div>
  );
});
export default (props: Props) => {
  const [range, setRange] = React.useState({ min: 0, max: 10 });
  const [count, setCount] = React.useState(0);
  console.log("APP render");
  const render = () => {
    let list = [];
    console.log("Render Function Action");
    for (let i = 0; i < range.max; i++) {
      list.push(<li key={i}>{i}</li>);
    }
    return list;
  };
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        <h1>+1</h1>
      </button>
      <button
        onClick={() =>
          setRange({
            min: range.min,
            max: range.max + 10,
          })
        }
      >
        <h1>range max +10</h1>
      </button>
      <Foo render={render}></Foo>
    </div>
  );
};

以上述代码为例render函数渲染了一个数字列表,当我们点击+1 按钮时count +1组件重新渲染,控制台会打印以下

index.tsx:17 APP render
index.tsx:7 Foo render
index.tsx:20 Render Function Action

但是此时 render 函数还是原来的那个 render,同一引用,本质上没有发生变化,但是它任然会再渲染一边,这不是我们想要的,此时就可以使用 useCallback 进行简单优化,仔细观察整个函数,函数的渲染只和renge.max有关,所以只需要监听这个 range 依赖,当 range 变化时再重新渲染

const render = React.useCallback(() => {
  console.log("Render Function Action");
  let list = [];
  console.log(1);
  for (let i = 0; i < range.max; i++) {
    list.push(<li key={i}>{i}</li>);
  }
  return list;
}, [range]);

useMemo

useMemouseCallback 都可以进行 react 组件优化,但不同的一点是 useCallback 是固定的是一个函数,而 useMemo 固定的是一个值 以 useCallback 代码为例,我们使用 useMemo 替换 useCallback

const render = React.useMemo(() => {
  console.log("Render Function Action");
  let list = [];
  console.log(1);
  for (let i = 0; i < range.max; i++) {
    list.push(<li key={i}>{i}</li>);
  }
  return list;
}, [range]);

函数已经经过替换,但是想要正常使用,仍需要修改 render 的使用方法,useCallback 包裹的函数在调用时仍是一个函数,我们需要加括号进行调用,而 useMemo 包裹的函数直接调用其值即可

// useCallback
<ul>{props.render()}</ul>
// useMemo
<ul>{props.render}</ul>

useMemo 是性能优化的手段,传入 useMemo 的函数会在渲染期间执行,最好不要再函数内部执行与渲染无关的操作,诸如副作用的操作属于 useEffect 的范畴,而不是 useMemo.useMemo 只负责渲染

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

memo

useCallbackuseMemo 定义函数逻辑,优化函数处理不同,memo 是优化函数组件的一种方式,它可以让组件不重复渲染,不进行刷新

// @flow
import * as React from "react";
const Foo = (props: any) => {
  console.log("Foo render");
  return (
    <div>
      <h2>{props.count}</h2>
    </div>
  );
};

export default (props: Props) => {
  const [count, setCount] = React.useState(0);
  console.log("APP render");
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Foo count={1}></Foo>
    </div>
  );
};

如上述代码所示,当我们点击+1 按钮时,count 发生变化此时页面会重新渲染,Foo render,App render均会打印,但是此时的Foo组件没有进行改变,和渲染前并无区别,所以没必要进行渲染,而memo所做的事就是当Foo的依赖如count发生变化时,Foo才会重新渲染

const Foo = React.memo((props: any) => {
  console.log("Foo render");
  return (
    <div>
      <h2>{props.count}</h2>
    </div>
  );
});

将上述Foo组件使用memo进行包裹此时再点击+1 按钮,只会打印App render,而Foo render不会进行打印,这就起到了优化的作用