React Reference | 阅读文档后理解 01

117 阅读7分钟

以下内容我在阅读官方文档中的 Reference 部分后产生的理解

Hook

useCallback

简介


useCallback(fn, dependencies)

Parameters

fn: 用于缓存的函数定义,hook 不会调用它,而是当作引用型数据缓存起来,以及根据依赖改变它

dependencies: fn 内部使用到的响应式数据,类似于 effect 的依赖

Returns

返回一个函数,如果依赖没有任何改变,那么这个函数将会是上一次渲染缓存的函数,就是从对象方面来看,这不是一个新的对象。如果依赖有所改变,那么返回的函数将会的这次渲染中缓存的新的函数。

  • 为什么要将函数缓存起来?

    因为在 js 中函数也是对象,不断生成函数也是需要消耗性能的。一般来讲,组件的重新渲染就是再次调用组件函数拿到返回值,组件函数中定义的函数,就会再次生成其对象体后执行,如果这次渲染中,函数的定义与上次渲染中的定义没有区别,是不是就不必要再次生成了。所以如果能拿到上次渲染中的函数,就避免了生成对象的消耗。

但是大部分情况下将函数缓存起来使用并不会带来多大提升,几乎可以看作没有

  • 那何时使用 useCallback 最有帮助呢?
    • 被 memo 标记的组件在 re-rendering 时如果接收的 props 与上次渲染一致,则不会被再次执行

      import { memo } from 'react';
      
      const ShippingForm = memo(function ShippingForm({ onSubmit }) {
        // ...
      });
      

      当函数被作为 props 传递时,如果没有缓存,那不同的渲染时机都是不同的对象。所以对于用 memo 标记的组件,想要回收利用其没有变化的状态,function props 就需要缓存了。

      这时回收利用没有变化的函数就不仅仅只是回收利用一个对象了,而是整个组件,这带来的收益就不可同日而语了。

    • 当函数作为一些 Hook 的依赖时,就要保证其逻辑上没有变化时,物理上确实没有变化,这样才能保证 Hook 逻辑上没有变化时,物理上也没有变化,也就是缓存函数依赖。

被缓存函数的逻辑是更新 state 时


更新 state 的逻辑是拿到之前的 state 来帮助更新,那么 state 就作为依赖了。每当 todos 被更新时,handleAddTodo 的定义中的 […todos] 也是新的,故而函数需要变化。

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos([...todos, newTodo]);
  }, [todos]);
  // ...

但是 setState 函数可以接收回调用于描述如何更新,不管 todos 更新几次,描述是不会变化的,所以被缓存函数不需要变化。

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos(todos => [...todos, newTodo]);
  }, []); // ✅ No need for the todos dependency
  // ...

优化自定义Hook


推荐缓存自定义 Hook 返回的函数

function useRouter() {
  const { dispatch } = useContext(RouterStateContext);

  const navigate = useCallback((url) => {
    dispatch({ type: 'navigate', url });
  }, [dispatch]);

  const goBack = useCallback(() => {
    dispatch({ type: 'back' });
  }, [dispatch]);

  return {
    navigate,
    goBack,
  };
}

useContext

简介


useContext(someContext)

Parameters

someContext: 由 createContext API 创建的数据类型。context 本身并不包含信息,它仅是代表由组件们提供的,也可以让组件们读取的信息。

Returns

useContext 的返回值对于调用它的不同组件可能是不同的。就正如其名上下文,它会寻找组件树上层最近的 SomeContext.Provider 接受的 value ,返回这个 value

就像理解函数寻找变量那样,沿着作用域链从自己开始向上寻找变量,找到的变量一定是最近的那个

使用 context 传递数据到深处


  • 怎样使用 context

    import { createContext, useContext } from 'react';
    
    const ThemeContext = createContext(null); // 创建context对象
    
    export default function MyApp() {
      return (
        <ThemeContext.Provider value="dark"> // 用 SomeContext.Provider 包裹需要上下文的
          <Form />                           // 顶层组件
        </ThemeContext.Provider>
      )
    }
    
    function Form() {
      return (
        <Panel title="Welcome">
          <Button>Sign up</Button>
          <Button>Log in</Button>
        </Panel>
      );
    }
    
    function Panel({ title, children }) {
      const theme = useContext(ThemeContext); // 使用 Hook,传入context对象,获取value
      const className = 'panel-' + theme;
      return (
        <section className={className}>
          <h1>{title}</h1>
          {children}
        </section>
      )
    }
    
    function Button({ children }) {
      const theme = useContext(ThemeContext); // 使用 Hook,传入context对象,获取value
      const className = 'button-' + theme;
      return (
        <button className={className}>
          {children}
        </button>
      );
    }
    
  • 为什么要用 useContext ?

