普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月1日首页

Vue-Data 属性避坑指南

2026年1月31日 22:34

前言

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

一、 组件中的 Data 为什么必须是函数?

在 Vue 2 中,根实例的 data 可以是对象,但组件中的 data 必须是函数

核心原因:数据隔离

  • 对象形式:JavaScript 中的对象是引用类型。如果 data 是对象,所有组件实例将共享同一个内存地址。修改实例 A 的数据,实例 B 也会跟着变。
  • 函数形式:当 data 是一个函数时,每次创建新实例,Vue 都会调用该函数,返回一个全新的数据对象拷贝。这保证了每个组件实例数据的独立性。

二、 Props 与 Data 的优先级之争

在组件初始化时,Vue 会按照特定的顺序处理选项。

初始化顺序

PropsMethodsDataComputedWatch

因为 Props 最先被初始化,所以我们可以在 data 中直接引用 props 传来的值

// Vue 3 + TS 示例
const props = defineProps<{ initialCount: number }>();
const count = ref(props.initialCount); // 合法,因为 props 优先初始化

三、 Vue2动态添加新属性的“失效”困局

1. 故障场景

vue2中当我们直接给对象添加一个原本不存在的属性时,视图不会产生任何变化。

<p v-for="(value,key)in item" :key="key">
    {{ value }}
</p>
<button@click="addProperty">动态添加新属性</button>

const app = new Vue({
  el: '#app',
  data: {
    item: {
      oldProperty: 'l日属性'
    }
  },
  methods: {
    addProperty() {
      this.items.newProperty = '新属性'; // 为items添加新属性
      console.log(this.items); // 输出带有newProperty的items
    }
  }
})

2. 原因剖析

  • Vue 2 局限性:使用 Object.defineProperty 实现响应式。它只能劫持对象已有的属性。对于后来新增的属性,Vue 无法感知其 getter/setter,因此无法触发视图更新。
  • Vue 3 的进化:改用 Proxy 代理整个对象。Proxy 可以拦截到属性的新增与删除,因此 Vue 3 不再有这个问题。

四、 解决方案(Vue 2 必备技巧)

如果你仍在使用 Vue 2,可以通过以下三种方式解决:

1. 推荐方案:Vue.set / this.$set

这是最正统的方法,它会手动将新属性转为响应式,并触发依赖更新。

语法: this.$set(target, propertyName/index, value)

  • target:data中要修改的对象或者数组
  • propertyName/index:要添加或修改的属性名称(对于对象)或索引(对于数组)
  • value:要设置的值
addProperty() {
   this.$set(this.item, 'newProperty', '新属性'); 
}

2. 对象整体替换:Object.assign

通过创建一个包含新属性的新对象,并将这个新对象赋值给原有对象,触发 Vue 对原对象引用的变更感知。

addProperty() {
   this.item = Object.assign({}, this.item, { newProperty: '新属性' });
   // 或者使用展开运算符
   this.item = { ...this.item, newProperty: '新属性' };
}

3. 暴力方案:$forceUpdate

迫使 Vue 重新渲染组件。

  • 注意:这只是“治标”。虽然视图刷新了,但该属性依然不是响应式的。后续再次修改 newProperty 时,视图依然不会动。

五、 Vue 3 + TS 最佳实践

在 Vue 3 中,借助 TypeScript 的类型定义,我们可以规避大部分因“动态添加”导致的逻辑混乱。

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

// 定义接口,提前声明可选属性
interface Item {
  oldProperty: string;
  newProperty?: string; // 声明可选属性
}

const item = reactive<Item>({
  oldProperty: '旧属性'
});

const addProperty = () => {
  // Vue 3 Proxy 自动处理响应式,无需 $set
  item.newProperty = '新属性'; 
};
</script>
昨天 — 2026年1月31日首页

Vue-组件通信全攻略

2026年1月31日 13:38

前言

在 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 或全局状态。

Vue-从 Vue 2 到 Vue 3:生命周期全图鉴与实战指南

