阅读视图

发现新文章,点击刷新页面。

Vue-Vue2中的Mixin 混入机制

在开发中,当多个组件拥有相同的逻辑(如分页、导出、权限校验)时,重复编写代码显然不够优雅。Vue 2 提供的 Mixin(混入)正是为了解决逻辑复用而生。本文将从基础用法出发,带你彻底理清 Mixin

Vue-Data 属性避坑指南

在 Vue 开发中,我们经常会遇到“明明修改了数据,视图却不更新”的尴尬场景。这通常与 Vue 的初始化顺序及响应式实现原理有关。本文将从 Data 属性的本质出发,解析响应式“丢失”的根本原因

Vue-组件通信全攻略

前言

在 Vue 开发中,组件通信是构建复杂应用的基础。随着 Vue 3 的普及,通信方式发生了不少变化(如 defineProps 的引入、EventBus 的退场)。本文将对比 Vue 2 与 Vue 3,带你梳理最常用的 5 种通信方案。

一、 父子组件通信:最基础的单向数据流

这是最常用的通信方式,遵循“Props 向下传递,Emit 向上通知”的原则。

1. Vue 2 经典写法

  • 接收:使用 props 选项。
  • 发送:使用 this.$emit

2. Vue 3 + TS 标准写法

在 Vue 3 <script setup> 中,我们使用 definePropsdefineEmits

父组件:Parent.vue

<template>
  <ChildComponent :id="currentId" @childEvent="handleChild" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './Child.vue';

const currentId = ref<string>('001');
const handleChild = (msg: string) => {
  console.log('接收到子组件消息:', msg);
};
</script>

子组件:Child.vue

<script setup lang="ts">
// 使用 TS 类型定义 Props
const props = defineProps<{
  id: string
}>();

// 使用 TS 定义 Emits,具备更好的类型检查
const emit = defineEmits<{
  (e: 'childEvent', args: string): void;
}>();

const sendMessage = () => {
  emit('childEvent', '这是来自子组件的参数');
};
</script>

二、 跨级调用:通过 Ref 访问实例

有时父组件需要直接调用子组件的内部方法。

1. Vue 2 模式

直接通过 this.$refs.childRef.someMethod() 调用。

2. Vue 3 模式(显式暴露)

Vue 3 的组件默认是关闭的。如果父组件想访问子组件的方法,子组件必须使用 defineExpose

子组件:Child.vue

<script setup lang="ts">
const childFunc = () => {
  console.log('子组件方法被调用');
};

// 必须手动暴露,父组件才能访问
defineExpose({
  childFunc
});
</script>

父组件:Parent.vue

<template>
  <Child ref="childRef" />
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import Child from './Child.vue';

// 这里的类型定义有助于获得代码提示
const childRef = ref<InstanceType<typeof Child> | null>(null);

onMounted(() => {
  childRef.value?.childFunc();
});
</script>

三、 非父子组件通信:事件总线 (EventBus)

1. Vue 2 做法

利用一个新的 Vue 实例作为中央调度器。

import Vue from 'vue';
export const EventBus = new Vue();

// 组件 A 发送
EventBus.$emit('event', data);
// 组件 B 接收
EventBus.$on('event', (data) => { ... });

2. Vue 3 重要变更

Vue 3 官方已移除了 $on$off$once 方法,因此不再支持直接通过 Vue 实例创建 EventBus。

  • 官方推荐方案:使用第三方库 mitttiny-emitter
  • 补充:如果逻辑简单,可以使用 Vue 3 的 provide / inject 实现跨级通信。

provide / inject 示例:

  1. 祖先组件:提供数据 (App.vue)
<template>
  <div class="ancestor">
    <h1>祖先组件</h1>
    <p>当前主题:{{ theme }}</p>
    <Middle />
  </div>
</template>

<script setup lang="ts">
import { ref, provide } from 'vue';
import Middle from './Middle.vue';

// 1. 定义响应式数据
const theme = ref<'light' | 'dark'>('light');

// 2. 定义修改数据的方法(推荐在提供者内部定义,保证数据流向清晰)
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light';
};

// 3. 注入 key 和对应的值/方法
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
  1. 中间组件:无需操作 (Middle.vue)

    中间组件不需要显式接收 theme,直接透传即可

  2. 后代组件:注入并使用 (DeepChild.vue)

<template>
  <div class="descendant">
    <h3>深层子组件</h3>
    <p>接收到的主题:{{ theme }}</p>
    <button @click="toggleTheme">切换主题</button>
  </div>
