阅读视图
Vue-插槽 (Slot) 的多种高级玩法
Vue-Key唯一标识作用
Vue-Computed 与 Watch 深度解读与选型指南
Vue-深度拆解 v-if 、 v-for 、 v-show
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> 中,我们使用 defineProps 和 defineEmits。
父组件: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。
-
官方推荐方案:使用第三方库
mitt或tiny-emitter。 -
补充:如果逻辑简单,可以使用 Vue 3 的
provide/inject实现跨级通信。
provide / inject 示例:
- 祖先组件:提供数据 (
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>
-
中间组件:无需操作 (
Middle.vue)中间组件不需要显式接收
theme,直接透传即可 -
后代组件:注入并使用 (
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 的繁琐逻辑、极其轻量。
-
核心:
state、getters、actions。
Pinia 示例:
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
name: '张三',
age: 18
}),
actions: {
updateName(newName: string) {
this.name = newName;
}
}
});
五、 总结与纠错
-
安全性建议:在使用
defineExpose时,尽量只暴露必要的接口,遵循最小暴露原则。 -
EventBus 警示:Vue 3 开发者请注意,不要再尝试使用
new Vue()来做事件总线,应当转向 Pinia 或全局状态。
Vue-从 Vue 2 到 Vue 3:生命周期全图鉴与实战指南
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 的心法
-
抽离状态而非仅逻辑:如果一段逻辑只涉及纯函数计算,不需要 Hook;只有涉及
useState或useEffect等状态管理时,才有必要封装 Hook。 - 保持纯净:自定义 Hook 应该只关心逻辑,而不应该直接操作 DOM。
-
TS 类型保护:利用泛型
<T>增强 Hook 的兼容性,让它能适配各种数据类型。
React-Scheduler 调度器如何掌控主线程?
前言
在 React 18 的并发时代,Scheduler(调度器) 是实现非阻塞渲染的幕后英雄。它不只是 React 的一个模块,更是一个通用的、高性能的 JavaScript 任务调度库。它不仅让 React 任务可以“插队”,还让“长任务”不再阻塞浏览器 UI 渲染。
一、 核心概念:什么是 Scheduler?
Scheduler 是一个独立的包,它通过与 React 协调过程(Reconciliation)的紧密配合,实现了任务的可中断、可恢复、带优先级执行。
主要职责
- 优先级管理:根据任务紧急程度(如用户点击 vs 数据预取)安排执行顺序。
- 空闲时间利用:在浏览器每一帧的空闲时间处理不紧急的任务。
- 防止主线程阻塞:通过“时间片(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 的最小延迟,且属于宏任务中执行时机较晚的。MessageChannel的port.postMessage产生的宏任务执行时机更早,且能更精准地在浏览器渲染帧之间切入。
4. 工作循环:workLoop
-
在宏任务回调中,调度器会进入
workLoop。它会调用performUnitOfWork函数循环地处理Fiber节点,对比新旧节点的props、state,并从队列中取出最紧急的任务交给 React 执行。 -
workLopp中会包含一个shouldYield函数中断检查函数,用于检查当前时间片是否耗尽以及是否有更高优先级的任务执行,如果有的话则会将主线程控制权交还给浏览器,以保证高优先级任务(如用户输入、动画)能及时响应。
5. 中断与恢复:shouldYield 的魔力
在 workLoop 执行过程中,每一项单元工作完成后,都会调用 shouldYield() 函数进行“路况检查”。
-
中断条件:如果当前时间片(通常为 5ms)耗尽,或者检测到有更紧急的用户交互(高优任务插队),
shouldYield返回true。 -
状态保存:此时 React 会记录当前
workInProgress树的位置,将控制权交还给浏览器。 -
任务恢复:Scheduler 会在下一个时间片通过
MessageChannel再次触发,从记录的位置继续执行,从而实现可恢复。
6. 任务插队
如果在执行一个低优先级任务时,有高优先级任务加入(如用户突然点击按钮),Scheduler会中断当前的低优任务并记录该位置,先执行高优任务。等高优任务完成后,再重新执行或继续之前的低优任务
三、 补充
-
执行时机对比:
MessageChannel确实在宏任务中非常快,但在某些极其特殊的情况下(如没有MessageChannel的旧环境),它会回退到setTimeout。 -
饥饿现象防止:如果一个低优先级任务一直被插队怎么办?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 来实现节点复用,这是性能优化的关键:
- 匹配新旧元素:当状态更新引发列表变化时,React 会对比新旧两棵虚拟 DOM 树,寻找具有相同 Key 的元素。
- 复用现有节点:如果 Key 相同,React 会认为这是同一个组件实例。它会选择复用现有的 DOM 节点和组件状态,仅仅更新发生变化的属性(如 textContent 或 className)。
- 减少重绘:由于复用了节点,浏览器不需要执行昂贵的“销毁旧节点 -> 创建新节点”操作,极大提高了更新效率。
三、 实战: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 树”。
- 同层比较:只比较同一层级的节点。
-
类型检查:如果节点类型变了(如
div变p),则直接销毁重建。 -
Key 值优化:通过
key属性识别节点是否只是移动了位置。
3. 渲染真实 DOM (Commit)
在计算出最小差异(Patches)后,React 的渲染器(如 react-dom)会将这些变更同步到真实浏览器环境,触发重绘与回流,使用户看到更新。
二、 触发渲染的四大时机
在函数式组件中,Render 过程可能由以下四种情况触发:
| 触发场景 | 描述 |
|---|---|
| 首次渲染 | 应用启动,将组件树完整挂载到页面上。 |
| State 改变 | 当调用 useState 的 set 函数或 useReducer 的 dispatch 时。 |
| Props 改变 | 父组件重新渲染导致传给子组件的属性发生变化。 |
| Context 改变 | 组件通过 useContext 订阅了上下文,且 Provider 的 value 发生变更。 |
三、 实战演示:观测渲染行为
我们可以通过简单的日志输出,来观察不同场景下的渲染行为。
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;