2026年1月31日 12:19

前言

生命周期钩子(Lifecycle Hooks)是 Vue 组件从诞生到销毁的全过程记录。掌握生命周期,不仅能让我们在正确的时间点执行逻辑,更是优化性能、排查内存泄露的关键。

一、 生命周期四大阶段

Vue 的生命周期大体可分为:创建、挂载、更新、销毁


二、 Vue 2 vs Vue 3 生命周期对比图

在 Vue 3 组合式 API 中,生命周期钩子需要从 vue 中导入,且命名上增加了 on 前缀。

阶段 Vue 2 (选项式 API) Vue 3 (组合式 API) 备注
创建 beforeCreate / created setup() Vue 3 中 setup 包含了这两个时期
挂载 beforeMount / mounted onBeforeMount / onMounted 常用:操作 DOM、请求接口
更新 beforeUpdate / updated onBeforeUpdate / onUpdated 响应式数据变化时触发
销毁 beforeDestroy / destroyed onBeforeUnmount / onUnmounted 注意:Vue 3 中命名的变更
缓存 activated / deactivated onActivated / onDeactivated 配合 <keep-alive> 使用

三、 详细解析与实战场景

1. 创建阶段 (Creation)

  • Vue 2 (beforeCreate / created)

    • beforeCreate:组件实例刚在内存中被创建,此时还没有初始化好 datamethods 属性。适合插件开发,注入全局变量。
    • created:实例已创建,响应式数据data、methods 已准备好。
      • 场景:最早可发起异步请求的时机。
  • Vue 3 (setup)

    • 在 Vue 3 中,setup 的执行早于 beforeCreate,它是组合式 API 的入口。

2. 挂载阶段 (Mounting)

  • Vue 2 (beforeMount / mounted)
    • beforeMount:此时已经完成了模板的编译,但是还没有挂载到页面中

    • mounted:此时已经将编译好的模板挂载到了页面指定的容器中,可以访问页面中的dom了

      • 场景:dom已创建,可用于获取接口数据和dom元素、访问子组件

  • Vue 3 (onBeforeMount / onMounted)
    • onBeforeMount:模板编译完成,但尚未渲染到 DOM 树中。

    • onMounted:组件已挂载,可以安全地访问 DOM 元素。

      • 场景:获取接口数据、初始化第三方插件(如 ECharts)、访问子组件。

3. 更新阶段 (Updating)

  • Vue 2 (beforeUpdate / updated)

    • beforeUpdate:数据状态更新之前执行,此时 data 中的状态值是最新的,但是界面上显示的数据还是旧的,因为此时还没有开始重新渲染DOM节点。

      • 场景 :此时view层还未更新,可用于获取更新前各种状态。
    • updated:实例更新完毕之后调用,此时 data 中的状态值和界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了。

  • Vue 3 (onBeforeUpdate / onUpdated)

    • onBeforeUpdate:数据已更新,但 DOM 尚未重新渲染。可用于获取更新前的 DOM 状态。
    • onUpdated:DOM 已完成更新。注意:不要在此钩子中修改状态,否则可能导致死循环。

4. 销毁阶段 (Unmounting / Destruction)

  • Vue 2 (beforeDestroy / destroyed)

    • beforeDestroy:实例销毁之前调用。
      • 场景:清理工作,如 清除定时器 (setInterval)、解绑全局事件监听、取消订阅
    • destroyed:Vue 实例销毁后调用。组件彻底从 DOM 中移除,所有的指令和事件监听都会被解除。
  • Vue 3 (onBeforeUnmount / onUnmounted)

    • onBeforeUnmount:实例销毁之前调用。

    • onUnmounted:组件彻底从 DOM 中移除,所有的指令和事件监听都会被解除。

5. 缓存阶段 (Keep-alive)

如果使用了keep-alive缓存组件会新增两个生命周期函数

  • onActivated:组件进入视野,被重新激活时调用。
  • onDeactivated:组件移出视野,进入缓存状态时调用。