</template>

<script setup lang="ts">
import { inject } from 'vue';

// 使用 inject 获取,第二个参数为默认值(可选)
const theme = inject('theme');
const toggleTheme = inject<() => void>('toggleTheme');
</script>

四、 集中式状态管理:Vuex 与 Pinia

当应用变得庞大,组件间的关系交织成网时,我们需要一个“单一事实来源”。

  • Vuex:Vue 2 时代的标准。基于 Mutation(同步)和 Action(异步)。

  • Pinia:Vue 3 的官方推荐。

    • 优势:更完美的 TS 支持、没有 Mutation 的繁琐逻辑、极其轻量。
    • 核心stategettersactions

Pinia 示例:

import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '张三',
    age: 18
  }),
  actions: {
    updateName(newName: string) {
      this.name = newName;
    }
  }
});

五、 总结与纠错

  1. 安全性建议:在使用 defineExpose 时,尽量只暴露必要的接口,遵循最小暴露原则。
  2. EventBus 警示:Vue 3 开发者请注意,不要再尝试使用 new Vue() 来做事件总线,应当转向 Pinia 或全局状态。

React-Hooks逻辑复用艺术

前言

在 React 开发中,Hooks 的出现彻底改变了逻辑复用的方式。它让我们能够将复杂的、可复用的逻辑从 UI 组件中抽离,实现真正的“关注点分离”。本文将分享 Hooks 的核心原则,并提供 4 个在真实业务场景中封装的实战案例。

一、 Hooks 核心

1. 概念理解

Hooks 本质上是将组件间共享的逻辑抽离并封装成的特殊函数

2. 使用“红线”:规则与原理

  • 命名规范:必须以 use 开头(如 useChat),这不仅是约定,也是静态检查工具(ESLint)识别 Hook 的依据。
  • 调用位置严禁在循环、条件判断或嵌套函数中调用 Hook

底层原理: React 内部并不是通过“变量名”来记录 Hook 状态的,而是通过链表 。每次渲染时,React 严格依赖 Hook 的调用顺序来查找对应的状态。

注意: 如果在 if 语句中调用 Hook,一旦条件不成立导致某次渲染跳过了该 Hook,整个链表的指针就会错位,导致状态读取异常。

二、 实战:自定义 Hooks 封装

1. AI 场景:消息点赞/点踩逻辑 (useChatEvaluate)

在 AI 对话系统中,消息评价是通用功能。我们需要处理:状态切换(点赞 -> 取消点赞)、单选逻辑、以及异步接口调用。

import React, { useState } from 'react';

// 模拟接口
const public_evaluateMessage = async (params: any) => ({ data: true });

type EvaluateType = "GOOD" | "BAD" | "NONE";

export const useChatEvaluate = (initialType: EvaluateType = "NONE") => {
  const [ratingType, setRatingType] = useState<EvaluateType>(initialType);

  const evaluateMessage = async (contentId: number, type: "GOOD" | "BAD") => {
    let newEvaluateType: EvaluateType;

    // 逻辑:如果点击已选中的类型,则取消选中(NONE);否则切换到新类型
    if (type === "GOOD") {
      newEvaluateType = ratingType === "GOOD" ? "NONE" : "GOOD";
    } else {
      newEvaluateType = ratingType === "BAD" ? "NONE" : "BAD";
    }

    try {
      const res = await public_evaluateMessage({
        contentId,
        ratingType: newEvaluateType,
        content: "",
      });

      if (res.data === true) {
        setRatingType(newEvaluateType);
      }
    } catch (error) {
      console.error("评价失败:", error);
    }
  };

  return { ratingType, evaluateMessage };
};

// 使用示例
const ChatMessage: React.FC<{ id: number }> = ({ id }) => {
  const { ratingType, evaluateMessage } = useChatEvaluate();
  return (
    <button onClick={() => evaluateMessage(id, "GOOD")}>
      {ratingType === "GOOD" ? "👍 已点赞" : "👍 点赞"}
    </button>
  );
};

2. 响应式布局:屏幕尺寸监听 (useMediaSize)

在响应式系统中,封装一个能根据窗口宽度自动切换“设备类型”的 Hook,可以极大地简化响应式开发。

import { useState, useEffect, useMemo } from 'react';

export enum MediaType {
  mobile = 'mobile',
  tablet = 'tablet',
  pc = 'pc',
}

