普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月15日首页

【vue篇】Vue 模板编译原理:从 Template 到 DOM 的翻译官

作者 LuckySusu
2025年10月15日 18:18

在 Vue 项目中,你写的:

<template>
  <div class="user" v-if="loggedIn">
    Hello, {{ name }}!
  </div>
</template>

最终变成了浏览器能执行的 JavaScript 函数。
这背后,就是 Vue 模板编译器 在默默工作。

本文将深入解析 Vue 模板编译的三大核心阶段parseoptimizegenerate,带你揭开 .vue 文件如何变成可执行代码的神秘面纱。


一、为什么需要模板编译?

🎯 浏览器不认识 <template>

<!-- 你写的 -->
<template>
  <div v-if="user.loggedIn">{{ user.name }}</div>
</template>

<!-- 浏览器看到的 -->
Unknown tag: template → 忽略 or 报错

✅ 解决方案:编译成 render 函数

// 编译后生成的 render 函数
render(h) {
  return this.user.loggedIn 
    ? h('div', { class: 'user' }, `Hello, ${this.user.name}!`)
    : null;
}

💡 render 函数返回的是 虚拟 DOM (VNode),Vue 拿它来高效更新真实 DOM。


二、模板编译三部曲

Template String 
     ↓ parse
   AST (抽象语法树)
     ↓ optimize
   优化后的 AST
     ↓ generate
   Render Function

第一步:🔍 解析(Parse)—— 构建 AST

目标:将 HTML 字符串转为 AST(Abstract Syntax Tree)

示例输入:

<div id="app" class="container">
  <p v-if="show">Hello {{ name }}</p>
</div>

输出 AST 结构:

{
  "type": 1,
  "tag": "div",
  "attrsList": [...],
  "children": [
    {
      "type": 1,
      "tag": "p",
      "if": "show",           // 指令被解析
      "children": [
        {
          "type": 3,
          "text": "Hello ",
          "static": false
        },
        {
          "type": 2,
          "expression": "_s(name)",  // {{ name }} 被编译
          "text": "{{ name }}"
        }
      ]
    }
  ]
}

🛠️ 如何实现?正则 + 状态机

编译器使用多个正则表达式匹配:

匹配内容 正则示例
标签开始 /<([^\s>/]+)/
属性 /(\w+)(?:=)(?:"([^"]*)")/
插值表达式 /{{\s*([\s\S]*?)\s*}}/
指令 /v-(\w+):?(\w*)/?

⚠️ 注意:Vue 的 parser 是一个递归下降解析器,比简单正则复杂得多,但原理类似。


第二步:⚡ 优化(Optimize)—— 标记静态节点

目标:提升运行时性能,跳过不必要的 diff

什么是静态节点?

  • 不包含动态绑定;
  • 内容不会改变;
  • 如:<p>纯文本</p><img src="/logo.png">

优化过程:

  1. 遍历 AST,标记静态根节点和静态子节点;
  2. 添加 static: truestaticRoot: true 标志。
{
  "tag": "p",
  "static": true,
  "staticRoot": true,
  "children": [
    { "type": 3, "text": "这是静态文本", "static": true }
  ]
}

运行时收益:

// patch 过程中
if (vnode.static && oldVnode.static) {
  // 直接复用,跳过 diff!
  vnode.componentInstance = oldVnode.componentInstance;
  return;
}

💥 对于大量静态内容(如文档页面),性能提升可达 30%+


第三步:🎯 生成(Generate)—— 输出 render 函数

目标:将优化后的 AST 转为可执行的 render 函数字符串

输入:优化后的 AST

输出:JavaScript 代码字符串

with(this) {
  return _c('div',
    { attrs: { "id": "app", "class": "container" } },
    [ (show) ?
      _c('p', [_v("Hello "+_s(name))]) :
      _e()
    ]
  )
}

🔤 代码生成规则

AST 节点 生成代码
元素标签 _c(tag, data, children)
文本节点 _v(text)
表达式 {{ }} _s(expression)
条件渲染 v-if (condition) ? renderTrue : renderFalse
静态节点 _m(index)(从 $options.staticRenderFns 中取)

💡 _c = createElement, _v = createTextVNode, _s = toString


三、完整流程图解

          Template
             │
             ▼
       [ HTML Parser ]
             │
             ▼
         AST (未优化)
             │
             ▼
      [ 静态节点检测与标记 ]
             │
             ▼
         AST (已优化)
             │
             ▼
     [ Codegen (生成器) ]
             │
             ▼
     Render Function String
             │
             ▼
     new Function(renderStr)
             │
             ▼
       可执行的 render()
             │
             ▼
        Virtual DOM
             │
             ▼
        Real DOM (渲染)

四、Vue 2 vs Vue 3 编译器对比

特性 Vue 2 Vue 3
编译目标 render 函数 render 函数
模板语法限制 较多(如必须单根) 更灵活(Fragment 支持多根)
静态提升 ✅✅ 更强的 hoist 静态节点
Patch Flag 动态节点标记,diff 更快
编译时优化 基础静态标记 Tree-shaking 友好,死代码消除
源码位置 src/compiler/ @vue/compiler-dom

💥 Vue 3 的编译器更智能,生成的代码更小、更快。


五、手写一个极简模板编译器(玩具版)

function compile(template) {
  // Step 1: Parse (简化版)
  const tags = template.match(/<(\w+)[^>]*>(.*?)<\/\1>/);
  if (!tags) return;

  const tag = tags[1];
  const content = tags[2];

  // Step 2: Optimize (判断是否静态)
  const isStatic = !content.includes('{{');

  // Step 3: Generate
  const renderCode = `
    function render() {
      return ${isStatic 
        ? `_v("${content}")` 
        : `_c("${tag}", {}, [ _v( _s(${content.slice(2,-2)})) ])`
      };
    }
  `;

  return renderCode;
}

// 使用
const code = compile('<p>{{ msg }}</p>');
console.log(code);
// 输出:function render() { return _c("p", {}, [ _v( _s(msg)) ]); }

🎉 这就是一个最简化的“编译器”雏形!


💡 结语

“Vue 模板编译器,是连接声明式模板与命令式 DOM 操作的桥梁。”

阶段 作用 输出
Parse 解析 HTML 字符串 AST
Optimize 标记静态节点 优化后的 AST
Generate 生成 JS 代码 render 函数

掌握编译原理,你就能:

✅ 理解 Vue 模板的底层机制;
✅ 写出更高效的模板(减少动态绑定);
✅ 调试编译错误更得心应手;
✅ 为学习其他框架(React JSX)打下基础。

【vue篇】Vue Mixin:可复用功能的“乐高积木”

作者 LuckySusu
2025年10月15日 18:18

在开发多个 Vue 组件时,你是否遇到过这样的问题:

“这几个组件都有相同的 loading 逻辑,要复制粘贴?” “如何共享通用的错误处理方法?” “有没有像‘插件’一样的功能可以注入?”

答案就是:Mixin(混入)

本文将全面解析 Vue Mixin 的核心概念使用场景潜在风险


一、什么是 Mixin?

Mixin 是一个包含 Vue 组件选项的对象,可以被“混入”到多个组件中,实现功能复用。

🎯 核心价值

  • 代码复用:避免重复编写相同逻辑;
  • 逻辑分离:将通用功能(如 loading、权限)抽离;
  • 渐进增强:为组件动态添加功能。

二、快速上手:一个 Loading Mixin 示例

场景:多个组件需要“加载中”状态

Step 1:创建 loading.mixin.js

// mixins/loading.mixin.js
export const loadingMixin = {
  data() {
    return {
      loading: false,
      errorMessage: null
    };
  },

  methods: {
    async withLoading(asyncFn) {
      this.loading = true;
      this.errorMessage = null;
      try {
        await asyncFn();
      } catch (err) {
        this.errorMessage = err.message;
      } finally {
        this.loading = false;
      }
    }
  },

  // 生命周期钩子
  created() {
    console.log('【Mixin】组件创建,初始化 loading 状态');
  }
};

Step 2:在组件中使用

<!-- UserProfile.vue -->
<script>
import { loadingMixin } from '@/mixins/loading.mixin';

export default {
  mixins: [loadingMixin],

  async created() {
    // 使用 mixin 提供的方法
    await this.withLoading(() => this.fetchUser());
  },

  methods: {
    async fetchUser() {
      // 模拟 API 调用
      await new Promise(r => setTimeout(r, 1000));
      this.user = { name: 'Alice' };
    }
  }
};
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="errorMessage">错误:{{ errorMessage }}</div>
  <div v-else>用户:{{ user.name }}</div>
</template>

✅ 效果:UserProfile 组件自动拥有了 loadingerrorMessagewithLoading 方法。


三、Mixin 合并规则:当名字冲突了怎么办?

当 Mixin 和组件定义了同名选项,Vue 会按规则合并:

选项类型 合并策略
data 函数返回对象合并(浅合并)
methods / computed / props 组件优先,Mixin 的会被覆盖
生命周期钩子 两者都执行,Mixin 的先执行
watch 同名 watcher 都会执行
computed 组件优先

🎯 生命周期执行顺序

const myMixin = {
  created() {
    console.log('1. Mixin created');
  }
};

export default {
  mixins: [myMixin],
  created() {
    console.log('2. Component created'); // 后执行
  }
}

输出:

1. Mixin created
2. Component created

💥 Mixin 的生命周期永远先于组件自身执行


四、实战应用场景

✅ 场景 1:表单验证逻辑复用

// mixins/validation.mixin.js
export const validationMixin = {
  data() {
    return {
      errors: {}
    };
  },
  methods: {
    validateEmail(email) {
      const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!re.test(email)) {
        this.errors.email = '邮箱格式不正确';
      } else {
        delete this.errors.email;
      }
    }
  }
};

✅ 场景 2:权限控制

// mixins/permission.mixin.js
export const permissionMixin = {
  mounted() {
    if (!this.$store.getters.hasPermission(this.requiredPermission)) {
      this.$router.push('/403');
    }
  }
};

// 组件中
export default {
  mixins: [permissionMixin],
  data() {
    return {
      requiredPermission: 'user:edit'
    };
  }
};

✅ 场景 3:第三方 SDK 集成

// mixins/analytics.mixin.js
export const analyticsMixin = {
  mounted() {
    this.$analytics.pageView(); // 记录页面访问
  },
  methods: {
    trackEvent(event, props) {
      this.$analytics.track(event, props);
    }
  }
};

五、Mixin 的“黑暗面”:潜在问题

❌ 问题 1:命名冲突(Name Collision)

// mixin 定义了 fetchData
const apiMixin = {
  methods: {
    fetchData() { /* ... */ }
  }
};

// 组件也定义了 fetchData
export default {
  mixins: [apiMixin],
  methods: {
    fetchData() { /* 覆盖了 mixin 的方法!*/ }
  }
}

⚠️ 组件的方法会覆盖 Mixin 的,可能导致逻辑丢失。


❌ 问题 2:隐式依赖(Implicit Dependency)

// mixin 依赖组件必须提供 `userId`
const userMixin = {
  async created() {
    this.userData = await fetch(`/api/users/${this.userId}`);
  }
};

如果组件没有定义 userId,就会报错,但没有明显提示


