Skip to content

React hooks 核心原理和实践

认识 React

使用组件的方式描述 UI

在 React 中,所有的 UI 都是通过组件去描述和组织的。

React 中所有的元素都是组件,具体而言分为两种:

  1. 内置组件。内置组件其实就是映射到 HTML 节点的组件,例如 divinputtable 等等,作为一种约定,它们都是小写字母。

  2. 自定义组件。自定义组件其实就是自己创建的组件,使用时必须以大写字母开头,例如 TopicListTopicDetail

理解 JSX 语法本质

JSX 是模板语言,这里的"模板语言"是加了引号的,因为从本质上来说,JSX 并不是一个新的模板语言,而可以认为是一个语法糖。也就是说,不用 JSX 的写法,其实也是能够写 React 的。

ts
React.createElement(
  "div",
  null,
  React.createElement(
    "button",
    { onClick: function onClick() {
        return setCount(count + 1);
      } },
    React.createElement(CountLabel, { count: count })
  )
);

React.createElement(componentName, props, childCompnentName)

作用就是创建一个组件的实例,接收一组参数:

  1. 第一个参数表示组件的类型
  2. 第二个参数是传给组件的属性,也就是 props
  3. 第三个以及后续所有的参数则是子组件

理解 Hooks

在 React 中,Hooks 就是把某个目标结果【DOM 结构】钩到某个可能会变化的数据源或者事件源【状态】上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

Hooks 只能在函数组件的顶级作用域使用所谓顶层作用域,

Hooks 不能在循环、条件判断或者嵌套函数内执行,而必须是在顶层。

Hooks 在组件的多次渲染之间,必须按顺序被执行。

useState

  1. state 里,不要保存通过计算获得的数据

从 props 传递过来的值。有时候 props 传递过来的值无法直接使用,而是要通过一定的计算后再在 UI 上展示,比如说排序。那么我们要做的就是每次用的时候,都重新排序一下,或者利用某些 cache 机制,而不是将结果直接放到 state 里。

从 URL 中读到的值。比如有时需要读取 URL 中的参数,把它作为组件的一部分状态。那么我们可以在每次需要用的时候从 URL 中读取,而不是读出来直接放到 state 里。

从 cookie、localStorage 中读取的值。通常来说,也是每次要用的时候直接去读取,而不是读出来后放到 state 里。

  1. useState 异步问题导致获取数据不及时问题?
ts
const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1); // 重点是在【快照】这个概念,count 其实是 count 的快照
  setCount(count + 1);
  setCount(count + 1);
};

useEffect(() => {
  console.log(count); // count 依然为 0
}, [count]);

但是如果我们采用回调的方式来获取数据

ts
const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);

  // console.log(count) 他还是 0
};

useEffect(() => {
  console.log(count); // 这里是最新的值 3
}, [count]);

React 将这三次操作都放在了队列里面,但是由于 count 属于快照,所以一直是 0,也就是他进行了,三次的 setCount(0+1) 的操作;【批处理】

由于 React 识别到三次进行了相同的操作,那么他就会将操作进行合并【合并机制】

理解为什么 setCount(prevCount => prevCount + 1); 这种写法就可以?

prevCount 这个值并不是我传递给 React 的,是他自己内部进行维护的,他会拿到上一个的状态

⭐如果你想在一个函数中多次改变状态,那么你需要使用回调函数

React 状态更新机制

对于任何函数,只要进入了 React 的调度流程,那就是异步的。

只要没有进入 React 的调度流程,那就是同步的。

由 React 引发的事件处理(如 onClick 等)就会进入 React 的调度流程;而诸如 setTimeoutsetInterval 或者直接在 DOM 上绑定原生事件等,这些都不会走 React 的调度流程。

在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 state 还是放到队列中回头再说,而 isBatchingUpdates 默认是 false,也就表示 setState 默认会同步更新 state。而 batchedUpdates 这个函数会把 isBatchingUpdates 修改为 true