const useMediaSize = (): MediaType => {
  const [width, setWidth] = useState<number>(globalThis.innerWidth);

  useEffect(() => {
    const handleWindowResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleWindowResize);
    // 记得清理事件监听
    return () => window.removeEventListener('resize', handleWindowResize);
  }, []);

  // 使用 useMemo 避免每次渲染都重新运行计算逻辑
  const media = useMemo(() => {
    if (width <= 640) return MediaType.mobile;
    if (width <= 768) return MediaType.tablet;
    return MediaType.pc;
  }, [width]);

  return media;
};

export default useMediaSize;

3. 性能优化:防抖与节流 Hook

A. 防抖 Hook (useDebounce)

常用于搜索框,防止用户快速输入时频繁触发请求。

import { useState, useEffect } from 'react';

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // 关键:在下一次 useEffect 执行前清理上一次的定时器
    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;

B. 节流 Hook (useThrottle)

常用于滚动加载、窗口缩放,确保在规定时间内只执行一次。

import { useState, useEffect, useRef } from 'react';

function useThrottle<T>(value: T, delay: number): T {
  const [throttledValue, setThrottledValue] = useState<T>(value);
  const lastExecuted = useRef<number>(Date.now());

  useEffect(() => {
    const now = Date.now();
    const remainingTime = delay - (now - lastExecuted.current);

    if (remainingTime <= 0) {
      // 立即执行
      setThrottledValue(value);
      lastExecuted.current = now;
    } else {
      // 设置定时器处理剩余时间
      const timer = setTimeout(() => {
        setThrottledValue(value);
        lastExecuted.current = Date.now();
      }, remainingTime);

      return () => clearTimeout(timer);
    }
  }, [value, delay]);

  return throttledValue;
}

export default useThrottle;

三、 总结:封装自定义 Hook 的心法

  1. 抽离状态而非仅逻辑:如果一段逻辑只涉及纯函数计算,不需要 Hook;只有涉及 useStateuseEffect 等状态管理时,才有必要封装 Hook。
  2. 保持纯净:自定义 Hook 应该只关心逻辑,而不应该直接操作 DOM。
  3. TS 类型保护:利用泛型 <T> 增强 Hook 的兼容性,让它能适配各种数据类型。

React-Scheduler 调度器如何掌控主线程?

前言

在 React 18 的并发时代,Scheduler(调度器) 是实现非阻塞渲染的幕后英雄。它不只是 React 的一个模块,更是一个通用的、高性能的 JavaScript 任务调度库。它不仅让 React 任务可以“插队”,还让“长任务”不再阻塞浏览器 UI 渲染。

一、 核心概念:什么是 Scheduler?

Scheduler 是一个独立的包,它通过与 React 协调过程(Reconciliation)的紧密配合,实现了任务的可中断、可恢复、带优先级执行。

主要职责

  1. 优先级管理:根据任务紧急程度(如用户点击 vs 数据预取)安排执行顺序。
  2. 空闲时间利用:在浏览器每一帧的空闲时间处理不紧急的任务。
  3. 防止主线程阻塞:通过“时间片(Time Slicing)”机制,避免长任务导致页面假死。

二、 Scheduler 的完整调度链路

当一个 setState 触发后,Scheduler 内部会经历以下精密流程:

1. 任务创建与通知

当状态更新时,React 不会立即执行 Render。它首先会创建一个 Update对象来记录这次变更,这个对象中包含这次更新所需的全部信息,例如更新后的状态值,Lane车道模型分配的任务优先级.

2. 优先级排序与队列维护

  • 任务优先级排序: 创建更新后,react会调用scheduleUpdateOnFiber函数通知scheduler调度器有个一个新的任务需要调度,这时scheduler会对该任务确定一个优先级,以及过期时间(优先级越高,过期时间越短,表示越紧急)

  • 队列维护: 接着scheduler会将该任务放入到循环调度中,scheduler对于任务循环调度在内部维护着两个队列,一个是立即执行队列taskQueue和延迟任务队列timeQueue,新任务会根据优先级进入到相应对列

    • timerQueue(延时任务队列) :存放还未到开始时间的任务,按开始时间排序。
    • taskQueue(立即任务队列) :存放已经就绪的任务,按过期时间排序。优先级越高,过期时间越短。

3. 时间片的开启:MessageChannel

将任务放入队列后,scheduler会调用requetHostCallback函数去请求浏览器在合适的时机去执行调度,该函数通过 MessageChannel对象中的port.postMessage 方法创建一个宏任务,浏览器在下一个宏任务时机触发 port.onmessage,并在这宏任务回调中启动 workLoop函数。