❌ 问题 3:来源不清晰(Source Ambiguity)

<template>
  <!-- 这个 `loading` 是哪来的? -->
  <div v-if="loading">加载中...</div>
</template>

🔍 开发者无法从模板直接看出 loading 是来自 Mixin 还是组件自身。


六、Vue 3 的替代方案:Composition API

// composables/useLoading.js
import { ref } from 'vue';

export function useLoading() {
  const loading = ref(false);
  const errorMessage = ref(null);

  const withLoading = async (asyncFn) => {
    loading.value = true;
    errorMessage.value = null;
    try {
      await asyncFn();
    } catch (err) {
      errorMessage.value = err.message;
    } finally {
      loading.value = false;
    }
  };

  return { loading, errorMessage, withLoading };
}
<!-- UserProfile.vue -->
<script setup>
import { useLoading } from '@/composables/useLoading';

const { loading, withLoading } = useLoading();

async function loadUser() {
  await withLoading(fetchUser);
}
</script>

✅ Composition API 的优势:

特性 Mixin Composition API
命名冲突 ❌ 易发生 ✅ 通过解构重命名
源头追踪 ❌ 困难 useXxx() 清晰可见
类型推导 ❌ 弱 ✅ TypeScript 友好
逻辑复用 ✅ 更灵活

💡 结语

“Mixin 是一把双刃剑:用得好,提升效率;用不好,制造混乱。”

方案 适用场景
Mixin Vue 2 项目、简单逻辑复用
Composition API Vue 3 项目、复杂逻辑、TypeScript

🚀 最佳实践建议:

  1. 优先使用 Composition API(Vue 3);
  2. ✅ 如果用 Mixin,命名清晰(如 useLoadingMixin);
  3. ✅ 避免在 Mixin 中引入隐式依赖
  4. ✅ 文档化 Mixin 的输入/输出

掌握 Mixin,你就能写出更 DRY(Don't Repeat Yourself)的代码。

【vue篇】Vue 2 响应式“盲区”破解:如何监听对象/数组属性变化

作者 LuckySusu
2025年10月15日 18:18

在 Vue 开发中,你是否遇到过这样的诡异问题:

“我明明改了 this.user.name,为什么页面没更新?” “this.arr[0] = 'new',视图怎么不动?” “Vue 不是响应式的吗?”

本文将彻底解析 Vue 2 的响应式限制,并提供五种解决方案,让你彻底告别“数据变了,视图没变”的坑。


一、核心问题:Vue 2 的响应式“盲区”

🎯 为什么直接赋值不触发更新?

// ❌ 无效:视图不更新
this.user.name = 'John';      // 对象新增属性
this.users[0] = 'Alice';      // 数组索引赋值

🔍 根本原因:Object.defineProperty 的限制

Vue 2 使用 Object.defineProperty 拦截:

  • ✅ 能监听 已有属性的修改
  • 不能监听
    • 对象新增属性
    • 数组索引直接赋值arr[0] = x);
    • 数组长度修改arr.length = 0)。

💥 Vue 无法“感知”这些操作,所以不会触发视图更新。


二、解决方案:五种正确姿势

✅ 方案 1:this.$set() —— Vue 官方推荐

// ✅ 对象新增属性
this.$set(this.user, 'name', 'John');

// ✅ 数组索引赋值
this.$set(this.users, 0, 'Alice');

// ✅ 等价于
Vue.set(this.user, 'name', 'John');

🎯 this.$set 的内部原理

function $set(target, key, val) {
  // 1. 如果是数组 → 用 splice 触发响应式
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1, val);
    return val;
  }
  
  // 2. 如果是对象
  const ob = target.__ob__;
  if (key in target) {
    // 已有属性 → 直接赋值(已有 getter/setter)
    target[key] = val;
  } else {
    // 新增属性 → 动态添加响应式
    defineReactive(target, key, val);
    ob.dep.notify(); // 手动派发更新
  }
  return val;
}

💡 $set = 智能判断 + 自动响应式处理


✅ 方案 2:数组专用方法 —— splice

// ✅ 修改数组某一项
this.users.splice(0, 1, 'Alice'); // 索引0,删除1个,插入'Alice'

// ✅ 新增元素
this.users.splice(1, 0, 'Bob'); // 在索引1前插入

// ✅ 删除元素
this.users.splice(0, 1); // 删除第一项

🎯 为什么 splice 可以?

Vue 2 重写了数组的 7 个方法

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

// 重写后,调用这些方法时会:
// 1. 执行原生方法
// 2. dep.notify() → 触发视图更新

✅ 这些方法是“响应式安全”的。


✅ 方案 3:对象整体替换

// ✅ 对象新增属性
this.user = { ...this.user, name: 'John' };

// ✅ 或
this.user = Object.assign({}, this.user, { name: 'John' });
  • ✅ 原理:重新赋值 → 触发 setter → 视图更新;
  • ❌ 缺点:失去响应式连接(如果 user 被深层嵌套)。

✅ 方案 4:初始化时声明属性

data() {
  return {
    user: {
      name: '',    // 提前声明
      age: null,
      email: ''    // 避免运行时新增
    }
  };
}

💡 最佳实践:data 中定义所有可能用到的属性


✅ 方案 5:使用 Vue.observable + computed

const state = Vue.observable({
  user: { name: 'Tom' }
});

// 在组件中
computed: {
  userName() {
    return state.user.name; // 自动依赖收集
  }
}
  • ✅ 适合全局状态;
  • ❌ 不推荐用于组件局部状态。

三、Vue 3 的革命性改进:Proxy 无所不能

import { reactive } from 'vue';

const state = reactive({
  user: {},
  users: []
});

// ✅ Vue 3 中,以下操作全部响应式!
state.user.name = 'John';        // 新增属性
state.users[0] = 'Alice';        // 索引赋值
state.users.length = 0;          // 修改长度
delete state.user.name;          // 删除属性

💥 Vue 3 使用 Proxy,能拦截 getsetdeleteProperty 等所有操作,彻底解决 Vue 2 的响应式盲区。


四、最佳实践清单

场景 推荐方案
Vue 2:对象新增属性 this.$set(obj, key, val)
Vue 2:数组索引赋值 this.$set(arr, index, val)arr.splice(index, 1, val)
Vue 2:批量更新数组 splice / push / pop
Vue 2:避免问题 初始化时声明所有属性
Vue 3:任何操作 直接赋值,Proxy 全部拦截

五、常见误区

❌ 误区 1:this.$set 只用于对象

// ❌ 错误:认为数组不需要 $set
this.users[0] = 'new'; // 不响应

// ✅ 正确
this.$set(this.users, 0, 'new');

❌ 误区 2:pushsplice 更好

// ✅ `splice` 更通用
this.users.splice(1, 0, 'Bob'); // 在中间插入
this.users.push('Bob');         // 只能在末尾

✅ 推荐:splice 是数组操作的“瑞士军刀”


💡 结语

“在 Vue 2 中,永远不要直接操作数组索引或对象新增属性。”

方法 是否响应式 适用场景
obj.key = val (已有) 修改已有属性
obj.newKey = val $set
arr[i] = val $setsplice
this.$set() 通用解决方案
splice() 数组操作首选

掌握这些技巧,你就能:

✅ 避免响应式失效的 bug;
✅ 写出更可靠的 Vue 代码;
✅ 理解 Vue 响应式的核心原理。

【vue篇】Vue.delete vs delete:数组删除的“陷阱”与正确姿势

作者 LuckySusu
2025年10月15日 18:17

在 Vue 开发中,你是否遇到过这样的问题:

“用 delete 删除数组项,视图为什么没更新?” “Vue.delete 和原生 delete 有什么区别?” “如何安全地删除数组元素?”

本文将彻底解析 deleteVue.delete删除数组时的根本差异。


一、核心结论:一个“打洞”,一个“重排”

操作 结果 响应式 视图更新
delete arr[index] 元素变 empty长度不变 ❌ 不响应 ❌ 不更新
Vue.delete(arr, index) 直接删除,长度改变 ✅ 响应式 ✅ 自动更新

💥 delete 只是“打了个洞”,而 Vue.delete 是真正的“移除”。


二、实战演示:同一个操作,两种结果

场景:删除数组第二项

const vm = new Vue({
  data: {
    users: ['Alice', 'Bob', 'Charlie']
  }
});

方式一:delete(错误方式)

delete vm.users[1];
console.log(vm.users); 
// ['Alice', empty, 'Charlie'] → 长度仍为 3!
  • 内存中users[1] 变为 empty slot
  • DOM 中:视图不会更新

⚠️ 控制台警告:

[Vue warn]: A value is trying to be set on a non-existent property...

方式二:Vue.delete(正确方式)

Vue.delete(vm.users, 1);
// 或 this.$delete(vm.users, 1)
console.log(vm.users); 
// ['Alice', 'Charlie'] → 长度变为 2!

✅ 视图自动更新,完美!


三、深入原理:为什么 delete 不行?

🔍 1. delete 的本质

let arr = ['a', 'b', 'c'];
delete arr[1];

// 等价于
arr[1] = undefined; // ❌ 错误理解
// 实际是:
Object.defineProperty(arr, 1, { configurable: true });
delete arr[1]; // 移除属性,但保留索引“空位”
索引:   0     1     2
值:   'a'   empty  'c'
  • 数组长度 不变
  • for...in 会跳过 empty 项;
  • Array.prototype 方法(如 map, filter)会跳过 empty

🔍 2. Vue 响应式的限制

Vue 2 使用 Object.defineProperty 拦截:

  • ✅ 能监听 arr[1] = newValue(赋值);
  • 不能监听 delete arr[1](删除属性)

💡 Vue 无法检测到“属性被删除”,所以不会触发视图更新。


🔍 3. Vue.delete 的内部实现

Vue.delete = function (target, key) {
  // 1. 执行原生 delete
  delete target[key];
  
  // 2. 手动触发依赖更新
  if (target.__ob__) {
    target.__ob__.dep.notify(); // 强制通知 watcher
  }
}

Vue.delete = delete + 手动派发更新


四、其他删除数组的方法(推荐)

✅ 1. splice() —— 最常用

vm.users.splice(1, 1); // 从索引1开始,删除1个
// ['Alice', 'Charlie']
  • ✅ 响应式(Vue 重写了 splice);
  • ✅ 支持删除多个元素;
  • ✅ 返回被删除的元素。

✅ 2. filter() —— 函数式编程

vm.users = vm.users.filter((user, index) => index !== 1);
// 或根据条件删除
vm.users = vm.users.filter(user => user !== 'Bob');
  • ✅ 不修改原数组,返回新数组;
  • ✅ 适合复杂条件删除;
  • ✅ 响应式(因为重新赋值)。

✅ 3. slice() + 解构

vm.users = [
  ...vm.users.slice(0, 1),
  ...vm.users.slice(2)
]; // 删除索引1
  • ✅ 函数式,不可变数据;
  • ✅ 适合组合多个片段。

五、Vue 3 的改进:Proxy 无所不能

import { reactive } from 'vue';

const state = reactive({
  users: ['Alice', 'Bob', 'Charlie']
});