为了合并 setState,我们需要一个队列来保存每次 setState 的数据,然后在一段时间后,清空这个队列并渲染组件,这个队列就是 dirtyComponents。当 isBatchingUpdatestrue 时,将会执行 dirtyComponents.push(component),将组件 pushdirtyComponents 队列。调用 setState 时,其实已经调用了 batchedUpdates,此时 isBatchingUpdates 便是 true。因此展示出异步合并更新

useEffect

在函数组件的当次执行过程中,useEffect 中代码的执行是不影响渲染出来的 UI 的。

ts
useEffect(() => {
    // useEffect 的 callback 要避免直接的 async 函数,需要封装一下
    const doAsync = async () => {
      // 当 id 发生变化时,将当前内容清楚以保持一致性
      setBlogContent(null);
      // 发起请求获取数据
      const res = await fetch(`/blog-content/${id}`);
      // 将获取的数据放入 state
      setBlogContent(await res.text());
    };
    doAsync();
  }, [id]); // 使用 id 作为依赖项,变化时则执行副作用

useEffect 让我们能够在下面四种时机去执行一个回调函数产生副作用:

  1. 每次 render 后执行:不提供第二个依赖项参数。比如 useEffect(() => {})
  2. 第一次 render 后执行:提供一个空数组作为依赖项。比如 useEffect(() => {}, [])
  3. 第一次以及依赖项发生变化后执行:提供依赖项数组。比如 useEffect(() => {}, [deps])
  4. 组件 unmount 后执行:返回一个回调函数。比如 useEffect() => { return () => {} }, [])

useCallback

ts
useCallback(fn, deps)

定义:这里 fn 是定义的回调函数,deps 是依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明 fn 这个回调函数。

ts
import React, { useState, useCallback } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const handleIncrement = useCallback(
    () => setCount(count + 1),
    [count], // 只有当 count 发生变化时,才会重新创建回调函数
  );
  // ...
  return <button onClick={handleIncrement}>+</button>
}

问题:为什么不能直接用 useEffect 代替 useCallback? 既然他们都存在一种含义:一个值发生改变的时候,去执行某一个函数

useEffect 主要用于处理副作用(比如 fetchData订阅组装数据 等)。

useCallback 返回一个函数,这个函数可以直接在 JSX 里使用,比如 onClick 事件。

useEffect 不会返回函数,它的作用只是执行某些逻辑,不能直接作为事件处理函数。

✔正确写法

ts
import React, { useState, useCallback } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  // 只有 count 变化时,才会创建新的 handleClick
  const handleClick = useCallback(() => {
    console.log("Count:", count);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={handleClick}>Log Count</button>
    </div>
  );
}

❌错误写法

ts
import React, { useState, useEffect } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  let handleClick;

  useEffect(() => {
    handleClick = () => {
      console.log("Count:", count);
    };
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={handleClick}>Log Count</button> {/* ❌ 这里会报错 */}
    </div>
  );
}

useMemo

ts
useMemo(fn, deps);

如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。=>类似于 Vue 的计算属性

useCallback 的功能其实是可以用 useMemo 来实现的

ts
 const myEventHandler = useMemo(() => {
   // 返回一个函数作为缓存结果
   return () => {
     // 在这里进行事件处理
   }
 }, [dep1, dep2]);

useRef

  1. useRef 还有一个重要的功能,就是保存某个 DOM 节点的引用。
  2. 在多次渲染之间共享数据
  3. 使用 useRef 保存的数据一般是和 UI 的渲染无关的,因此当 ref 的值发生变化时,是不会触发组件的重新渲染的,这也是 useRef 区别于 useState 的地方。

useContext

React 提供了 Context 【上下文】一个机制,能够让所有在某个组件开始的组件树上创建一个 Context。这样这个组件树上的所有组件,就都能访问和修改这个 Context 了。

ts
// 创建 context
const MyContext = React.createContext(initialValue);

// 使用 context
const value = useContext(MyContext);
ts
const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
// 创建一个 Theme 的 Context