正如标题所言,当父组件要传递数据到深层的子组件时,如果简单使用传递 propsprops 就会经过许多组件,而部分组件并不需要这些 props,这就造成了代码冗余以及无用的重复。但是context 不会在组件间层层穿越,而是谁需要谁就拿。

  • 如何理解 context 的表现?

context.provider 将需要数据的顶层组件包裹起来,其实就是为顶层组件及其子组件提供了一个上下文。这些组件可以通过 useContext 获取数据,是不是就像函数在上下文中寻找变量一样(本来组件的渲染就是先调用其函数),假设顶层组件为 A,则子组件为 B,C ….. ,如果 A 修改了上下文中的数据,那么在其底层的 B C …. 也只能拿到修改后的数据。就像上层函数更改了上下文中的变量,内部函数拿到的变量也就是修改后的。

集中存放与 context 有关的逻辑


一般来讲,与 context 有关的逻辑都放在另外的文件中,如 SomeContext.js ,就像编写组件那样,最后返回的 jsx 就是 SomeContext.Provider 包裹顶层组件

下面的例子是登录时保存当前用的信息到浏览器的本地仓库,同时通过 context 方便其他组件访问当前用户的信息。对于其他组件而言,只管拿到当前用户的信息,以及对于登录页面组件而言只需要拿到并调用 login 方法即可,无须在意用户信息怎么来的,怎么登录的。

import { useState, createContext, useEffect } from 'react';
import axios from 'axios';

export const AuthContext = createContext(); // 创建context对象,并导出

export function AuthContextProvider({ children }) {
		// 当该组件被挂载时,访问本地仓库拿到用户信息,可能之前登录过,也可能是首次登录,都无妨
    const [currentUser, setCurrentUser] = useState(JSON.parse(localStorage.getItem('user')) || null);

    const login = async (inputs) => {
        const res = await axios.post("http://localhost:8800/api/auth/login", inputs, {
            withCredentials: true
        });

        setCurrentUser(res.data);
    }

    useEffect(() => {
        localStorage.setItem('user', JSON.stringify(currentUser))
    }, [currentUser]);

    return (
        <AuthContext.Provider value={{ currentUser, login }} >
            {children}
        </AuthContext.Provider>
    )
}

useReducer

简介


useReducer(reducer, initialArg, init?)

Parameters

reducer: 一个函数,内部逻辑是如何更新 state ,它必须接受两个参数,state and action。返回值是新的 state 。state and action 可以是任意类型。这个函数必须是纯函数。

initialArg: 计算 state 初始值的值

optional init : 计算 state 初始值的函数,参数是 initialArg 。 如果不传该参数,则 state 的初始值就是 initialArg

Returns


返回值是一个数组,内容有

  1. 当前的 state
  2. dispatch 函数,使用它可以更新 state 的值以及触发重新渲染

dispatch function


简单理解就是发号施令,传入 actionaction 可以是任意类型,一般是含有 type 属性的对象。逻辑上就是指定进行某种类型的更新。

无返回值

可以看出来 useReduceruseState 很相似,都是使用 state 和更新 state

  • 既然有 useState ,为什么还要使用 useReducer ?

    对于 useState Hook 而言,更新 state 就是调用 setState 函数,传入下一个 state 的值。而至于这个 next state 怎么来的,就需要组织逻辑了。这些逻辑往往是放在事件处理函数中的,比如一个点击事件让 state 增加或减少一定数值。然后我们会在事件处理函数中命令式地说明怎么加减。当逻辑变多时,事件处理函数的阅读性就会变差,从而导致组件的阅读性变差。

    useReducer 其实就是让这个事件处理函数中的更新逻辑从命令式变为了声明式,如 function handleClick() { dispatch({type: ‘add’}) } ,这样真正的更新逻辑其实放在 reducer 函数中,就是传给 useReducer Hook 的第一个参数,这样的一个函数是可以放在其他文件中的,需要使用的组件导入即可,这样对于组件而言,更新就是声明式的了,非常清晰。

  • 怎么写一个 reducer function ?

    接受参数 state and action ,需要注意的是这个 state 仅可读

    function reducer(state, action) {
    	//...........
    }
    

    因为是将更新逻辑集中起来,也就是说如何更新是分多种类型,就是需要条件语句,一般是使用 switch 语句

    返回值就是更新后 state ,当需要使用当前 state 时,也只是读而非写。遵从 useState 的规则

    function reducer(state, action) {
      switch (action.type) {
        case 'incremented_age': {
          return {
            name: state.name,
            age: state.age + 1
          };
        }
        case 'changed_name': {
          return {
            name: action.nextName,
            age: state.age
          };
        }
      }
      throw Error('Unknown action: ' + action.type);
    }