// Vue 3 中,delete 也能触发更新!
delete state.users[1]; // ✅ 视图自动更新

💥 Vue 3 使用 Proxy,能拦截 deleteProperty,因此原生 delete 也响应式!


六、最佳实践清单

场景 推荐方法
删除指定索引 splice(index, 1)
删除满足条件的元素 filter(condition)
需要兼容 Vue 2 Vue.delete(array, index)
Vue 3 项目 delete array[index]
性能敏感场景 splice(原地修改)

💡 结语

“在 Vue 2 中,永远不要用 delete 删除数组!”

方法 是否响应式 是否推荐
delete arr[i] ❌ 绝对避免
Vue.delete(arr, i) ✅ Vue 2 推荐
arr.splice(i, 1) ✅ 首选
arr.filter(...) ✅ 函数式首选

掌握这些删除技巧,你就能:

✅ 避免视图不更新的 bug;
✅ 写出更健壮的 Vue 代码;
✅ 顺利过渡到 Vue 3 的响应式系统。

【vue篇】Vue 项目中的静态资源管理:assets vs static 终极指南

作者 LuckySusu
2025年10月15日 18:17

在 Vue 项目中,你是否遇到过这样的困惑:

assetsstatic 文件夹有什么区别?” “图片到底该放哪个文件夹?” “为什么有的资源路径变了,有的没变?”

本文将彻底解析 assetsstatic核心差异使用场景最佳实践


一、核心结论:一句话总结

assets 走构建流程(可处理),static 直接拷贝(不处理)。

维度 assets static
是否参与构建 ✅ 是 ❌ 否
是否被 webpack 处理 ✅ 是 ❌ 否
是否支持模块化导入 ✅ 是 ❌ 否
是否会被重命名(hash) ✅ 是 ❌ 否
是否支持 Tree-shaking ✅ 是 ❌ 否

二、详细对比:从构建流程说起

🔄 1. assets:构建流程的“参与者”

src/assets/logo.png
     ↓
  webpack 处理
     ↓
  压缩、转 base64、生成 hash 名
     ↓
dist/static/img/logo.2f1f87g.png

assets 的特点:

  • 参与构建:被 webpack 处理;
  • 优化处理
    • 图片压缩(image-webpack-loader);
    • 小图转 base64(减少 HTTP 请求);
    • 文件名加 hash(缓存优化);
  • 支持模块化导入
import logo from '@/assets/logo.png';
console.log(logo); // /static/img/logo.abc123.png
  • 路径动态化:路径由构建工具生成,不可预测

🔄 2. static:构建流程的“旁观者”

static/favicon.ico
     ↓
  直接拷贝
     ↓
dist/favicon.ico

static 的特点:

  • 不参与构建:原封不动拷贝到 dist
  • 无优化:不压缩、不转码、不加 hash;
  • 路径固定:访问路径 = / + 文件名
  • 适合“即插即用”资源
<!-- 直接通过绝对路径访问 -->
<link rel="icon" href="/favicon.ico">
<script src="/js/third-party.js"></script>

三、实战演示:同一个图片的不同命运

场景:项目中使用 logo.png

方式一:放在 assets

<template>
  <img :src="logo" alt="Logo">
</template>

<script>
import logo from '@/assets/logo.png';
// logo = "/static/img/logo.abc123.png"
</script>

优势

  • 图片被压缩,体积更小;
  • 文件名加 hash,缓存友好;
  • 支持按需加载。

方式二:放在 static

<template>
  <img src="/static/logo.png" alt="Logo">
</template>

优势

  • 构建速度快(跳过处理);
  • 路径固定,适合第三方脚本引用。

劣势

  • 图片未压缩,体积大;
  • 无 hash,缓存更新困难。

四、何时使用 assets?何时使用 static

✅ 推荐使用 assets 的场景:

资源类型 示例
项目自用图片 logo、banner、icon
CSS/SCSS 文件 @import '@/assets/styles/main.scss'
字体文件 .woff, .ttf(可被 hash)
SVG 图标 可被 svg-sprite-loader 处理
需要按需引入的 JS 工具函数、配置文件

💡 原则:项目源码中直接引用的资源 → 放 assets


✅ 推荐使用 static 的场景:

资源类型 示例
第三方库 static/js/jquery.min.js
Favicon favicon.ico
Robots.txt SEO 爬虫规则
大型静态文件 PDF、视频(避免 webpack 处理)
CND 回退文件 当 CDN 失败时本地加载
<!-- 第三方库回退 -->
<script src="https://cdn.example.com/vue.js"></script>
<script>window.Vue || document.write('<script src="/static/js/vue.min.js"><\/script>')</script>

💡 原则:不希望被构建工具处理的资源 → 放 static


五、Vue CLI 项目结构示例

my-project/
├── public/               # Vue CLI 中 static 的新名字
│   ├── favicon.ico
│   ├── robots.txt
│   └── static/
│       └── js/
│           └── analytics.js
├── src/
│   ├── assets/           # 所有需要构建的资源
│   │   ├── images/
│   │   ├── fonts/
│   │   └── styles/
│   └── components/
└── package.json

⚠️ 注意:在 Vue CLI 3+ 中,static 文件夹已更名为 public


六、常见误区与最佳实践

❌ 误区 1:所有图片都放 static

<!-- 错误:大图未压缩,无 hash -->
<img src="/static/banner.jpg">

✅ 正确做法:

import banner from '@/assets/banner.jpg';
<img :src="banner">

❌ 误区 2:在 assets 中放第三方库

// ❌ 错误:第三方库应放 public
import 'jquery'; // 来自 node_modules 或 assets

✅ 正确做法:

<!-- 放 public,通过 script 标签引入 -->
<script src="/static/js/jquery.min.js"></script>

✅ 最佳实践清单

实践 说明
✅ 小图放 assets 转 base64,减少请求
✅ 大图放 assets 压缩,但不转 base64
✅ 第三方库放 public 避免重复打包
✅ 使用 require 动态加载 :src="require('@/assets/dynamic.png')"
✅ 配置 publicPath 部署到子目录时设置

💡 结语

assets 是你的‘智能资源库’,staticpublic)是你的‘原始文件仓库’。”

选择 使用场景
assets 项目源码引用、需要优化、支持 hash
static / public 第三方资源、固定路径、避免构建

掌握这一原则,你就能:

✅ 优化项目性能;
✅ 减少打包体积;
✅ 提升缓存效率;
✅ 避免资源加载错误。

昨天 — 2025年10月14日首页

【vue篇】Vue.js 2025:为何全球开发者都在拥抱这个前端框架?

作者 LuckySusu
2025年10月14日 23:03

在 React、Angular、Svelte 等众多前端框架中,Vue.js 凭借其独特的设计理念,持续赢得开发者青睐。

“Vue 到底强在哪?” “为什么中小企业首选 Vue?” “它的性能真的比 React 快吗?”

本文将从 轻量易学响应式生态,全面解析 Vue 的六大核心优势。


一、🔥 优势 1:极致轻量,启动飞快

Vue 3 (gzip): ~22KB
React 18 (gzip): ~40KB + react-dom

✅ 轻量带来的好处:

优势 说明
快速加载 移动端、低网速环境体验更佳
首屏更快 TTI(可交互时间)提前
Bundle 更小 减少用户流量消耗
// CDN 引入,5 秒上手
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

💡 Vue 是“渐进式框架”,你可以从 <script> 开始,逐步升级到 Vue CLI / Vite。


二、📚 优势 2:简单易学,中文友好

🌍 国人开发,文档贴心

  • 中文文档:官方文档翻译精准,无语言障碍;
  • 渐进式学习:从模板 → Options API → Composition API,平滑过渡;
  • 开发者友好:错误提示清晰,调试工具强大。

🎯 学习曲线对比

阶段 Vue React
第一天 能写 v-model 需理解 JSX、state
第一周 掌握组件通信 理解 Hooks、不可变性
第一个月 上线项目 仍在优化性能

Vue 是前端新手的“最佳第一课”


三、🔁 优势 3:双向数据绑定,开发更高效

<template>
  <!-- v-model:自动同步 -->
  <input v-model="message" />
  <p>{{ message }}</p>
</template>

<script>
export default {
  data() {
    return { message: 'Hello' }
  }
}
</script>

🆚 对比 React

// React:手动同步
function Input() {
  const [message, setMessage] = useState('');
  return (
    <input 
      value={message} 
      onChange={e => setMessage(e.target.value)} 
    />
  );
}

💥 Vue 的 v-model 让表单操作减少 50% 代码量


四、🧩 优势 4:组件化,复用无处不在

<!-- Button.vue -->
<template>
  <button :class="`btn-${type}`" @click="$emit('click')">
    <slot></slot>
  </button>
</template>
<!-- 使用 -->
<Btn type="primary" @click="save">保存</Btn>
<Btn type="danger">删除</Btn>

✅ 组件化优势:

优势 说明
UI 一致性 全站按钮风格统一
开发效率 修改一处,全局生效
团队协作 设计师 + 前端可共建组件库

📌 Vue 的单文件组件(.vue)将 模板、逻辑、样式 封装在一起,清晰易维护。


五、🧱 优势 5:关注点分离,结构清晰

视图 (template)    ←→    数据 (data)
       ↑                    ↑
   用户操作           状态管理 (Vuex/Pinia)

✅ 三大分离:

  1. 视图与数据分离

    • 修改 data,视图自动更新;
    • 无需手动操作 DOM。
  2. 结构与样式分离

    • <style scoped> 避免样式污染;
    • 支持 CSS Modules、PostCSS。
  3. 逻辑与模板分离

    • setup() / methods 集中处理业务逻辑;
    • 模板只负责展示。

💡 这种分离让维护成本大幅降低


六、⚡ 优势 6:虚拟 DOM + 响应式 = 性能王者

🎯 Vue 的性能优势在哪?

机制 说明
自动依赖追踪 渲染时自动收集依赖,只更新相关组件
细粒度更新 不像 React 默认全量 diff
编译优化 Vue 3 的 PatchFlag 标记动态节点,跳过静态节点
Tree-shaking 按需引入,减少打包体积

📊 性能对比(同场景)

操作 Vue 3 React 18
列表更新(1000项) ✅ 60fps ⚠️ 需 React.memo 优化
首次渲染 ✅ 更快 ❌ Bundle 更大
内存占用 ✅ 更低 ⚠️ 较高

💥 Vue 的响应式系统是“智能的”,它知道谁依赖谁,无需手动优化。


七、🚀 2025 Vue 生态全景

工具 说明
Vite 下一代构建工具,秒级启动
Pinia Vue 3 官方状态管理,TypeScript 友好
Vue Router 官方路由,支持懒加载
Nuxt.js SSR / SSG 框架,SEO 友好
UnoCSS 原子化 CSS,极速样式开发
# 5 秒创建项目
npm create vue@latest

💡 结语

“Vue 不是最快的框架,但可能是最平衡的。”

优势 说明
轻量 22KB,CDN 可用
易学 中文文档,渐进式学习
高效 v-model、组件化减少代码量
清晰 关注点分离,维护简单
性能 响应式 + 虚拟 DOM,自动优化
生态 Vite + Pinia + Nuxt,现代开发闭环