const ThemeContext = React.createContext(themes.light);
function App() {
  // 整个应用使用 ThemeContext.Provider 作为根组件
  return (
    // 使用 themes.dark 作为当前 Context
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{
      background: theme.background,
      color: theme.foreground
    }}>
      I am styled by theme context!
    </button>
  );
}

当然如果需要使用状态管理,对于小型的项目,直接使用 useModel 也是一个好办法。

React 生命周期

alt text

React 存在了多个版本,React 16 之前,React 16 以后【Class 写法和 Hooks 写法】,无论版本是什么,React 的生命周期主要包含三个部分:挂载,更新,卸载。

React 16 之前(Class 组件)

  • 挂载
    • constructor
    • render
    • componentDidMount
  • 更新
    • componentWillReceiveProps(React 16.3 弃用)
    • shouldComponentUpdate:控制是否更新,如果返回 false,则不会调用 rendercomponentWillUpdatecomponentDidUpdate
    • componentWillUpdate(React 16.3 弃用)
    • render
    • componentDidUpdate
  • 卸载
    • componentWillUnmount

React 16 之后(Class 组件)

  • 挂载
    • constructor
    • getDerivedStateFromProps(新增)
    • render
    • componentDidMount
  • 更新
    • getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate 【获取更新前的 DOM 状态(旧的 DOM)】
    • componentDidUpdate
  • 卸载
    • componentWillUnmount
Class 的生命周期对应 Hooks
挂载【componentDidMount】useEffect(()=>{},[])
更新【componentDidUpdate】useEffect(() => {}, [state]) // 根据状态来进行更新
卸载【componentWillUnmount】useEffect(() => { return () => {} }, [])
是否更新【shouldComponentUpdate】useMemo() / useCallback()
getDerivedStateFromPropsuseEffect(() => {}, [props])
getSnapshotBeforeUpdateuseEffect + useRef

引申

父子组件更新问题

当父组件更新的时候,子组件无论是否更新;

如果你不想让他进行更新:

  1. 子组件变为 memo 组件;
  2. 父组件内使用 useCallback 避免函数 props 变化

Diff 算法

比较的是什么呢?在哪里比较?

定义:从上而下,同层比较。比较的是 V-DOM 树,比较的 VDOM 节点,【Fiber 节点】

React 采用了 “同层 Diff” 策略,只比较同一层的 Fiber 节点,不跨层级比较:

  1. 如果类型相同 → 复用节点,更新 props
  2. 如果类型不同 → 删除旧 Fiber,创建新 Fiber
  3. 如果子元素发生变化 → 递归比较子 Fiber,使用 Key 进行优化

Fiber 结构是什么?

在本质上,Fiber 是一个 JavaScript 对象,代表 React 的一个工作单元,它包含了与组件相关的信息。

ts
{
  type: 'h1',  // 组件类型
  key: null,   // React key
  props: { ... }, // 输入的 props
  state: { ... }, // 组件的 state (如果是 class 组件或带有 state 的 function 组件)
  child: Fiber | null,  // 第一个子元素的 Fiber
  sibling: Fiber | null,  // 下一个兄弟元素的 Fiber
  return: Fiber | null,  // 父元素的 Fiber
  // ...其他属性
}

alt text

因为 React 在更新时总是维护了两个 Fiber 树,所以可以随时进行比较、中断或恢复等操作,而且这种机制让 React 能够同时具备拥有优秀的渲染性能和 UI 的稳定性。【这个应该在 getSnapshotBeforeUpdate 这个时候,进行判断】

万字长文介绍 React Fiber 架构的原理和工作模式

Fiber 不是单纯的节点结构,而是 React 重新设计的核心架构,优化了渲染性能!

  1. 使用 Fiber 作为数据结构,更容易管理组件更新状态
  2. 支持可中断的渲染,React 可以暂停低优先级任务,优先处理用户交互
  3. 任务优先级调度(Time Slicing),不同任务有不同的执行优先级
  4. 双缓存机制(双 Fiber 树),避免页面闪烁,提高渲染效率