以下内容我在阅读官方文档中的 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?
正如标题所言,当父组件要传递数据到深层的子组件时,如果简单使用传递 props ,props 就会经过许多组件,而部分组件并不需要这些 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
返回值是一个数组,内容有
- 当前的 state
dispatch函数,使用它可以更新 state 的值以及触发重新渲染
dispatch function
简单理解就是发号施令,传入 action ,action 可以是任意类型,一般是含有 type 属性的对象。逻辑上就是指定进行某种类型的更新。
无返回值
可以看出来 useReducer 与 useState 很相似,都是使用 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); }