【vue篇】React vs Vue:2025 前端双雄终极对比

作者 LuckySusu
2025年10月14日 23:03

在选择前端框架时,你是否在 React 和 Vue 之间犹豫不决?

“React 和 Vue 到底有什么区别?” “哪个更适合我的团队?” “它们的未来趋势如何?”

本文将从 数据流模板响应式生态,全面解析 React 与 Vue 的异同。


一、相似之处:现代前端的共同基石

特性 React Vue
核心库 聚焦 UI 渲染 聚焦 UI 渲染
虚拟 DOM ✅ 支持 ✅ 支持(Vue 2+)
组件化 ✅ 鼓励 ✅ 鼓励
构建工具 Create React App Vue CLI / Vite
状态管理 Redux / MobX / Zustand Vuex / Pinia
路由 React Router Vue Router

✅ 两者都遵循 现代前端最佳实践:组件化、虚拟 DOM、单向数据流。


二、核心差异:哲学与设计

🎯 1. 数据流:双向 vs 单向

框架 数据流 示例
Vue 默认支持双向绑定v-model <input v-model="msg" />
React 严格单向数据流 <input value={msg} onChange={e => setMsg(e.target.value)} />

💡 Vue 更“贴心”,React 更“可控”。


🎯 2. 模板 vs JSX:声明式 UI 的两种范式

Vue:HTML 扩展式模板

<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <ChildComponent 
      :msg="message" 
      @update="handleUpdate" 
    />
  </div>
</template>
  • ✅ 语法接近 HTML,设计师友好;
  • ✅ 指令系统(v-if, v-for)简洁;
  • ❌ 逻辑能力有限,复杂逻辑需写在 script

React:JSX(JavaScript XML)

function App() {
  const [msg, setMsg] = useState('');
  
  return (
    <div className="container">
      <h1>{title}</h1>
      <ChildComponent 
        msg={msg} 
        onUpdate={handleUpdate} 
      />
    </div>
  );
}
  • ✅ 逻辑与 UI 在同一文件,更灵活;
  • ✅ 可用完整 JavaScript 表达式;
  • ❌ 学习成本略高(JSX 语法)。

🎯 3. 响应式系统:谁更高效?

框架 实现原理 性能特点
Vue getter/setter 拦截(Vue 2)
Proxy(Vue 3)
自动依赖追踪
无需手动优化,更新粒度更细
React 手动触发更新setState
默认全量 diff
❌ 可能导致不必要的渲染
✅ 可通过 useMemo/useCallback/React.memo 优化

💥 Vue 的响应式是“自动挡”,React 是“手动挡”。


🎯 4. 组件通信与复用

React:高阶组件(HOC)与 Hooks

// HOC
const withLogger = (Component) => {
  return (props) => {
    console.log('Render:', props);
    return <Component {...props} />;
  };
};

// Hooks
function useCounter() {
  const [count, setCount] = useState(0);
  return { count, increment: () => setCount(c => c + 1) };
}
  • ✅ 函数式,组合能力强;
  • ✅ Hooks 解决了 mixin 的问题。

Vue:Mixins 与 Composition API

// Mixin(Vue 2)
const logMixin = {
  created() {
    console.log('Component created');
  }
};

// Composition API(Vue 3)
function useCounter() {
  const count = ref(0);
  const increment = () => count.value++;
  return { count, increment };
}

💡 Vue 3 的 Composition API 已向 React Hooks 靠拢。


🎯 5. 监听数据变化的实现

框架 实现方式 特点
Vue Object.defineProperty / Proxy 精确追踪
知道哪个属性变了
React 引用比较(shallowEqual) ❌ 不比较值,只比较引用
✅ 鼓励不可变数据(Immutability)

📌 Vue:可变数据 + 精确更新
📌 React:不可变数据 + 手动优化


🎯 6. 跨平台能力

平台 React Vue
Web
移动端 React Native(成熟) Weex(已停止维护)
UniApp(第三方)
桌面端 Electron + React Electron + Vue
小程序 Taro / Remax UniApp / Taro

✅ React 在跨平台(尤其是移动端)生态更强大。


🎯 7. 学习曲线

框架 学习难度 适合人群
Vue ⭐⭐☆ 初学者、HTML 开发者
React ⭐⭐⭐ 有 JavaScript 基础的开发者
  • Vue:渐进式,从模板开始;
  • React:需理解 JSX、状态、不可变性。

三、实战对比:同一个功能

需求:计数器组件

Vue 3(Composition API)

<script setup>
import { ref } from 'vue';

const count = ref(0);
const increment = () => count.value++;
</script>

<template>
  <button @click="increment">Count: {{ count }}</button>
</template>

React 18(Hooks)

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(c => c + 1);
  
  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}

💡 代码结构高度相似!Vue 3 的 Composition API 明显受 React Hooks 启发。


四、如何选择?

你的需求 推荐框架
快速上手,团队有 HTML 经验 Vue
复杂应用,需要强大状态管理 React
移动端开发(React Native) React
小程序(UniApp) Vue
喜欢函数式编程 React
喜欢模板语法 Vue

💡 结语

“React 和 Vue 不是敌人,而是共同推动前端进步的力量。”

维度 React Vue
哲学 “Just JavaScript” “渐进式框架”
模板 JSX(JavaScript) 模板(HTML 扩展)
响应式 手动触发 自动追踪
复用 Hooks / HOC Composition API / Mixins
生态 更大(尤其移动端) 更聚焦 Web
学习曲线 较陡 较平缓

🚀 2025 趋势

  • Vue 3 + Composition API:向 React Hooks 学习,提升逻辑复用;
  • React Server Components:服务端渲染新范式;
  • Vite:取代 Webpack,成为新一代构建工具(Vue 和 React 都支持)。

选择建议

  • 团队新手多?→ Vue
  • 需要跨平台?→ React
  • 追求最新技术?→ 两者都支持 Reactivity、SSR、Micro Frontends。

【vue篇】Vue 响应式核心:依赖收集机制深度解密

作者 LuckySusu
2025年10月14日 23:02

在 Vue 应用中,你是否好奇:

“当我修改 this.message 时,DOM 为何能自动更新?” “为什么只有被模板用到的数据才会触发更新?” “Vue 是如何知道哪个组件依赖哪个数据的?”

这一切的背后,是 Vue 依赖收集(Dependency Collection) 的精妙设计。

本文将从 Object.definePropertyDep-Watcher 模型,彻底解析 Vue 2 的响应式原理。


一、核心结论:依赖收集 = 数据 ↔ 视图 的双向绑定

数据变化 → 通知视图更新
     ↑          ↓
   收集      触发 getter
  • 谁收集? Dep(依赖中心)
  • 被谁收集? Watcher(观察者)
  • 何时收集? 组件渲染时读取数据(触发 getter)

二、三大核心角色

🎯 1. defineReactive:让数据“响应式”

function defineReactive(obj, key, val) {
  // 每个属性都有一个独立的依赖中心
  const dep = new Dep();
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // ✅ 依赖收集:谁在读我?
      if (Dep.target) {
        dep.depend(); // 通知 dep:当前 watcher 依赖我
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      // ✅ 派发更新:通知所有依赖者
      dep.notify();
    }
  });
}

💥 dep 是每个属性的“私人秘书”,记录谁依赖它。


🎯 2. Dep:依赖管理中心

class Dep {
  static target = null; // 🌟 全局唯一,指向当前正在计算的 Watcher
  subs = []; // 存储所有依赖此数据的 Watcher

  // 被收集:当前 Watcher 依赖我
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this); // 告诉 Watcher:你依赖我
    }
  }

  // 添加订阅者
  addSub(watcher) {
    this.subs.push(watcher);
  }

  // 派发更新:数据变了!
  notify() {
    // 避免在 notify 时修改数组
    const subs = this.subs.slice();
    for (let i = 0; i < subs.length; i++) {
      subs[i].update(); // 通知每个 Watcher 更新
    }
  }
}

🔑 Dep.target 是关键:它确保同一时间只有一个 Watcher 在收集依赖。


🎯 3. Watcher:观察者(组件/计算属性)

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.getter = expOrFn; // 如 vm._update(vm._render())
    this.cb = cb;
    this.deps = [];      // 记录依赖了哪些 dep
    this.depIds = new Set(); // 去重
    this.value = this.get(); // 🚀 首次执行,触发依赖收集
  }

  // 读取数据,触发 getter
  get() {
    pushTarget(this); // 设置当前 Watcher
    const value = this.getter.call(this.vm, this.vm);
    popTarget(); // 清除
    return value;
  }

  // 被 dep 收集
  addDep(dep) {
    const id = dep.id;
    if (!this.depIds.has(id)) {
      this.depIds.add(id);
      this.deps.push(dep);
      dep.addSub(this); // dep 记录我
    }
  }

  // 更新:数据变化后调用
  update() {
    queueWatcher(this); // 异步更新
  }

  run() {
    const value = this.get(); // 重新计算
    this.cb(value, this.value); // 执行回调(如更新 DOM)
    this.value = value;
  }
}

💡 Watcher 是“消费者”,它知道自己依赖哪些数据。


三、依赖收集全过程(图文解析)

🔄 阶段 1:初始化响应式数据

// data: { message: 'Hello' }
defineReactive(data, 'message', 'Hello');
// → 为 message 创建 dep 实例
data.message
     ↓
   dep (subs: [])

🔄 阶段 2:组件挂载,创建 Watcher

new Watcher(vm, () => {
  vm._update(vm._render());
});
  • Watcher.get() 被调用;
  • pushTarget(this)Dep.target = watcher
Dep.target → Watcher实例

🔄 阶段 3:渲染触发 getter,完成收集

vm._render(); // 生成 VNode
// 模板中:{{ message }}
// → 读取 this.message → 触发 getter
// getter 执行
get() {
  if (Dep.target) {
    dep.depend(); // dep.depend()
  }
  return val;
}
// dep.depend()
depend() {
  Dep.target.addDep(this); // watcher.addDep(dep)
}
// watcher.addDep(dep)
addDep(dep) {
  this.deps.push(dep);
  dep.addSub(this); // dep.subs.push(watcher)
}

收集完成!

data.message
     ↓
   dep (subs: [watcher])
     ↑
Watcher (deps: [dep])

🔄 阶段 4:数据变化,派发更新

this.message = 'World'; // 触发 setter
// setter 执行
set(newVal) {
  val = newVal;
  dep.notify(); // 通知所有 subs
}
// dep.notify()
notify() {
  this.subs.forEach(watcher => watcher.update());
}

queueWatcher(watcher) → 异步更新 DOM。


四、实战演示:一个简单的响应式系统

// 1. 数据
const data = { count: 0 };

// 2. 响应式化
defineReactive(data, 'count', 0);

// 3. 创建 Watcher(模拟组件)
new Watcher(null, () => {
  console.log('Render:', data.count);
}, null);
// → 触发 getter → 依赖收集完成

// 4. 修改数据
data.count = 1;
// → 触发 setter → dep.notify() → Watcher.update()
// → 输出:Render: 1

五、Vue 3 的改进:Proxy + effect

// Vue 3 使用 Proxy
const reactiveData = reactive({ count: 0 });