补充:Scheduler 会调用 requestHostCallback 请求浏览器调度。它没有选择 setTimeout,而是选择了 MessageChannel

为什么选 MessageChannel? setTimeout(fn, 0) 在浏览器中通常有 4ms 的最小延迟,且属于宏任务中执行时机较晚的。MessageChannelport.postMessage 产生的宏任务执行时机更早,且能更精准地在浏览器渲染帧之间切入。

4. 工作循环:workLoop

  • 在宏任务回调中,调度器会进入 workLoop。它会调用performUnitOfWork函数循环地处理Fiber节点,对比新旧节点的props、state,并从队列中取出最紧急的任务交给 React 执行。

  • workLopp中会包含一个shouldYield函数中断检查函数,用于检查当前时间片是否耗尽以及是否有更高优先级的任务执行,如果有的话则会将主线程控制权交还给浏览器,以保证高优先级任务(如用户输入、动画)能及时响应。


5. 中断与恢复:shouldYield 的魔力

workLoop 执行过程中,每一项单元工作完成后,都会调用 shouldYield() 函数进行“路况检查”。

  • 中断条件:如果当前时间片(通常为 5ms)耗尽,或者检测到有更紧急的用户交互(高优任务插队),shouldYield 返回 true
  • 状态保存:此时 React 会记录当前 workInProgress 树的位置,将控制权交还给浏览器。
  • 任务恢复:Scheduler 会在下一个时间片通过 MessageChannel 再次触发,从记录的位置继续执行,从而实现可恢复。

6. 任务插队

如果在执行一个低优先级任务时,有高优先级任务加入(如用户突然点击按钮),Scheduler会中断当前的低优任务并记录该位置,先执行高优任务。等高优任务完成后,再重新执行或继续之前的低优任务


三、 补充

  1. 执行时机对比MessageChannel 确实在宏任务中非常快,但在某些极其特殊的情况下(如没有 MessageChannel 的旧环境),它会回退到 setTimeout
  2. 饥饿现象防止:如果一个低优先级任务一直被插队怎么办?Scheduler 通过过期时间解决。一旦任务过期,它会从 taskQueue 中被提升为同步任务,强制执行。

React-深度解析Diff 算法中Key 的作用

前言

在 React 开发中,我们经常会在控制台看到 Each child in a list should have a unique "key" prop 的警告。Key 到底是什么?它仅仅是一个为了消除警告的“随机字符串”吗?本文将带你从底层原理出发,看透 Key 在 Diff 算法中的核心价值。

一、 核心概念:什么是 Key?

Key 是 React 用于追踪列表元素身份的唯一辅助标识。它就像是每个 DOM 元素的“身份证号”,让 React 在复杂的更新过程中,能够精准地识别哪些元素被修改、添加或删除。

  • 唯一性:Key 必须在同级元素(Siblings)之间保持唯一。
  • 稳定性:一个元素的 Key 应该在其整个生命周期内保持不变,不建议使用 Math.random() 动态生成。

二、 Key 在 Diff 算法中的作用

React 的 Diff 算法通过 Key 来实现节点复用,这是性能优化的关键:

  1. 匹配新旧元素:当状态更新引发列表变化时,React 会对比新旧两棵虚拟 DOM 树,寻找具有相同 Key 的元素。
  2. 复用现有节点:如果 Key 相同,React 会认为这是同一个组件实例。它会选择复用现有的 DOM 节点和组件状态,仅仅更新发生变化的属性(如 textContent 或 className)。
  3. 减少重绘:由于复用了节点,浏览器不需要执行昂贵的“销毁旧节点 -> 创建新节点”操作,极大提高了更新效率。

三、 实战:Key 的正确用法

在 TSX 中,当我们使用 map 方法渲染列表时,务必在返回的最外层标签上绑定 Key。

import React, { useState } from 'react';

interface Todo {
  id: string; // 唯一标识符
  text: string;
}

const TodoList: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([
    { id: '1', text: '学习 React' },
    { id: '2', text: '整理掘金笔记' }
  ]);

  return (
    <ul>
      {/* 这里的 Key 使用数据的唯一 ID */}
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
          <input type="checkbox" />
        </li>
      ))}
    </ul>
  );
};

export default TodoList;

四、 注意事项:为什么不能盲目使用 Index 作为 Key?