四、 Vue 3 + TypeScript 实战演示

以下是使用 script setup 语法编写的生命周期示例:

<template>
  <div ref="container">
    <h2>当前计数:{{ count }}</h2>
    <button @click="count++">增加</button>
  </div>
</template>

<script setup lang="ts">
import { 
  ref, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated, 
  onBeforeUnmount 
} from 'vue'

const count = ref<number>(0)
const container = ref<HTMLElement | null>(null)
let timer: number | null = null

// 挂载阶段
onMounted(() => {
  console.log('Component Mounted. DOM element:', container.value)
  // 模拟一个定时任务
  timer = window.setInterval(() => {
    console.log('Timer running...')
  }, 1000)
})

// 运行阶段
onBeforeUpdate(() => {
  console.log('Data updated, but DOM is not yet re-rendered.')
})

onUpdated(() => {
  console.log('Data updated and DOM re-rendered.')
})

// 销毁阶段
onBeforeUnmount(() => {
  console.log('Cleanup before unmount.')
  if (timer) {
    clearInterval(timer) // 关键:防止内存泄漏
  }
})
</script>

五、 进阶:父子组件生命周期执行顺序

为了清晰起见,我们将顺序拆解为三个主要场景(vue3):

1. 初始挂载阶段

父组件必须等待所有子组件挂载完成后,才能完成自己的挂载逻辑。

  1. setup(开始创建)
  2. onBeforeMount
  3. setup
  4. onBeforeMount
  5. onMounted (子组件渲染完毕,向上通知)
  6. onMounted (父组件接收到信号,宣布整体挂载完毕)

记忆口诀: 父创 -> 子创 -> 子挂 -> 父挂。


2. 更新阶段

当父组件传递给子组件的 props 发生变化时,更新逻辑如下:

  • onBeforeUpdate
  • onBeforeUpdate
  • onUpdated
  • onUpdated

注意: 如果只是父组件自身的私有状态更新,且未影响到子组件,则子组件的更新钩子不会被触发。


3. 销毁阶段

销毁过程同样是“递归”式的,父组件先启动销毁,等子组件销毁完毕后,父组件正式功成身退。

  1. onBeforeUnmount
  2. onBeforeUnmount
  3. unmounted
  4. onUnmounted

六、 Vue 3 + TS 模拟演示

你可以通过以下代码在控制台直接观察执行逻辑。

父组件 Parent.vue

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

console.log('1. 父 - setup')

onBeforeMount(() => console.log('3. 父 - onBeforeMount'))
onMounted(() => console.log('8. 父 - onMounted'))
</script>

<template>
  <div class="parent">
    <h1>父组件</h1>
    <Child />
  </div>
</template>

子组件 Child.vue

<script setup lang="ts">
import { onMounted, onBeforeMount } from 'vue'

console.log('4. 子 - setup')

onBeforeMount(() => console.log('6. 子 - onBeforeMount'))
onMounted(() => console.log('7. 子 - onMounted'))
</script>

<template>
  <div class="child">子组件内容</div>
</template>

📝 总结与避坑

  1. 接口请求放哪里?

    • 如果子组件的渲染依赖父组件接口返回的数据,请在父组件的 created(Vue 2)或 setup(Vue 3)中请求。
    • 注意:即便你在父组件的 onMounted 发请求,子组件此时也已经渲染完成了。
  2. Refs 访问时机

    • 父组件想通过 ref 访问子组件实例,必须在父组件的 onMounted 之后,因为只有这时子组件才真正挂载完成。
  3. 异步组件

    • 如果子组件是异步组件(如使用 defineAsyncComponent),顺序会发生变化,父组件可能会先执行 onMounted
昨天以前首页

React-Hooks逻辑复用艺术

2026年1月30日 17:38

前言

在 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 调度器如何掌控主线程?

2026年1月30日 17:21

前言

在 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 的作用

2026年1月30日 10:33

前言

在 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 机制

2026年1月30日 09:49

前言

在 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;
❌
❌