effect(() => {
  console.log(reactiveData.count); // 收集依赖
});

reactiveData.count++; // 触发更新
  • 优势
    • 支持动态新增属性;
    • 性能更好(无需递归 defineProperty);
    • 代码更简洁。

💡 结语

“依赖收集是 Vue 响应式的灵魂。”

角色 职责
defineReactive 拦截 getter/setter
Dep 管理订阅者(Watcher)
Watcher 观察数据变化,执行更新
过程 关键操作
初始化 defineReactive
收集 Dep.target = watcher + dep.depend()
更新 dep.notify()watcher.update()

掌握依赖收集机制,你就能:

✅ 理解 Vue 响应式原理;
✅ 调试响应式问题;
✅ 设计自己的响应式系统;
✅ 顺利过渡到 Vue 3 的 reactiveeffect

【vue篇】Vue 单向数据流铁律:子组件为何不能直接修改父组件数据?

作者 LuckySusu
2025年10月14日 23:02

在 Vue 开发中,你是否写过这样的代码:

<!-- ChildComponent.vue -->
<template>
  <button @click="changeParentData">修改父数据</button>
</template>

<script>
export default {
  methods: {
    changeParentData() {
      // ❌ 危险操作!
      this.$parent.formData.name = 'Hacker';
    }
  }
}
</script>

“为什么 Vue 要禁止子组件修改父数据?” “直接改不是更方便吗?” “如果必须改,该怎么办?”

本文将从 设计哲学实战模式,彻底解析 Vue 的单向数据流原则。


一、核心结论:绝对禁止!

子组件绝不能直接修改父组件的数据。

<!-- Parent.vue -->
<template>
  <Child :user="user" />
</template>

<script>
export default {
  data() {
    return {
      user: { name: 'Alice', age: 20 }
    }
  }
}
</script>
<!-- Child.vue -->
<script>
export default {
  props: ['user'],
  methods: {
    // ❌ 错误:直接修改 prop
    badWay() {
      this.user.name = 'Bob'; // ⚠️ Vue 会警告!
    },
    
    // ✅ 正确:通过事件通知父组件
    goodWay() {
      this.$emit('update:user', { ...this.user, name: 'Bob' });
    }
  }
}
</script>

二、为什么禁止?三大核心原因

🚫 1. 破坏单向数据流

父组件 → (props) → 子组件
   ↑
   └── (events) ← 子组件
  • 单向:数据流动清晰可预测;
  • 双向:数据可能从任意子组件修改,形成“意大利面条式”数据流。

💥 复杂应用中,你将无法追踪数据变化来源。


🚫 2. 导致难以调试

// 10 个子组件都可能修改 user.name
// 问题:name 何时、何地、被谁修改?
  • 控制台警告:

    [Vue warn]: Avoid mutating a prop directly...
    
  • 调试时需检查 所有子组件$emit$parent 调用。


🚫 3. 组件复用性降低

<!-- 假设 Child 可以直接修改 user -->
<Child :user="user1" />
<Child :user="user2" />

<!-- 如果 Child 修改了 user1,user2 也会被意外修改(引用传递) -->

✅ 组件应是“纯”的:相同输入 → 相同输出。


三、正确修改父数据的 4 种方式

✅ 1. v-model / .sync 修饰符(Vue 2)