很多新手喜欢直接用数组的 index 作为 Key,但在逆序添加、删除或排序列表时,这会导致严重的性能问题和 UI Bug。

1. 性能降低

假设你在列表头部插入一条数据,原来的 index 0 变成了 index 1。React 会发现 Key 对应的“数据”变了,从而导致原本可以复用的节点全部被迫重新渲染(Re-render)。

2. 状态错位 Bug

如果列表项中包含非受控组件(如 <input />),使用 Index 作为 Key 会导致输入框内容“串位”。因为 React 认为 Key 没变,就复用了旧的 Input 节点及其内部的本地状态。


五、 总结与最佳实践

  • 首选方案:使用来自数据库的唯一 ID(如 UUID 或主键 ID)。
  • 备选方案:如果数据确实是静态的(永远不会排序、过滤、增删),且没有唯一 ID,可以使用 Index。
  • 禁忌:绝对不要在渲染时使用 Math.random()Date.now() 生成 Key。这会导致每次渲染 Key 都不同,React 将无法复用任何节点,造成巨大的性能浪费。

React-深度拆解 React Render 机制

前言

在 React 中,我们常说“渲染(Render)”,但它不仅仅是将 HTML 丢给浏览器那么简单。Render 是一个包含 计算(Reconciliation)提交(Commit) 的复杂过程。理解这一过程,能帮助我们写出更高性能的代码。

一、 Render 的核心三部曲

当 React 决定更新界面时,会经历以下三个关键阶段:

1. 创建虚拟 DOM (Virtual DOM)

JSX 本质上是 React.createElement() 的语法糖。Babel 会将 JSX 编译为 JS 调用,生成一个描述 UI 的对象树(即虚拟 DOM)。

结构定义参考:

// 编译后的逻辑(简化版)
const vDom = {
  type: 'div',
  props: {
    className: 'active',
    children: 'Hello'
  }
};

2. Diff 算法比较 (Reconciliation)

React 并不会盲目替换整个 DOM,而是通过 Diff 算法 对比“新旧两棵虚拟 DOM 树”。

  • 同层比较:只比较同一层级的节点。
  • 类型检查:如果节点类型变了(如 divp),则直接销毁重建。
  • Key 值优化:通过 key 属性识别节点是否只是移动了位置。

3. 渲染真实 DOM (Commit)

在计算出最小差异(Patches)后,React 的渲染器(如 react-dom)会将这些变更同步到真实浏览器环境,触发重绘与回流,使用户看到更新。


二、 触发渲染的四大时机

在函数式组件中,Render 过程可能由以下四种情况触发:

触发场景 描述
首次渲染 应用启动,将组件树完整挂载到页面上。
State 改变 当调用 useStateset 函数或 useReducerdispatch 时。
Props 改变 父组件重新渲染导致传给子组件的属性发生变化。
Context 改变 组件通过 useContext 订阅了上下文,且 Providervalue 发生变更。

三、 实战演示:观测渲染行为

我们可以通过简单的日志输出,来观察不同场景下的渲染行为。

import React, { useState, useContext, createContext } from 'react';

// 创建 Context
const AppContext = createContext(0);

// 子组件
const Child: React.FC<{ count: number }> = ({ count }) => {
  console.log("子组件 Render...");
  return <div>父级传入的 Props: {count}</div>;
};

// 顶层组件
const Home: React.FC = () => {
  const [num, setNum] = useState<number>(0);
  const [other, setOther] = useState<boolean>(false);

  console.log("Home 组件 Render...");

  return (
    <AppContext.Provider value={num}>
      <div style={{ padding: '20px' }}>
        <h2>Render 触发测试</h2>
        
        {/* 1. 修改 State 触发 */}
        <button onClick={() => setNum(prev => prev + 1)}>
          修改 State (Count: {num})
        </button>

        {/* 2. 这里的修改虽然没传给 Child,但父组件重新渲染会导致 Child 也重新渲染 */}
        <button onClick={() => setOther(!other)}>
          无关渲染测试: {String(other)}
        </button>

        {/* 3. Props 改变触发子组件渲染 */}
        <Child count={num} />
      </div>
    </AppContext.Provider>
  );
};

export default Home;

React-useImmerReducer进阶

在React 中,useReducer 是处理复杂状态的首选方案。但 Reducer 要求必须返回一个新的状态对象,这导致代码中充斥着大量的展开运算,通过结合 Immer,可以像修改对象一样操作状态
❌