方式一:v-model(默认 value / input

<!-- Parent -->
<Child v-model="userName" />

<!-- Child -->
<input 
  :value="value" 
  @input="$emit('input', $event.target.value)" 
/>

方式二:.sync 修饰符

<!-- Parent -->
<Child :user.sync="user" />

<!-- Child -->
<button @click="$emit('update:user', { ...user, name: 'New' })">
  更新
</button>

💡 .sync 本质是 :user + @update:user 的语法糖。


✅ 2. 自定义事件($emit

<!-- Parent -->
<Child 
  :config="config" 
  @change-config="updateConfig" 
/>

<!-- Child -->
<button @click="$emit('change-config', newConfig)">
  修改配置
</button>
// Parent method
updateConfig(newConfig) {
  this.config = newConfig;
}

✅ 3. 作用域插槽(传递方法)

<!-- Parent -->
<Child>
  <template #default="{ updateUser }">
    <button @click="updateUser({ name: 'New' })">
      通过插槽修改
    </button>
  </template>
</Child>
<!-- Child -->
<template>
  <div>
    <slot :updateUser="updateUser" />
  </div>
</template>

<script>
export default {
  methods: {
    updateUser(newData) {
      this.$emit('update:user', newData);
    }
  }
}
</script>

✅ 4. 状态管理(Vuex / Pinia)

// store.js
const userStore = defineStore('user', {
  state: () => ({ user: { name: 'Alice' } }),
  actions: {
    updateUser(payload) {
      this.user = { ...this.user, ...payload };
    }
  }
});

// Child.vue
import { useUserStore } from '@/stores/user';

export default {
  setup() {
    const userStore = useUserStore();
    return {
      updateUser: () => userStore.updateUser({ name: 'Bob' })
    }
  }
}

✅ 适合跨层级、复杂状态


四、特殊情况:如何“安全”地修改?

⚠️ 仅当 prop 是“配置对象”时

<!-- Parent -->
<Child :options="chartOptions" />

<!-- Child -->
<script>
export default {
  props: ['options'],
  mounted() {
    // ✅ 安全:只读取,不修改
    const chart = new Chart(this.$el, this.options);
  }
}
</script>

❌ 即使是配置对象,也不应修改其属性。


五、Vue 3 中的 definePropsdefineEmits

<script setup>
const props = defineProps(['user']);
const emit = defineEmits(['update:user']);

function changeName() {
  emit('update:user', { ...props.user, name: 'Charlie' });
}
</script>

definePropsdefineEmits 是 Vue 3 <script setup> 的推荐方式。


💡 结语

“单向数据流不是限制,而是自由的保障。”

方式 适用场景
$emit 简单父子通信
.sync / v-model 双向绑定场景
作用域插槽 需要传递方法
Vuex/Pinia 复杂全局状态
反模式 正确做法
this.$parent.xxx = value $emit('update:xxx', value)
直接修改 prop 对象属性 通过事件通知父组件

记住:

“子组件只应通过事件告诉父组件‘我想改变’,而非直接动手。”

掌握这一原则,你就能:

✅ 构建可维护的大型应用;
✅ 快速定位数据变更问题;
✅ 提升组件复用性;
✅ 为迁移到 Pinia 打下基础。

【vue篇】Vue 自定义指令完全指南:从入门到高级实战

作者 LuckySusu
2025年10月14日 23:02

在 Vue 开发中,你是否遇到过:

“如何让输入框自动聚焦?” “如何实现图片懒加载?” “如何集成 Chart.js 到 Vue 组件?”

数据驱动 无法满足需求时,自定义指令(Custom Directives)就是你的终极武器。

本文将从 基础语法高级实战,全面解析 Vue 自定义指令的用法与原理。


一、为什么需要自定义指令?

✅ Vue 的哲学

数据驱动视图” —— 大部分情况下,你只需修改数据,Vue 自动更新 DOM。

❌ 但有些场景例外

场景 数据驱动不足
输入框聚焦 无数据变化
图片懒加载 需监听 scroll 事件
集成第三方库(如 DatePicker 需直接操作 DOM
按钮权限控制(v-permission) 需动态显示/隐藏

💥 这些场景需要直接操作 DOM,此时自定义指令是最佳选择。


二、基础语法:钩子函数详解

📌 钩子函数执行时机

bind → inserted → update → componentUpdated → unbind
钩子 触发时机 典型用途
bind 指令第一次绑定到元素 初始化设置(如添加事件监听)
inserted 元素插入父节点 访问 DOM 尺寸、位置
update 组件 VNode 更新时 值变化时更新 DOM
componentUpdated 组件及其子组件更新后 执行依赖完整 DOM 的操作
unbind 指令解绑时 清理事件、定时器

🎯 钩子函数参数

function myDirective(el, binding, vnode, prevVnode) {
  // el: 绑定的 DOM 元素
  // binding: 指令对象
  // vnode: 虚拟节点
  // prevVnode: 上一个 VNode(仅 update/componentUpdated)
}

binding 对象详解

属性 示例 说明
value v-my-dir="msg"msg 的值 指令绑定的值
oldValue 更新前的值 仅在 update/componentUpdated 中可用
arg v-my-dir:arg'arg' 传入的参数
modifiers v-my-dir.mod1.mod2{ mod1: true, mod2: true } 修饰符对象
expression v-my-dir="a + b"'a + b' 绑定的表达式字符串

三、定义方式

✅ 1. 全局指令

Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

✅ 2. 局部指令

<template>
  <input v-focus />
</template>

<script>
export default {
  directives: {
    focus: {
      inserted(el) {
        el.focus();
      }
    }
  }
}
</script>

四、初级应用:5 个经典案例

🎯 1. 自动聚焦(v-focus

Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});
<input v-focus />

🎯 2. 点击外部关闭(v-click-outside

Vue.directive('click-outside', {
  bind(el, binding) {
    const handler = (e) => {
      if (!el.contains(e.target)) {
        binding.value(e); // 执行传入的函数
      }
    };
    document.addEventListener('click', handler);
    el._clickOutside = handler;
  },
  unbind(el) {
    document.removeEventListener('click', el._clickOutside);
  }
});
<div v-click-outside="closeMenu">菜单</div>

🎯 3. 相对时间(v-timeago

Vue.directive('timeago', {
  bind(el, binding) {
    const date = new Date(binding.value);
    el.textContent = `${Math.floor((Date.now() - date) / 60000)}分钟前`;
  },
  update(el, binding) {
    // 值变化时更新
    if (binding.value !== binding.oldValue) {
      const date = new Date(binding.value);
      el.textContent = `${Math.floor((Date.now() - date) / 60000)}分钟前`;
    }
  }
});
<span v-timeago="post.createdAt"></span>

🎯 4. 按钮权限(v-permission

Vue.directive('permission', {
  bind(el, binding) {
    const userRoles = this.$store.getters.roles;
    if (!userRoles.includes(binding.value)) {
      el.parentNode.removeChild(el); // 移除无权限的按钮
    }
  }
});
<button v-permission="'admin'">删除</button>

🎯 5. 滚动动画(v-scroll

Vue.directive('scroll', {
  inserted(el, binding) {
    const onScroll = () => {
      if (window.scrollY > 100) {
        el.classList.add('scrolled');
      } else {
        el.classList.remove('scrolled');
      }
    };
    window.addEventListener('scroll', onScroll);
    el._scrollHandler = onScroll;
  },
  unbind(el) {
    window.removeEventListener('scroll', el._scrollHandler);
  }
});
<header v-scroll></header>

五、高级应用:2 个深度实战

🚀 1. 图片懒加载(v-lazy

const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove('lazy');
      imageObserver.unobserve(img);
    }
  });
});

Vue.directive('lazy', {
  bind(el, binding) {
    el.dataset.src = binding.value;
    el.classList.add('lazy');
    imageObserver.observe(el);
  },
  update(el, binding) {
    if (binding.value !== binding.oldValue) {
      el.dataset.src = binding.value;
      // 如果已进入视口,立即加载
      if (el.getBoundingClientRect().top < window.innerHeight * 1.5) {
        el.src = binding.value;
      }
    }
  },
  unbind(el) {
    imageObserver.unobserve(el);
  }
});
<img v-lazy="imageUrl" />

🚀 2. 集成 ECharts(v-chart

Vue.directive('chart', {
  bind(el) {
    el._chart = echarts.init(el);
  },
  update(el, binding) {
    const chart = el._chart;
    if (binding.value) {
      chart.setOption(binding.value, true);
    }
  },
  unbind(el) {
    el._chart.dispose();
  }
});
<div v-chart="chartOption" style="width: 400px; height: 300px;"></div>

六、重要注意事项

⚠️ 1. 不要修改 v-model 绑定的值

<input v-model="msg" v-my-directive />
  • ❌ 在指令中直接 el.value = 'new'msg 不会更新;
  • ✅ 正确做法:触发 inputchange 事件。
el.value = 'new';
el.dispatchEvent(new Event('input'));

⚠️ 2. 清理副作用

  • unbind 中移除事件监听;
  • 清除定时器;
  • 销毁第三方实例(如 ECharts)。

⚠️ 3. 性能优化

  • 避免在 update 中做昂贵操作;
  • 使用 binding.valuebinding.oldValue 判断是否需要更新。

💡 结语

“自定义指令是 Vue 的‘最后一公里’解决方案。”

场景 推荐方案
简单 DOM 操作 自定义指令
复杂逻辑复用 Mixin / Composition API
UI 组件 普通组件
钩子 使用场景
bind 初始化
inserted 访问布局
update 值变化
unbind 清理资源

掌握自定义指令,你就能:

✅ 实现原生 DOM 操作;
✅ 集成第三方库;
✅ 创建可复用的 DOM 行为;
✅ 补充数据驱动的不足。

昨天以前首页

【vue篇】单页 vs 多页:Vue 应用架构的终极对决

作者 LuckySusu
2025年10月11日 12:02

在启动新项目时,你是否纠结过:

“我该选择 SPA 还是 MPA?”

“为什么后台系统用 MPA,而管理后台用 SPA?”

“SEO 友好和用户体验,必须二选一吗?”

本文将从 用户体验、性能、SEO、开发模式 四大维度,全面解析 Vue 单页(SPA)与多页(MPA)应用的核心差异。


一、核心概念对比

维度 单页应用 (SPA) 多页应用 (MPA)
页面数量 1 个 HTML 多个 HTML
跳转方式 前端路由切换组件 页面跳转,重新加载
资源加载 首次加载所有资源 每页独立加载资源
代表框架 Vue + Vue Router Vue + Webpack 多入口

二、深度对比:七大核心差异

🔋 1. 首次加载性能

类型 优势 劣势
SPA 后续跳转极速 首屏慢(需下载整个 JS 包)
MPA 首屏快(按需加载) 每次跳转都要重新加载

💡 SPA 优化:代码分割(Code Splitting)、懒加载、预加载。


⚡ 2. 用户体验

类型 体验 典型场景
SPA 类似原生 App,流畅无刷新 后台管理系统、在线编辑器
MPA 传统网页,有刷新感 企业官网、博客

✅ SPA 更适合高交互、复杂状态的应用。


🔍 3. SEO 友好性

类型 SEO 支持 解决方案
SPA ❌ 差(初始 HTML 空白) SSR(Nuxt.js)、预渲染
MPA ✅ 好(每页有完整内容) 天然支持

📌 内容型网站(如新闻、电商)优先考虑 MPA 或 SPA + SSR。


🧩 4. 开发与维护

维度 SPA MPA
状态管理 集中式(Vuex/Pinia) 分散式
组件复用 高(全局组件) 低(需手动引入)
开发复杂度 高(路由、状态) 低(接近传统开发)
调试难度 中等 简单

💬 SPA 更适合中大型团队,MPA 适合小型项目或团队


📦 5. 打包与部署

类型 打包结果 部署方式
SPA 1 个 index.html + JS/CSS 部署到静态服务器
MPA 多个 HTML + 资源 需配置多入口,部署复杂

⚠️ MPA 需要 Webpack 多入口配置:

// webpack.config.js
module.exports = {
  entry: {
    home: './src/home.js',
    about: './src/about.js',
    admin: './src/admin.js'
  },
  output: {
    filename: '[name].js'
  }
}

🔐 6. 安全性

类型 风险 优势
SPA 所有逻辑暴露在前端 后端只需提供 API
MPA 每页独立,攻击面分散 服务端可做更多校验

💡 SPA 更依赖后端 API 安全


📱 7. 缓存策略

类型 缓存优势 注意点
SPA JS/CSS 长期缓存,HTML 不缓存 更新后需用户刷新
MPA 每页可独立缓存 资源重复下载

✅ SPA 更适合频繁更新的应用。


三、Vue 中如何实现?

🎯 SPA 实现(标准方式)

npm install vue-router
// router/index.js
import { createRouter } from 'vue-router'

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
]

const router = createRouter({ history: createWebHistory(), routes })

export default router
<!-- App.vue -->
<template>
  <div id="app">
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
    <router-view />
  </div>
</template>

🎯 MPA 实现(Webpack 多入口)

// vue.config.js (Vue CLI)
module.exports = {
  pages: {
    home: {
      entry: 'src/pages/home/main.js',
      template: 'public/home.html',
      filename: 'home.html'
    },
    about: {
      entry: 'src/pages/about/main.js',
      template: 'public/about.html',
      filename: 'about.html'
    }
  }
}
// src/pages/home/main.js
import { createApp } from 'vue'
import Home from './Home.vue'

createApp(Home).mount('#app')

四、如何选择?决策树

你的应用是内容型网站? → 是 → MPA 或 SPA + SSR
                    ↓ 否
是否需要极致用户体验? → 是 → SPA
                    ↓ 否
项目是否简单、页面独立? → 是 → MPA
                    ↓ 否
团队是否有 SSR 能力? → 是 → SPA + SSR
                    ↓ 否 → SPA(接受 SEO 折衷)

五、混合架构:最佳实践

🌟 场景:企业级应用

  • 官网、博客:MPA 或 SSR SPA(SEO 友好)
  • 管理后台:SPA(交互复杂)
  • 移动端 H5:SPA(快速加载)

💡 使用 微前端 架构整合 SPA 与 MPA。


💡 结语

选择 推荐场景
SPA 后台系统、Web App、高交互应用
MPA 企业官网、博客、简单营销页

“没有最好的架构,只有最合适的方案。”

掌握 SPA 与 MPA 的差异,你就能:

✅ 根据业务需求选择技术栈;
✅ 规避首屏性能、SEO 等陷阱;
✅ 设计更合理的前端架构。

【vue篇】Vue 数组响应式揭秘:如何让 push 也能更新视图?

作者 LuckySusu
2025年10月11日 12:02

在 Vue 开发中,你是否思考过:

“为什么 this.items.push(newItem) 能自动更新页面?”

“Vue 是如何监听数组内部变化的?”

sort()reverse() 为何也能触发视图更新?”

这背后是 Vue 对数组方法的精妙劫持与增强

本文将从 Object.defineProperty 的局限Vue 源码级实现,彻底解析 Vue 数组响应式的黑科技。


一、问题根源:Object.defineProperty 的缺陷

✅ Vue 2 响应式原理

Vue 2 使用 Object.defineProperty 劫持对象属性的 getset

Object.defineProperty(obj, 'prop', {
  get() { /* 依赖收集 */ },
  set(newVal) { /* 派发更新 */ }
});

❌ 数组的“盲区”

const arr = ['a', 'b'];
arr[2] = 'c';        // ❌ 无法触发 set
arr.length = 0;      // ❌ 无法触发 set
arr.push('d');       // ❌ 原生方法不走 set

💥 Object.defineProperty 无法监听

  • 数组索引赋值;
  • 数组长度变化;
  • 数组方法调用。

二、Vue 的解决方案:方法劫持

✅ 核心思路

“既然不能监听属性,那就劫持会改变数组的方法!”

Vue 创建了一个增强版数组原型,覆盖了 7 个会改变原数组的方法。


三、源码级实现:arrayMethods 详解

📌 步骤 1:创建增强原型

// 缓存原生 Array.prototype
const arrayProto = Array.prototype;

// 创建新对象,__proto__ 指向原生原型
export const arrayMethods = Object.create(arrayProto);

💡 这样既能保留原生功能,又能扩展新逻辑。


📌 步骤 2:拦截变异方法

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

methodsToPatch.forEach(function(method) {
  const original = arrayProto[method]; // 保存原生方法
  
  def(arrayMethods, method, function mutator(...args) {
    // 1. 执行原生逻辑
    const result = original.apply(this, args);
    
    // 2. 获取 Observer 实例
    const ob = this.__ob__;
    
    // 3. 处理新增元素(需要响应式化)
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args; // 新增的元素
        break;
      case 'splice':
        inserted = args.slice(2); // splice(索引, 删除数, 新增元素...)
        break;
    }
    
    // 4. 对新增元素进行响应式处理
    if (inserted) {
      ob.observeArray(inserted);
    }
    
    // 5. 通知依赖更新!
    ob.dep.notify();
    
    // 6. 返回原生方法结果
    return result;
  });
});

四、如何让数组使用增强方法?

✅ 在 Observer 中“偷天换日”

class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    
    // 关键:修改数组的原型
    if (Array.isArray(value)) {
      // value.__proto__ = arrayMethods
      value.__proto__ = arrayMethods;
      
      // 或对非浏览器环境使用方法拷贝
      // def(value, method, arrayMethods[method])
    } else {
      this.walk(value);
    }
  }
}

💥 这样一来,所有响应式数组调用 push 等方法时,实际执行的是增强版方法


五、7 个被劫持的方法详解

方法 是否新增元素 是否触发更新
push(...items)
pop() ✅(长度变)
shift() ✅(长度变)
unshift(...items)
splice(start, deleteCount, ...items)
sort() ✅(顺序变)
reverse() ✅(顺序变)

⚠️ 注意:popshift 虽不新增,但改变了数组,所以也要 notify


六、实战演示

📌 场景:动态添加列表项

<template>
  <ul>
    <li v-for="item in items" :key="item.id">{{ item.name }}</li>
  </ul>
  <button @click="addItem">添加</button>
</template>

<script>
export default {
  data() {
    return {
      items: [{ id: 1, name: 'A' }]
    };
  },
  methods: {
    addItem() {
      // 调用被劫持的 push
      this.items.push({ id: 2, name: 'B' });
      // 1. 执行原生 push
      // 2. observeArray([{ id: 2, name: 'B' }]) → 响应式化
      // 3. dep.notify() → 视图更新
    }
  }
}
</script>

七、Vue 3 的革命:Proxy 彻底解决

✅ Vue 3 响应式原理

const arr = reactive(['a', 'b']);
arr[2] = 'c';     // ✅ Proxy 捕获 set
arr.length = 0;   // ✅ 捕获 length 变化
arr.push('d');    // ✅ 原生方法调用,但 Proxy 仍能感知数组变化

💥 Vue 3 不再需要劫持数组方法Proxy 天然支持所有变化监听。


八、其他数组操作的注意事项

❌ Vue 无法检测的操作

// 1. 索引直接赋值
this.items[0] = { ... }; // ❌

// 2. 修改数组长度
this.items.length = 0;   // ❌

✅ 解决方案(Vue 2)

// 使用 $set
this.$set(this.items, 0, { ... });

// 使用 splice
this.items.splice(0, 1, { ... });

// 清空数组
this.items.splice(0);
// 或
this.items = [];

💡 结语

“Vue 的数组响应式,是工程智慧的典范。”

机制 Vue 2 Vue 3
核心技术 方法劫持 + __proto__ Proxy
劫持方法 7 个变异方法 无需劫持
监听能力 部分支持 完全支持
方法 增强逻辑
push 响应式化新元素 + 通知更新
splice 响应式化新增项 + 通知更新
sort 直接触发更新(顺序变)

掌握这一机制,你就能:

✅ 理解 push 为何能更新视图;
✅ 避开 this.items[0] = value 的陷阱;
✅ 在 Vue 2 和 Vue 3 间平滑迁移。

【vue篇】Vue 响应式陷阱:动态添加对象属性为何不更新?如何破解?

作者 LuckySusu
2025年10月11日 12:02

在 Vue 开发中,你是否遇到过这样的“诡异”现象?

“我明明给对象加了新属性,console.log 显示它存在,但页面就是不更新!”

“为什么 this.obj.newProp = 'value' 不触发视图刷新?”

这背后隐藏着 Vue 响应式系统的根本限制

本文将从 Object.defineProperty 的缺陷Vue 3 的 Proxy 革命,彻底解析这一经典问题。


一、问题重现:动态添加属性,视图不更新

📌 场景代码

<template>
  <div>
    <ul>
      <li v-for="(value, key) in obj" :key="key">{{ key }}: {{ value }}</li>
    </ul>
    <button @click="addProp">添加 obj.b</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: {
        a: 'obj.a'
      }
    }
  },
  methods: {
    addProp() {
      this.obj.b = 'obj.b'; // ❌ 视图不更新
      console.log(this.obj); // ✅ 控制台显示 { a: 'obj.a', b: 'obj.b' }
    }
  }
}
</script>

🚨 现象

  • 控制台:obj.b 成功添加;
  • 页面:列表没有新增 b: obj.b

二、根本原因:Vue 2 的响应式机制缺陷

✅ Vue 2 响应式原理

Vue 2 使用 Object.defineProperty() 劫持对象属性:

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集
      return val;
    },
    set(newVal) {
      val = newVal;
      // 派发更新
      updateView();
    }
  });
}

❌ 问题所在

在组件初始化时,Vue 会递归遍历 data 对象的所有属性,并将其转换为响应式。

// 初始化时,只处理了 'a'
{
  obj: {
    a: 'obj.a'  // ✅ 被 defineProperty 劫持
    // b 不存在,所以不会被劫持
  }
}

当你执行:

this.obj.b = 'obj.b'; // 直接赋值,未经过 defineProperty
  • b 属性未被劫持
  • 没有 setter,无法触发 updateView()
  • 所以视图不更新。

三、解决方案(Vue 2)

✅ 方案 1:this.$set()(推荐)

addProp() {
  this.$set(this.obj, 'b', 'obj.b'); // ✅ 视图更新
}

🔍 this.$set 做了什么?

Vue.prototype.$set = function(obj, key, val) {
  defineReactive(obj, key, val); // 手动将新属性变为响应式
  updateView(); // 触发视图更新
}

💡 本质:手动调用 defineReactive


✅ 方案 2:Vue.set()

import Vue from 'vue';

addProp() {
  Vue.set(this.obj, 'b', 'obj.b'); // ✅ 同 $set
}

✅ 方案 3:使用 Object.assign() 或 展开运算符

addProp() {
  this.obj = Object.assign({}, this.obj, { b: 'obj.b' });
  // 或
  this.obj = { ...this.obj, b: 'obj.b' };
}

🔍 原理

  • 创建一个新对象
  • Vue 检测到 obj 被重新赋值(引用变化);
  • 触发整个对象的响应式更新。

⚠️ 缺点:性能稍差,因为是全量更新


四、Vue 3 的革命性突破:Proxy

✅ Vue 3 响应式原理

Vue 3 使用 Proxy 代理整个对象:

const reactive = (obj) => {
  return new Proxy(obj, {
    get(target, key) {
      // 依赖收集
      return target[key];
    },
    set(target, key, val) {
      target[key] = val;
      // 派发更新
      updateView();
      return true;
    }
  });
}

🎉 自动支持动态添加

const obj = reactive({ a: 'obj.a' });
obj.b = 'obj.b'; // ✅ 自动触发 setter,视图更新!

💥 Vue 3 中,this.obj.b = 'value' 可以直接更新视图!


五、其他“陷阱”场景

📌 场景 1:数组索引赋值

this.items[0] = 'new'; // ❌ Vue 2 不更新

✅ 解决方案

// $set
this.$set(this.items, 0, 'new');

// 或 splice
this.items.splice(0, 1, 'new');

// 或 length
this.items.length = 0; // 清空

📌 场景 2:删除属性

delete this.obj.a; // ❌ 不更新

✅ 解决方案

this.$delete(this.obj, 'a'); // ✅

六、最佳实践

✅ Vue 2 最佳实践

操作 推荐方法
添加属性 this.$set(obj, 'key', val)
删除属性 this.$delete(obj, 'key')
更新数组 splicesliceconcat

✅ Vue 3 最佳实践

  • ✅ 直接使用 obj.newKey = value
  • ✅ 直接使用 delete obj.key
  • ⚠️ 仍建议使用 refreactive 的规范用法。

💡 结语

“理解响应式机制,才能避开 Vue 的‘坑’。”

版本 机制 动态添加支持
Vue 2 Object.defineProperty ❌ 需 $set
Vue 3 Proxy ✅ 原生支持
方法 适用场景
$set Vue 2 动态添加属性
Object.assign 简单场景,可接受性能损耗
Proxy Vue 3,开箱即用

记住:

“在 Vue 2 中,不要直接给响应式对象添加属性,否则你会掉进‘响应式陷阱’。”

【vue篇】Vue 异步更新之魂:$nextTick 原理与实战全解

作者 LuckySusu
2025年10月11日 12:01

在Vue 开发中,你是否遇到过这样的现象?

“我明明修改了 message,为什么 document.getElementById('msg').innerText 还是旧值?”

“在 created 钩子中操作 DOM,报错 Cannot read property 'xxx' of null?”

“表单输入后,this.$refs.input.focus() 失效?”

$nextTick 就是解决这些问题的“时间控制器”。

它能确保你在DOM 更新完成后执行代码。

但你是否真正理解:

  • Vue 为什么需要异步更新?
  • $nextTick 如何利用 EventLoop
  • 它的内部优先级策略是什么?

本文将从 浏览器事件循环Vue 源码,彻底解析 $nextTick 的核心机制。


一、问题场景:为什么需要 $nextTick

📌 场景 1:数据更新后立即操作 DOM

this.message = 'Hello';
console.log(this.$el.textContent); // ❌ 可能还是旧值

📌 场景 2:在 created 钩子中操作 DOM

created() {
  console.log(this.$el); // ❌ null,DOM 尚未挂载
}

📌 场景 3:动态添加元素后获取焦点

this.showInput = true;
this.$refs.input.focus(); // ❌ 报错,元素还未渲染

💥 根本原因:Vue 的 DOM 更新是异步的。


二、核心机制:异步更新队列

✅ 为什么异步更新?

  1. 性能优化

    • 同步更新:每次 data 变化都触发 render → 低效;
    • 异步更新:将多次数据变更合并为一次 render → 高效。
  2. Virtual DOM 计算

    • 状态变化 → 触发 Watcher → 推入异步队列;
    • 下一个“tick”中,批量执行 patch 更新 DOM。

📊 异步更新流程

data change
    ↓
Watcher.enqueue()
    ↓
queue.push(watcher)
    ↓
nextTick(flushSchedulerQueue)
    ↓
DOM 更新完成
    ↓
$nextTick 回调执行

三、$nextTick 原理:EventLoop 的精妙应用

✅ 核心思想

利用 JavaScript 的 事件循环(EventLoop) 机制,在 DOM 更新后执行回调。

🔁 降级策略(优先级从高到低)

方法 类型 浏览器支持
Promise 微任务 高版本浏览器、Node.js
MutationObserver 微任务 现代浏览器
setImmediate 宏任务 IE、Node.js
setTimeout(fn, 0) 宏任务 所有环境

📌 源码简化版

const callbacks = [];
let pending = false;

function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

let timerFunc;

// 1. Promise (优先)
if (typeof Promise !== 'undefined') {
  timerFunc = () => {
    Promise.resolve().then(flushCallbacks);
  };
}
// 2. MutationObserver
else if (typeof MutationObserver !== 'undefined') {
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, { characterData: true });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
}
// 3. setImmediate
else if (typeof setImmediate !== 'undefined') {
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
}
// 4. setTimeout
else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    timerFunc(); // 执行异步任务
  }
}

四、微任务 vs 宏任务:性能差异

类型 执行时机 性能 兼容性
微任务 当前宏任务结束前 ⚡ 更快 较新浏览器
宏任务 下一个宏任务 🐢 稍慢 全兼容

💡 为什么优先微任务?

  • 更快执行回调;
  • 避免不必要的页面重绘。

五、实战应用:何时使用 $nextTick

✅ 场景 1:数据更新后操作 DOM

this.message = 'New Message';
this.$nextTick(() => {
  // DOM 已更新
  console.log(this.$el.textContent); // ✅ 'New Message'
});

✅ 场景 2:动态组件后获取 ref

this.showModal = true;
this.$nextTick(() => {
  this.$refs.modal.focus(); // ✅ 元素已渲染
});

✅ 场景 3:在 created 钩子中操作 DOM

created() {
  this.$nextTick(() => {
    // DOM 挂载完成
    this.initChart();
  });
}

✅ 场景 4:计算元素尺寸

this.items.push(newItem);
this.$nextTick(() => {
  const height = this.$el.offsetHeight;
  console.log('新高度:', height);
});

六、Vue 3 中的 nextTick

Vue 3 使用 Promise 作为主要实现,更简洁:

import { nextTick } from 'vue';

// Composition API
setup() {
  const updateMessage = async () => {
    state.message = 'Updated';
    await nextTick();
    console.log('DOM 已更新');
  };
}

七、避坑指南

❌ 错误用法

// ❌ 不要这样:依赖 setTimeout
setTimeout(() => {
  console.log(this.$el.textContent);
}, 100);

// ❌ 不要这样:在数据变化前调用
this.$nextTick(() => { /* ... */ });
this.message = 'new'; // 回调可能在更新前执行!

✅ 正确模式

// ✅ 先改数据,再 $nextTick
this.message = 'new';
this.$nextTick(() => { /* ... */ });

💡 结语

$nextTick 是 Vue 异步世界的桥梁。”

要点 说明
本质 利用 EventLoop 实现异步回调
目的 确保 DOM 更新后执行代码
优先级 Promise → MutationObserver → setImmediate → setTimeout
类型 微任务优先,性能更优

掌握 $nextTick,你就能:

✅ 精准控制 DOM 操作时机;
✅ 避免“数据变了,视图没变”的尴尬;
✅ 编写出更健壮的 Vue 应用。

【vue篇】Vue 性能优化神器:keep-alive 深度解析与实战指南

作者 LuckySusu
2025年10月11日 12:01

在 Vue 开发中,你是否遇到过:

“切换页面时,列表滚动位置丢失?”

“组件反复创建销毁,性能卡顿?”

“表单输入内容在路由切换后清空?”

keep-alive 就是解决这些问题的“银弹”。

它能缓存组件状态,避免重复渲染,大幅提升用户体验。

但你是否真正理解:

  • keep-alive 缓存的是什么?
  • 它是如何实现的?
  • LRU 策略如何工作?

本文将从 源码级 深度解析 keep-alive 的核心机制。


一、keep-alive 是什么?

✅ 核心定义

keep-alive 是 Vue 的内置抽象组件,用于:

缓存动态组件或路由视图的实例,避免重复销毁与重建

📌 基础用法

<!-- 缓存所有匹配的组件 -->
<keep-alive>
  <component :is="currentView"></component>
</keep-alive>

<!-- 或缓存路由视图 -->
<keep-alive>
  <router-view></router-view>
</keep-alive>

二、三大核心属性

属性 类型 说明
include string | RegExp | Array 只缓存名称匹配的组件
exclude string | RegExp | Array 不缓存名称匹配的组件
max number | string 最多缓存组件实例数量,实现内存控制

💡 实战示例

<keep-alive 
  include="UserInfo,OrderList"
  exclude="SearchResult"
  :max="10"
>
  <router-view />
</keep-alive>
  • ✅ 缓存 UserInfoOrderList
  • ❌ 不缓存 SearchResult
  • ⚠️ 最多缓存 10 个实例,超出时按 LRU 策略清除。

三、keep-alive 如何工作?源码级解析

🧩 核心数据结构

// 初始化缓存
created() {
  this.cache = Object.create(null); // 存储 { key: vnode }
  this.keys = []; // 存储 key,实现 LRU
}
  • cache: 缓存对象,keyvnode 映射;
  • keys: key 数组,记录访问顺序,实现 LRU(最近最少使用) 策略。

🔁 render 函数核心流程

步骤 1:获取第一个子组件

const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot);

💡 keep-alive 只对第一个子组件生效。


步骤 2:判断是否需要缓存

const name = getComponentName(vnode.componentOptions);

// 不在 include 或在 exclude 中,直接返回
if (
  (include && !matches(include, name)) ||
  (exclude && matches(exclude, name))
) {
  return vnode;
}

步骤 3:生成唯一 key

const key = vnode.key == null
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key;

❗ 为什么不用 cid 唯一标识?

因为同一个构造函数可以注册为多个本地组件cid 相同但组件不同。


步骤 4:缓存命中与 LRU 更新

if (cache[key]) {
  // 命中缓存
  vnode.componentInstance = cache[key].componentInstance;
  
  // LRU:将 key 移到末尾(最新使用)
  remove(keys, key);
  keys.push(key);
} else {
  // 未命中,加入缓存
  cache[key] = vnode;
  keys.push(key);
  
  // 超出 max,清除最久未使用的
  if (max && keys.length > parseInt(max)) {
    pruneCacheEntry(cache, keys[0], keys);
  }
}

步骤 5:标记 keepAlive

vnode.data.keepAlive = true;

这个标记告诉 Vue:跳过组件的 createdmounted 等生命周期


四、缓存的是什么?真相揭秘

✅ 正确答案

keep-alive 缓存的是:

组件的 vnode 实例 和 componentInstance(组件实例)

包括:

  • 组件的 DOM 结构;
  • 组件的 data 状态;
  • 事件监听器;
  • 子组件树。

❌ 常见误解

  • ❌ 不是只缓存 HTML 字符串;
  • ❌ 不是只缓存 data 对象;
  • ❌ 不是“冻结”组件。

五、生命周期钩子:activateddeactivated

当组件被 keep-alive 包裹时,会新增两个生命周期钩子:

钩子 触发时机
activated 组件被激活(从缓存中显示)时调用
deactivated 组件被缓存(切换离开)时调用

💡 实战应用

export default {
  data() {
    return {
      timer: null
    };
  },
  activated() {
    // 页面可见时恢复轮询
    this.timer = setInterval(() => {
      console.log('轮询中...');
    }, 1000);
  },
  deactivated() {
    // 页面隐藏时清除轮询,避免内存泄漏
    clearInterval(this.timer);
  }
}

六、首次渲染 vs 缓存渲染

📌 首次渲染

init(vnode) {
  // 初次渲染,componentInstance 为 undefined
  const child = createComponentInstanceForVnode(vnode);
  child.$mount();
}
  • 正常执行 beforeCreatecreatedmounted

📌 缓存渲染

init(vnode) {
  if (vnode.componentInstance && vnode.data.keepAlive) {
    // 直接复用实例,执行 prepatch
    componentVNodeHooks.prepatch(vnode, vnode);
  }
}
  • 跳过 createdmounted
  • 直接将缓存的 DOM 插入文档;
  • 触发 activated 钩子。

七、LRU 缓存策略详解

📌 LRU(Least Recently Used)

“最近使用的,将来最可能再次使用。”

🔁 工作机制

操作 行为
新组件 插入 keys 数组末尾
命中缓存 将 key 从原位置移除,插入末尾
超出 max 移除 keys[0](最久未使用)

📊 示例:max=3

操作: A → B → C → A → D
keys: [A] → [A,B] → [A,B,C] → [B,C,A] → [C,A,D]
  • D 加入时,B 被清除(最久未使用)。

八、最佳实践与避坑指南

✅ 最佳实践

  1. 合理设置 max:避免内存泄漏;
  2. deactivated 中清理资源:定时器、事件监听;
  3. 使用 include 精准控制:避免缓存重型组件。

❌ 常见错误

  • 忘记给组件命名(name 选项);
  • created 中启动轮询,未在 deactivated 中清除;
  • 缓存大量列表组件,导致内存飙升。

💡 结语

keep-alive 是性能与体验的平衡艺术。”

要点 说明
缓存内容 vnode + 组件实例
核心机制 cache + keys + LRU
生命周期 activated / deactivated
适用场景 频繁切换的路由、表单、列表

掌握 keep-alive,你就能:

✅ 显著提升应用流畅度;
✅ 保留用户交互状态;
✅ 优化关键路径性能。

【vue篇】Vue 核心机制揭秘:为什么组件的 data 必须是函数?

作者 LuckySusu
2025年10月11日 12:01

在 Vue 开发中,你一定见过这样的代码:

// ❌ 错误写法
data: {
  message: 'Hello'
}

// ✅ 正确写法
data() {
  return {
    message: 'Hello'
  }
}

但你是否思考过:

“为什么组件的 data 必须是函数,而根实例可以是对象?”

“如果写成对象会怎样?”

本文将从 内存模型、实例化机制、响应式系统 三个维度,彻底解析 data 为何必须是函数。


一、问题重现:如果 data 是对象,会发生什么?

📌 场景模拟

<!-- Counter.vue -->
<template>
  <div>
    <p>计数: {{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
export default {
  // ❌ 危险!data 是对象
  data: {
    count: 0
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}
</script>
<!-- App.vue -->
<template>
  <div>
    <Counter />
    <Counter />
    <Counter />
  </div>
</template>

🚨 实际行为

  1. 点击第一个组件的按钮;
  2. 所有三个组件的 count 同时增加!

💥 状态污染:所有实例共享同一个 data 对象。


二、根本原因:JavaScript 的对象引用机制

📌 内存模型分析

// ❌ 错误方式:所有实例引用同一个对象
const sharedData = { count: 0 };

const vm1 = { data: sharedData }; // 指向同一块内存
const vm2 = { data: sharedData }; // 指向同一块内存
const vm3 = { data: sharedData }; // 指向同一块内存
  • 修改 vm1.data.countsharedData 改变 → vm2vm3 也受影响。

✅ 正确方式:每个实例拥有独立数据

// ✅ 正确方式:工厂函数返回新对象
function createData() {
  return { count: 0 };
}

const vm1 = { data: createData() }; // 新对象
const vm2 = { data: createData() }; // 新对象
const vm3 = { data: createData() }; // 新对象
  • 每个实例的 data 指向不同的内存地址,互不影响。

三、Vue 源码中的 initData 逻辑

在 Vue 初始化过程中,initData 函数会处理 data

function initData(vm) {
  let data = vm.$options.data;
  
  // 判断 data 是否为函数
  if (typeof data === 'function') {
    // 调用工厂函数,获取全新对象
    data = data.call(vm);
  }
  
  // 将 data 响应式化
  observe(data);
  
  // 代理到 vm 实例
  proxy(vm, 'data', key);
}
  • data()call() → 返回新对象 → 响应式化;
  • data{} → 直接使用 → 所有实例共用 → 状态污染。

四、为什么根实例可以是对象?

// ✅ 合法:根实例
new Vue({
  el: '#app',
  data: {
    message: 'Hello'
  }
})

✅ 原因:单例原则

  • 一个 Vue 应用只有一个根实例;
  • 不存在“多个实例共享数据”的问题;
  • 虽然技术上可以写成函数,但没必要。

💡 类比:全局变量可以是对象,因为只有一个。


五、深入理解:组件复用的本质

📌 组件 = 工厂函数

// 组件定义
const MyComponent = {
  data() {
    return { count: 0 }
  }
}

// 每次使用 <my-component>,相当于:
const instance1 = new ComponentFactory(MyComponent);
const instance2 = new ComponentFactory(MyComponent);
  • data() 就像工厂中的“原材料生成器”,每次生产都提供全新的原材料
  • data{} 就像所有产品共用同一块原材料,一损俱损。

六、TypeScript 中的体现

在 Vue 3 的 Composition API 或 TypeScript 中,这一原则更加清晰:

// Vue 3 + TS
export default defineComponent({
  data() {
    return {
      count: 0,
      list: [] as string[]
    }
  }
})
  • 类型系统强制要求 data 是函数;
  • 提供更好的类型推断和开发体验。

七、常见误区与最佳实践

❌ 误区 1:认为“函数更高级”

✅ 正确认知:这是语言特性(引用类型)和设计模式(工厂模式)的必然选择。

❌ 误区 2:在函数中返回同一个对象

// ❌ 仍然错误!
const shared = { count: 0 };
data() {
  return shared; // 所有实例仍共享
}

✅ 最佳实践

  1. 始终使用函数形式
  2. 避免闭包污染
// ❌ 危险:闭包共享
let count = 0;
data() {
  return { count: count++ }; // 状态跨实例累积!
}

💡 结语

“data 为函数,是 Vue 组件可复用的基石。”

关键点 说明
引用类型 对象是引用,函数可返回新实例
工厂模式 data() 是数据工厂,生产独立状态
响应式安全 避免多个实例的 observe 相互干扰
设计哲学 组件应是“独立、可复用”的单元

记住:

“组件的 data 必须是函数,否则你的应用将陷入状态混乱的泥潭。”

❌
❌