20251017-Vue2八股文整理(上篇)

🧭 一、Vue 的生命周期是什么?(就像"人一生的流程")

你可以把一个 Vue 组件想成一个人,它也有从出生 → 工作 → 退休 的过程。 这个过程在 Vue 里就叫做 生命周期(Life Cycle)

🍼 beforeCreate:刚出生,还没张开眼。 👶 created:开始能看见世界了(可以访问 data、methods)。 🏗 beforeMount:在布置房间(准备把模板挂到页面上)。 🏠 mounted:正式入住(页面元素渲染出来了)。 🔄 beforeUpdate:房间要装修(数据变了、准备更新)。 🧱 updated:装修完成(页面内容更新了)。 🧹 beforeDestroy:准备搬家(解绑监听器、清理)。 💀 destroyed:人走茶凉,彻底消失。


⚙️ 二、生命周期阶段详解(结合"奶茶店装修"例子)

阶段 场景解释 比喻
beforeCreate Vue 实例刚被创建,还没 data、methods 奶茶店还在图纸上
created 数据、方法都能用了 装修好了,但还没开门
beforeMount Vue 准备把模板挂到页面 准备贴"菜单"、"价目表"
mounted 页面显示了(DOM 挂载成功) 正式开业,顾客能点单
beforeUpdate 数据变了,但页面还没刷新 奶茶口味配方改了,还没通知前台
updated 页面更新完成 前台菜单同步更新完毕
beforeDestroy 组件即将被销毁,可做清理工作 准备关门,把机器断电
destroyed 销毁完成,解绑所有事件 奶茶店关门歇业

🧠 三、生命周期顺序图(记忆口诀)

创建阶段:beforeCreate → created → beforeMount → mounted 更新阶段:beforeUpdate → updated 销毁阶段:beforeDestroy → destroyed

📦 记忆口诀

"先造人,再挂家;家装完,再装修;最后再搬家。"


🔍 四、created 和 mounted 的区别

这两个阶段最容易搞混,小可爱记这个口诀:

🐣 created:数据可以访问,但 DOM 还没准备好。 🏠 mounted:DOM 已经渲染好,可以操作页面元素。

举例:

javascript 复制代码
created() {
  console.log(this.$el) // undefined,还没挂载
}
mounted() {
  console.log(this.$el) // 可以访问到真实 DOM 节点
}

🌸生活比喻:

  • created:你刚搬进新家(知道自己房间布局),但家具还没送到;
  • mounted:家具送来了,能开始布置房间、挂海报啦~

💾 五、数据请求放在哪个阶段?

很多面试题都会问这个:

推荐:放在 created() 阶段!

因为:

  • created 时数据和 methods 已经可用了;
  • 如果放 mounted,有时页面可能"先渲染空壳",再加载内容,容易闪烁。

💡口诀:

"要拿数据,找 created;要操作页面,找 mounted。"


🔁 六、Vue 双向绑定是什么?

Vue 的核心特性之一就是「双向绑定」! 一句话解释:

当数据变 → 页面自动变 当用户输入 → 数据自动更新

🌰 生活例子: 在表单里输入名字 "小可爱", data 里的 name 也立刻变成 "小可爱"。 两边就像镜子一样同步!

xml 复制代码
<input v-model="name">
<p>{{ name }}</p>

输入框改了 → <p> 自动更新; 或者你手动改 JS 的 this.name → 输入框也同步。


🧩 七、双向绑定的原理

Vue 的魔法来自三个好朋友:

名字 职责 比喻
Model(数据层) 存数据 奶茶后厨原料区
View(视图层) 显示界面 前台菜单
ViewModel 桥梁,连接前后 小明传话员

🧠 当顾客在前台点"去冰":

  • View 改变 → 小明(ViewModel)通知后厨更新数据;
  • 后厨做完 → 小明再通知前台更新显示。

💬 Vue 就是靠这个机制自动帮我们做"来回同步"!


🔧 八、代码核心理解(简化版)

xml 复制代码
<input v-model="name">
<p>{{ name }}</p>

背后其实发生了两件事:

1️⃣ 当输入框内容变化时,触发 input 事件 → 更新 data.name。 2️⃣ 当 data.name 变化时,Vue 自动更新页面上 {{ name }}

这就是 "双向绑定" ------ 你动我也动 💞。


📚 九、重点小总结(背口诀!)

内容 口诀
生命周期阶段 先造人 → 挂家 → 装修 → 搬家
created 用法 拿数据用 created
mounted 用法 操作 DOM 用 mounted
双向绑定原理 数据动页面动,页面动数据动
ViewModel 比喻 小明传话员(View ↔ Model)

🍡 一、2.2 理解 ViewModel(奶茶店传话员)

在 Vue 里,ViewModel 是连接视图(View)和数据(Model)的桥梁。 你可以把整个系统想成这样:

层级 角色 比喻
Model 数据 奶茶店后厨(材料)
View 界面 顾客点单界面(菜单)
ViewModel 桥梁 小明传话员(负责同步前后信息)

🪄 ViewModel 的作用:

  1. 当数据变 → 视图自动变(比如后厨改了糖度,前台菜单显示也更新)
  2. 当用户操作视图 → 数据也自动更新(顾客在点单界面选了"去冰",data 里也改成"去冰")

这就是"双向绑定"的核心灵魂!


⚙️ 二、2.2.2 实现双向绑定的思路

Vue 的数据响应系统其实分为几个小步骤👇

🌟 步骤 1:new Vue()

当你写:

css 复制代码
new Vue({
  el: "#app",
  data: { name: "小可爱" }
});

Vue 会开始:

  • 初始化 data;
  • 执行响应式处理(observe);
  • 编译模板(compile);
  • 建立监听(watcher)。

🌟 步骤 2:核心流程图(奶茶店工作流 🧋)

看第 8 页的图可以这样理解👇

模块 作用 奶茶店比喻
Observer 监听所有原料变化 监控后厨材料有没有变(糖、冰、茶底)
Compile 扫描模板并绑定数据 菜单扫描员:把"{{name}}"替换成"顾客名字"
Watcher 监听具体数据变化 小明传话员:一旦数据变,马上通知前台
Dep(依赖收集器) 管理所有 Watcher 管理"订阅名单"的经理
Updater 负责界面更新 执行实际更新菜单的动作

整个流程就像这样运行:

复制代码
数据变动 → Observer 发现 → 通知 Dep → 触发 Watcher → 更新视图

反之,当视图变化(比如输入框改值),也会触发反向更新,做到双向绑定✨


🧩 三、2.2.3 实现代码讲解(小可爱友好版)

✨ Vue 主体类

kotlin 复制代码
class Vue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
​
    observe(this.$data);      // 监听 data 中的每个属性变化
    proxy(this);              // 代理,让 this.xxx 能访问 data.xxx
    new Compile(options.el, this); // 编译模板
  }
}

💬 翻译成人话:

  • observe():在 data 上安装监听器,就像在厨房每个锅上装温度计。
  • proxy():让你可以直接说 this.name,不用再写 this.$data.name
  • Compile():扫描页面模板,把 {{name}} 换成实际数据。

✨ 数据监听 observe()

javascript 复制代码
function observe(obj) {
  if (typeof obj !== "object" || obj === null) return;
  new Observer(obj);
}
​
class Observer {
  constructor(value) {
    this.value = value;
    this.walk(value);
  }
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

💬 通俗解释:

这个部分相当于------

"在厨房每个食材罐子(data 的 key)上绑一个传感器。"

  • observe(obj):判断是不是对象,是就进入监听。
  • Observer.walk():遍历每个属性,给它绑上 getter/setter。
  • defineReactive():核心的"响应式开关",拦截数据变化。

🧠 四、2.2.3.1 编译 Compile(模板扫描仪)

javascript 复制代码
class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) this.compile(this.$el);
  }
​
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      if (this.isElement(node)) {
        console.log("编译元素 " + node.nodeName);
      } else if (this.isInterpolation(node)) {
        console.log("编译插值文本 " + node.textContent);
      }
​
      // 若还有子节点,递归编译
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    });
  }
​
  isElement(node) {
    return node.nodeType == 1; // 元素节点
  }
​
  isInterpolation(node) {
    return node.nodeType == 3 && /{{(.*)}}/.test(node.textContent);
  }
}

💬 翻译:

  • Compile 会扫描整个 HTML;
  • 如果遇到 {{name}},就去 data 里拿 name
  • 如果遇到指令(比如 v-modelv-text),就帮它建立绑定关系;
  • 数据变了时,能自动更新 UI。

📦 类比: 这一步就像「菜单扫描仪」,会看菜单上的每一项(HTML 元素), 识别出哪部分需要从 data 拿数据、哪部分要响应变化。


🔗 五、2.2.3.2 依赖收集 Dep(订阅系统)

每一个数据 key 都可能在多个地方被用到,比如:

css 复制代码
<p>{{ name }}</p>
<h3>{{ name }}</h3>

那就需要:

  • name 这个 key 建立一个 "订阅名单";
  • 这份名单里放着所有要更新它的 Watcher;
  • 一旦数据变了,就由 Dep 通知所有 Watcher 更新。

📦 比喻:

Dep 就是"会员系统", 每个 Watcher 都是会员,订阅了自己关心的那杯奶茶(数据)。 当后厨改了配方,Dep 负责群发通知,所有 Watcher 自动更新。


🎁 六、总结成"奶茶店版 Vue 响应机制图"

阶段 对应代码模块 奶茶店场景
初始化 new Vue() 开店准备
监听数据 observe() + Observer 每个原料罐加传感器
模板解析 Compile 扫描菜单模板,建立绑定关系
依赖收集 Dep 订阅通知系统
监听变化 Watcher 小明传话员
更新视图 Updater 菜单刷新

💡 七、超简口诀(让你背下来!)

"Observer 看厨房,Compile 扫菜单, Watcher 小明跑通知,Dep 管会员团。 数据动了菜单变,Vue 魔法自动换!" 🌈

🧠 第 11-12 页:依赖收集(Dep & Watcher)篇

🧩 一、这是干嘛的?

Vue 里要实现「数据变 → 视图自动更新」,就得靠两个"好兄弟":

名称 职责 比喻
Dep(依赖管理器) 记录谁在用这份数据 订阅会员表 📋
Watcher(订阅者) 监听数据变化 订阅的顾客 👀

比如:

  • 你有个 {{name}} 在页面上显示;
  • 这时 Vue 会让这个 name 对应一个 Dep;
  • 同时为页面上的显示逻辑建一个 Watcher;
  • 以后 name 一旦变化,Dep 就通知 Watcher 更新!

💡 形象点: Dep 就像一个公众号; Watcher 就是订阅它的粉丝。 公众号发了新动态(数据变了)→ 所有粉丝自动收到更新。


🧮 二、实现思路(第 11 页)

在 Vue 中,这套机制大致分 4 步走👇:

1️⃣ 每个数据(key)都会创建一个 Dep 实例; 2️⃣ 模板编译时(Compile 阶段)创建一个对应的 Watcher; 3️⃣ 当读取这个 key 时(getter),把当前 Watcher 加入到 Dep; 4️⃣ 当数据更新(setter),Dep 会通知所有 Watcher 调用更新函数。


💻 三、Watcher 类(第 11 页)

kotlin 复制代码
class Watcher {
  constructor(vm, key, updater) {
    this.vm = vm;
    this.key = key;
    this.updaterFn = updater;
​
    Dep.target = this;  // 把当前 watcher 挂到 Dep 上
    vm[key];            // 触发 getter,完成依赖收集
    Dep.target = null;  // 收集完就清除
  }
​
  update() {
    this.updaterFn.call(this.vm, this.vm[this.key]);
  }
}

📘 用小可爱能懂的话讲:

  • 这个类专门盯着某个数据;
  • 它知道:当数据改了,就执行更新函数;
  • 相当于"菜单观察员":数据动了 → 菜单立刻重绘!

💻 四、Dep 类(第 12 页)

javascript 复制代码
class Dep {
  constructor() {
    this.deps = []; // 存放所有订阅它的 Watcher
  }
  addDep(dep) {
    this.deps.push(dep);
  }
  notify() {
    this.deps.forEach(dep => dep.update());
  }
}

📘 通俗解释:

  • Dep 就是"订阅名单";
  • addDep() 加入一个订阅者;
  • notify() 一键群发"更新消息";
  • 每个订阅者(Watcher)都执行自己的更新逻辑。

💻 五、defineReactive(第 12 页)

kotlin 复制代码
function defineReactive(obj, key, val) {
  this.observe(val);
  const dep = new Dep();
​
  Object.defineProperty(obj, key, {
    get() {
      Dep.target && dep.addDep(Dep.target); // 依赖收集
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      dep.notify(); // 通知所有 watcher 更新
    }
  });
}

📘 这段是关键核心:

  • 通过 Object.defineProperty() 拦截数据的读写;
  • get() 时收集依赖;
  • set() 时通知更新;
  • 这就是 Vue 2 响应式的"黑魔法"!🪄

📦 六、整段流程总结(口诀):

"Watcher 盯着看,Dep 把名单管, Getter 时登记,Setter 时广播。"

或者更生活化一点:

"粉丝订阅公众号 → 公号发动态 → 所有粉丝都知道啦~🎉"


💬 第 13-15 页:Vue 组件通信篇


🧩 3.1 组件通信的概念(第 13 页)

Vue 的每个组件就像一间"奶茶小分店"🧋 它们之间有时需要交流,比如:

  • 父组件(总部) → 子组件(分店)发菜单;
  • 子组件(分店) → 父组件(总部)汇报销售;
  • 两个兄弟组件(两家分店)之间分享原料;
  • 没直接关系的两个组件(比如隔壁街奶茶店)也能通信。

这就是组件间通信


🧩 3.3 组件通信的分类(第 13 页底部 & 第 14 页图)

类型 举例 通信方向
父子通信 父传子(props) / 子传父(emit) 垂直方向
兄弟通信 EventBus / Vuex 平级方向
隔代通信 Provide & Inject 跨层方向
全局通信 Vuex(全局状态管理) 任意方向

💡图上红箭头就是这些关系。


🧩 3.4 八种常见通信方式(第 14 页)

  1. props ------ 父传子
  2. $emit ------ 子传父
  3. ref ------ 父访问子实例
  4. EventBus ------ 兄弟传递消息
  5. parentroot ------ 访问父或根组件
  6. attrslisteners ------ 透传属性/事件
  7. provideinject ------ 跨层级传值
  8. Vuex ------ 全局共享状态

🍭 第 15 页重点:Props & Emit 实战

1️⃣ 父传子(props)

👨‍👧 场景:父组件要把数据传给子组件。

arduino 复制代码
// 子组件 Children.vue
props: {
  name: String,
  age: {
    type: Number,
    default: 18,
    require: true
  }
}

// 父组件 Father.vue
<Children name="jack" age="18" />

🧋 比喻: 总部(父组件)通过快递(props)把菜单参数发给分店(子组件)。


2️⃣ 子传父($emit)

👶 场景:子组件要告诉父组件一些事(比如点击事件)。

kotlin 复制代码
// 子组件 Children.vue
this.$emit("updateAge", 20)

// 父组件 Father.vue
<Children @updateAge="handleAgeChange" />

🧋 比喻: 分店(子组件)用对讲机 $emit 通知总部(父组件):"老板,客人点了加珍珠!"


🪄 终极总结表(第 11~15 页)

模块 作用 比喻 核心关键词
Dep & Watcher 响应式核心 公众号与粉丝 get 收集,set 通知
defineReactive 实现绑定 读写拦截器 getter / setter
组件通信 数据传递方式 奶茶店总部与分店沟通 props / emit
Vuex 全局状态仓库 奶茶总部共享库存 state / store
Provide & Inject 隔代传值 总部直接给孙子店传配方 跨层通信

🎶 背诵口诀:

Vue 响应靠 Dep 看,Watcher 更新视图忙; 父传子用 props 讲,子传父靠 emit 放。 兄弟通信 EventBus,全局数据 Vuex 扛。

🧋第16页:ref & EventBus


🧩 3.4.3 ref ------ 父组件直接拿子组件的"实例"

csharp 复制代码
// Father.vue
<Children ref="foo" />

// JS
this.$refs.foo // 获取子组件实例

📘解释:

  • 给子组件加个 ref="foo" 标识。
  • 父组件通过 this.$refs.foo 直接拿到子组件实例。
  • 然后就能操作子组件的数据或方法,比如 this.$refs.foo.someMethod()

🍰生活比喻: 就像奶茶店老板(父组件)在每个分店门口贴了标签:ref="分店A"。 这样他就能直接喊:"分店A,今天销量多少?" ------ 这就是通过"门牌号(ref)"直接找子组件。


🧩 3.4.4 EventBus ------ 兄弟组件通信神器 🚍

当两个兄弟组件要交流,而又没有直接父子关系时,可以搞一个"事件总线(EventBus) ":

ini 复制代码
// Bus.js
class Bus {
  constructor() {
    this.callbacks = {};
  }
  $on(name, fn) {
    this.callbacks[name] = this.callbacks[name] || [];
    this.callbacks[name].push(fn);
  }
  $emit(name, args) {
    if (this.callbacks[name]) {
      this.callbacks[name].forEach(cb => cb(args));
    }
  }
}

// main.js
Vue.prototype.$bus = new Bus();

👦子组件1:

bash 复制代码
this.$bus.$emit('foo', 'hi~');

👧子组件2:

kotlin 复制代码
this.$bus.$on('foo', this.handle);

📘解释:

  • $emit 发消息(广播 📢)
  • $on 收消息(监听 👂) 这样兄弟组件之间就能互相通信啦!

🍵生活比喻: 这就像奶茶店有个"广播中心(Bus)":

  • 店员A拿起麦克风喊:"珍珠卖完啦~"
  • 店员B听到广播:"好嘞,我这边暂停加料!"

🧋第17页:$parent & $root

kotlin 复制代码
// 兄弟组件1
this.$parent.on('add', this.add);

// 兄弟组件2
this.$parent.emit('add');

📘解释:

  • $parent:访问父组件实例
  • $root:访问根组件实例(App.vue) 。 可以通过它们让兄弟组件"间接交流"。

🍰生活比喻: A店和B店不直接联系,而是都通过"总部经理($parent)"来传话。 A → 总部 → B。


🧋第18页:attrslistenersprovideinject


🧩 3.4.6 $attrs$listeners

xml 复制代码
<!-- parent -->
<HelloWorld foo="foo" />

<!-- child -->
<p>{{ $attrs.foo }}</p>

📘解释:

  • $attrs 会收集父组件传来的但子组件没在 props 声明的属性
  • $listeners 收集所有传来的事件监听器
  • 常用于多层组件传递时------属性透传给孙子组件。

🍵生活比喻: 爷爷(父组件)寄了个包裹 foo。 爸爸(子组件)没拆,直接帮转寄给孙子(v-bind="$attrs")。 ------ 一种"中转快递"。


🧩 3.4.7 provide & inject(隔代通信)

javascript 复制代码
// 祖先组件
provide() {
  return { foo: 'foo' }
}

// 孙组件
inject: ['foo']

📘解释:

  • provide:像"广播发射器",定义要共享的数据。
  • inject:像"接收天线",哪怕隔了几层组件,也能拿到祖先提供的数据。

🍰生活比喻: 爷爷(App.vue)直接把秘方(foo)传给孙子(Grandson.vue), 中间的爸爸(Father.vue)不用管~ 就像家族传承:跳过一代直接传秘诀。


🧋第19页:Vuex(全局状态管理)


🧩 3.4.8 Vuex 是什么?

📘Vuex 相当于一个「全局共享仓库」: 所有组件都可以存取其中的数据。 适合复杂项目中多个组件共享状态的情况。

🍵生活比喻: 你可以把 Vuex 想成奶茶总部仓库:

  • 各分店(组件)都能去取/改库存;
  • 不用自己各存一份。

💻核心概念:

名称 作用 比喻
state 存放共享数据 奶茶库存
getters 派生状态(计算属性) 库存汇总报表
mutations 修改 state 的方法(同步) 员工写"出入库登记"
actions 异步修改(含接口请求) 经理打电话调货

🧋第20页:总结 + 高频问答


🧩 3.5 小结

场景 推荐通信方式
父 → 子 props / ref
子 → 父 $emit
兄弟组件 EventBus / $parent
隔代组件 provide / inject / attrs
全局共享 Vuex

🍵奶茶总类比总结:

场景 奶茶店类比
props 总部发菜单给分店
emit 分店向总部汇报订单
ref 老板直接问分店
EventBus 广播系统
provide/inject 家族秘方传承
Vuex 总部中央仓库

🧩 4. 为什么 data 是函数而不是对象?

Vue 组件的 data 必须是函数:

javascript 复制代码
export default {
  data() {
    return { count: 0 }
  }
}

📘原因解释:

  • 每个组件都是"复用"的。
  • 如果 data 是对象,所有组件会共用一份数据!😱
  • 用函数返回新对象,就能保证每个组件实例有独立数据

🍰生活比喻: 如果每个奶茶店都共用一个冰箱(对象), 那A店加了珍珠,B店的冰箱也变甜了!😵 所以要用"函数"来创建独立冰箱(返回新对象)。


🧠 终极口诀总结(第16~20页)

ref找子组件,Bus兄弟传, parent root搭桥连。 attrs props透传远, provide inject祖孙谈。 Vuex全局共享馆, data函数独冰箱~🍨

🧋第21页:data 对象 vs 函数的区别


🌟 代码演示

javascript 复制代码
const app = new Vue({
  el: "#app",
  // 对象形式
  data: {
    foo: "foo"
  }

  // 函数形式
  data() {
    return {
      foo: "foo"
    }
  }
})

📘解释:

  • Vue 根实例(new Vue())可以用对象形式。
  • 组件中必须用函数形式。

🚨 如果你在组件里这样写:

php 复制代码
Vue.component('component1', {
  template: `<div>组件</div>`,
  data: {
    foo: "foo"
  }
})

Vue 会报错:

sql 复制代码
[Vue warn]: The "data" option should be a function 
that returns a per-instance value in component definitions.

💡 提示: 组件的 data 必须是函数返回一个新对象(per-instance = 每个实例独立)。


🍰生活比喻:

想象你开了 5 家奶茶分店(组件复用)。 如果大家公用一个"库存对象"(data 是对象), 那 A 店加了珍珠,B 店的库存也少了 😵‍💫。

所以 Vue 让每个店(组件实例)都要用函数新建自己的库存(data 函数返回新对象)。

一句话口诀:

"每个奶茶店有自己冰箱,不共用食材!"


🧋第22页:通过实例对比看区别


❌ 错误写法(对象形式)

javascript 复制代码
function Component() {}
Component.prototype.data = { count: 0 }

const componentA = new Component()
const componentB = new Component()

componentA.data.count = 1

console.log(componentB.data.count) // 输出 1!

📘解释: 两个实例共享同一个 data 对象(同一块内存), A 改了 count,B 也被污染。


✅ 正确写法(函数形式)

javascript 复制代码
function Component() {
  this.data = this.data()
}
Component.prototype.data = function () {
  return { count: 0 }
}

const componentA = new Component()
const componentB = new Component()

componentA.data.count = 1
console.log(componentB.data.count) // 0

📘解释: 函数返回新的对象 → 每个实例都有独立副本


🍵比喻:

  • 第一种写法像"共享冰箱",任何一家动了都会影响别人。
  • 第二种写法像"每家店开业都配一台新冰箱",互不影响。

🧋第23页:源码角度分析 Vue 初始化流程


📘 initData 函数(来自 /vue-dev/src/core/instance/state.js

kotlin 复制代码
function initData(vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
}

💬解释:

  • 判断 data 是否是函数;
  • 如果是,就执行函数 getData() 得到一个新对象;
  • 否则直接使用对象(root 实例允许这样)。

🍵比喻:

"每家店开张时检查冰箱:

  • 如果是函数,就新买一个冰箱;
  • 如果是对象,就用总部的公共仓库(root用)。"

💻 mergeOptions 合并逻辑(/core/util/options.js

当 Vue 创建组件时会合并配置:

ini 复制代码
Vue.prototype._init = function (options) {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

📘解释: 在这里 Vue 检查组件 data 是不是函数; 如果不是函数,会警告 ⚠️。


🧋第24页:底层验证逻辑(源码追踪)


📘 _init 初始化逻辑(Vue 核心入口)

scss 复制代码
Vue.prototype._init = function (options) {
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
}

这段是 Vue 初始化时的分支逻辑:

  • 如果是内部组件,走 initInternalComponent()
  • 否则正常合并选项(会触发 data 检查)。

📘关键点:

Vue 在这里会对 data 做类型检测。 如果 data 不是函数,就会弹出警告信息。

源码中警告逻辑是:

erlang 复制代码
if (childVal && typeof childVal !== "function") {
  warn("The data option should be a function ...")
}

🍵小可爱理解法:

Vue 在初始化时像个面试官 👩‍💻:

"你好,请问你的 data 是函数吗? 不是?那你就不合格,我要提醒你一声~🚨"


🧋第25页:结论 & 思考延伸


💡 4.4 结论

场景 data 形式 原因
根实例(new Vue) 可以是对象 只有一个实例,不会污染
组件定义 必须是函数 防止多个组件实例共享同一对象

Vue 初始化时会把函数形式的 data 执行为工厂函数, 保证每次 new 一个组件 → 都是全新的 data


🍰生活比喻总结:

场景 类比
根实例 data 是对象 总部仓库(只有一个)
组件 data 是函数 每家分店独立仓库
若共用对象 库存串味、互相污染
函数返回对象 各店独立,互不影响

🧩 延伸问题(5️⃣ 动态添加 data 属性)

问:如果运行时再给 data 添加新属性,会发生什么?

答:Vue2 的响应式系统基于 Object.defineProperty , 新增的属性不会自动变成响应式。 需要用:

kotlin 复制代码
this.$set(this.obj, 'newKey', value)

🍵比喻:

"Vue 在开店时只给冰箱登记了现有的食材。 你后来突然塞进新的珍珠,Vue 不知道那是库存, 所以要手动打个登记卡($set)告诉它!"


🎯终极总结口诀(第21~25页)

Vue 实例可用对象写, 组件必须函数返。 防共享、防串味, 每家冰箱独开线。

新增属性没监听, $set 通知 Vue 备案~🧋

🧋第 26 页:为什么直接给 data 加新属性会"没反应"


🌰 示例代码

javascript 复制代码
<p v-for="(value, key) in item" :key="key">
  {{ value }}
</p>
<button @click="addProperty">动态添加新属性</button>
const app = new Vue({
  el: "#app",
  data: () => ({
    item: { oldProperty: "旧属性" }
  }),
  methods: {
    addProperty() {
      this.item.newProperty = "新属性"
      console.log(this.item)
    }
  }
})

😵 结果:

控制台能看到 newProperty,但页面没更新!


💡 原因:

Vue2 的响应式系统是通过 Object.defineProperty() 实现的。 而这个方法只会在初始化时对已有属性加上"监听器"。 你后来添加的新属性 → 没人监听它 😴。


🍰 生活比喻:

你开了家"智能奶茶店", 一开始系统记录了"珍珠"和"椰果"的库存。 但你后来偷偷多加了"布丁", 系统没监听到,所以屏幕上库存不更新。🤣


🧋第 27 页:原理分析(为什么没响应)


📘 Vue 响应式底层实现

javascript 复制代码
const obj = {}
Object.defineProperty(obj, 'foo', {
  get() {
    console.log(`get foo: ${val}`)
    return val
  },
  set(newVal) {
    console.log(`set foo: ${newVal}`)
    val = newVal
  }
})
  • 当我们访问 obj.foo → 触发 get
  • 修改 obj.foo → 触发 set

🍵但如果你后来:

ini 复制代码
obj.bar = "新属性"

🚫 就没触发任何监听!


📘 原因总结:

初始化时,Vue 只给已有的属性加了"getter/setter 监听器"; 你后来添加的属性没注册监听,所以 Vue 根本不知道它变了。


🍰 类比:

你在开店时只登记了"珍珠、椰果"。 后来突然多进了"布丁", 系统(Vue)没给它打标签,当然不会更新库存界面。😅


🧋第 28 页:解决方案 3 种 ✅


Vue 官方给了三种方案:

🧠 1️⃣ Vue.set()

语法:

bash 复制代码
Vue.set(target, propertyName/index, value)

比如:

kotlin 复制代码
Vue.set(this.items, 'newProperty', '新属性')

✔ Vue 会在内部调用 defineReactive(), 自动给 newProperty 加上"监听器"。


📘 Vue.set 源码(精简版):

kotlin 复制代码
function set(target, key, val) {
  defineReactive(target, key, val)
  target.dep.notify()
  return val
}

相当于:

"帮你在布丁上贴上库存标签,再通知系统刷新。"


🍋 2️⃣ Object.assign()

kotlin 复制代码
this.someObject = Object.assign({}, this.someObject, { newProp: 1 })

👉 创建了一个新对象(带新属性),然后替换原对象。 Vue 会重新检测这个对象(因为引用变了)。

🍰 比喻:

"直接把旧冰箱换掉,换一台包含布丁的新冰箱~!"


💥 3️⃣ $forceUpdate()

kotlin 复制代码
this.$forceUpdate()

🚨 强制刷新页面,但只是临时补救(不推荐)。 说明你逻辑写错了,要查问题。

🍰 比喻:

"老板发现库存不对,直接重启系统刷新数据 💻⚡。"


🧋第 29 页:源码延伸 + 小结


📘 defineReactive(核心响应式原理)

javascript 复制代码
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`)
        val = newVal
      }
    }
  })
}

Vue.set 内部其实就是调用它来"给新属性装监听器"。


💡 小结表格:

需求 推荐方案 说明
添加少量属性 Vue.set() 最直接、官方推荐
添加大量属性 Object.assign() 重新创建对象
实在搞不定 ⚠️ $forceUpdate() 临时刷新(不推荐)

🍰 比喻总结:

操作 奶茶店比喻
Vue.set 给"布丁"打上库存标签 ✅
Object.assign 换一台新冰箱 ✅
$forceUpdate 老板暴力刷新系统 ❌

🧋第 30 页:v-if 与 v-for 的优先级


🧠 6.1 作用

  • v-if:控制是否渲染(有条件地显示某块内容)
  • v-for:循环渲染列表

例子:

ini 复制代码
<Modal v-if="isShow" />
<li v-for="item in items" :key="item.id">{{ item.label }}</li>

🍰 类比:

"v-if 是决定要不要开店, v-for 是决定开几家分店。"


🧩 6.2 优先级

在 Vue 模板编译时, v-for 的优先级比 v-if 更高

也就是说:

Vue 会先循环(生成多个 item), 然后再对每个 item 判断 v-if 是否成立。


🚫 错误用法示例

ini 复制代码
<li v-for="item in items" v-if="item.show">{{ item.name }}</li>

这会让 Vue 每次循环都判断一次 v-if,性能浪费。

✅ 推荐写法:

css 复制代码
<li v-for="item in items.filter(i => i.show)" :key="item.id">
  {{ item.name }}
</li>

🍵 比喻:

"先挑出要营业的分店(filter), 再挨个装修(for)。"


🎀终极可爱总结(第 26--30 页)

知识点 通俗记忆口诀
data 动态加属性没反应 因为 Vue 没监听"新布丁"
Vue.set 给布丁贴监听标签 ✅
Object.assign 换一台带布丁的新冰箱 ✅
$forceUpdate 暴力刷新系统 ❌
v-if vs v-for "先开分店再判断营业" → v-for 优先

🧋第 31 页:v-if 与 v-for 同时使用的例子


🌰 代码示例

javascript 复制代码
<div id="app">
  <p v-if="isShow" v-for="item in items">
    {{ item.title }}
  </p>
</div>
const app = new Vue({
  el: "#app",
  data() {
    return {
      isShow: true,
      items: [{ title: "foo" }, { title: "baz" }]
    }
  },
  computed: {
    isShow() {
      return this.items && this.items.length > 0
    }
  }
})

🧠 讲解:

这段代码同时用 v-ifv-for。 Vue 内部编译时,会先"for 循环",再"if 判断"。


🍵生活比喻:

"v-for 就像店长要点清所有分店(循环列表); v-if 是看某家店今天开没开(条件渲染)。"

所以顺序是: 👉 先数一遍店(for), 👉 再判断要不要营业(if)。


🧩 Vue 编译 render 函数生成逻辑

javascript 复制代码
f anonymous() {
  with(this) {
    return _c('div',{attrs:{"id":"app"}},
      _l((items), function (item) {
        return (isShow)
          ? _c('p', [_v(item.title)]) : _e()
      })
    )
  }
}

🔍 _l 是 Vue 内部的循环函数, 可以看到它在循环里才执行 if 判断。 这说明 👉 v-for 优先级高于 v-if


🧋第 32 页:放不同标签的情况


ini 复制代码
<div id="app">
  <template v-if="isShow">
    <p v-for="item in items">{{ item.title }}</p>
  </template>
</div>

这时候,v-if 放在外层 template 上, Vue 编译成的 render 函数就会先判断再循环 👇

javascript 复制代码
f anonymous() {
  with(this) {
    return _c('div',{attrs:{"id":"app"}},
      [(isShow)
        ? _l((items), function(item){return _c('p',[_v(item.title)])})
        : _e()]
    )
  }
}

🧠说明:

  • v-if 在外层 → 优先判断条件
  • v-ifv-for 在同一标签 → 先循环后判断

🍰比喻:

"如果老板先看'今天开店不?(if)',再派人去清点分店(for)",效率更高。 所以推荐:把 v-if 包在外层 template 上


🧋第 33 页:Vue 源码验证优先级


Vue 源码在 /src/compiler/codegen/index.js 中:

scss 复制代码
export function genElement(el, state) {
  ...
  else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  }
}

📘 说明: Vue 在编译时,会先判断 el.for,再判断 el.if , 也就是 ------ v-for 优先级更高 ✅。


💡 6.3 注意事项

1️⃣ 不要同时用在一个元素上! 每个循环都会先判断 if,性能浪费。

2️⃣ 推荐写法:

ini 复制代码
<template v-if="isShow">
  <p v-for="item in items">{{ item.title }}</p>
</template>

3️⃣ 循环里再过滤条件项:

javascript 复制代码
computed: {
  items() {
    return this.list.filter(item => item.isShow)
  }
}

🍰 类比:

"先过滤出要开的分店(computed), 再去巡店(v-for)。"


🧋第 34 页:v-show 与 v-if 的区别与场景


🌸 共同点

ini 复制代码
<Model v-show="isShow" />
<Model v-if="isShow" />

两者都能控制显示/隐藏。 当表达式为 true 时显示,为 false 时隐藏。

🍵 类比:

"v-if 是临时开关电源的插座🔌; v-show 是拉上/放下窗帘的遮光布🌇。"


⚙️ 不同点总结表(非常重要):

对比项 v-if v-show
本质 条件渲染 CSS 控制显示
操作方式 元素创建/销毁 元素始终存在,只是 display:none
性能 频繁切换性能差(重新挂载) 初始化开销大,切换性能好
使用场景 不常切换 频繁显示隐藏

🧋第 35 页:源码与原理深挖


⚙️ 控制方式

  • v-show → 给元素加上 display:none
  • v-if → 直接在 DOM 中创建或销毁节点

⚙️ 编译条件不同

  • v-if 需要完整的挂载 / 卸载过程;
  • v-show 只是简单地控制 CSS。

🧩 生命周期区别

生命周期 v-if v-show
初始隐藏 → 显示 重新触发 created、mounted 等钩子 不会重新触发生命周期
性能 高消耗(创建销毁) 轻量(仅样式切换)

🍰 类比:

操作 比喻
v-if "拆掉奶茶店再重建 🏗️"
v-show "拉开或拉上门帘 🚪"

💡 总结口诀(第 31~35 页)

🧠 v-for 先循环,v-if 再判断; 🚫 同时用,性能慢。 🪄 v-if 创建删 DOM, 🎭 v-show 拉门帘。 不常切换选 if,频繁切换用 show。


🌟【一图脑记】

关键点 口诀
v-if vs v-for for 先 if 后
不要同用 用 template 包外层
v-show vs v-if show 控 display,if 控 DOM
使用场景 show 频繁切换,if 偶尔显示

🧋第 36 页:v-show 与 v-if 的源码与原理


💻 v-show 源码实现(Vue3 示例)

javascript 复制代码
export const vShow = {
  beforeMount(el, { value }) {
    el._vod = el.style.display === 'none' ? '' : el.style.display
    setDisplay(el, value)
  },
  updated(el, { value, oldValue }) {
    if (value !== oldValue) setDisplay(el, value)
  },
  beforeUnmount(el, { value }) {
    setDisplay(el, value)
  }
}

📘解释:

  • v-show 的本质就是------控制元素的 CSS display
  • value=false 时,加上 display:none
  • value=true 时,重新显示。

🍰类比生活:

"v-show 像是奶茶店的帘子:拉上帘子(隐藏),拉开帘子(显示)。 店面还在,只是顾客看不见。"

所以:

  • 显示隐藏切换很快(性能好)
  • 但初始化就要创建所有元素(首屏成本高)

💡 v-if 源码(简化理解)

kotlin 复制代码
return (isShow) ? createVNode('div') : null
  • v-if 是真的 创建 / 销毁 DOM 节点
  • 当条件为 false 时,该节点直接被移除。

🍵比喻:

"v-if 像是拆掉奶茶店重建的老板: 不开门 → 拆掉整间店;要开门 → 重新建一家。"


🧋第 37 页:v-if 源码实现细节


scss 复制代码
export const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  (node, dir, context) => {
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      if (isRoot) {
        ifNode.codegenNode = createCodegenNodeForBranch(branch, key, context)
      } else {
        const parentCondition = getParentCondition(ifNode.codegenNode)
        parentCondition.alternate = createCodegenNodeForBranch(branch)
      }
    })
  }
)

📘解释:

  • Vue 在编译阶段处理 v-ifv-elsev-else-if
  • 最终生成渲染逻辑:哪个条件成立,就渲染对应分支;
  • 没有成立的分支就不生成任何 DOM。

🍰小比喻:

"v-if 是老板下命令:

  • 如果今天下雨 → 关店。
  • 如果晴天 → 开店卖奶茶。
  • 如果大风 → 改卖热饮 ☕。"

💡 v-show 与 v-if 使用场景总结

对比点 v-if v-show
本质 DOM 创建/销毁 控制 CSS
首次渲染开销
切换频率 低(偶尔切换) 高(频繁切换)
生命周期触发 会重新执行 不会重新触发

🍰口诀记忆:

"频繁切换选 v-show, 偶尔显示用 v-if 喽~" 😆


🧋第 38 页:key 是什么?为什么要加 key?


💡 8.1 Key 是什么?

一句话解释:

key 是每个虚拟 DOM(VNode)的唯一身份证 🪪。

ini 复制代码
<li v-for="item in items" :key="item.id">...</li>

📘作用:

  • 帮助 Vue 在虚拟 DOM diff 时更快找到对应节点;
  • 减少不必要的重新渲染;
  • 提高性能。

🍰生活比喻:

"key 就像每杯奶茶的订单号 🔢。 没 key 时,Vue 只能'大海捞针'地对比内容; 有 key 时,Vue 直接看订单号匹配,立刻定位那杯奶茶。"


💡 用 new Date() 生成 key 的特殊用法

ini 复制代码
<Comp :key="+new Date()" />

意思是:

每次都会生成新的 key(新的身份证) Vue 认为是一个"新元素",会强制重新渲染

🍵比喻:

"换了一杯新奶茶(新编号),老那杯直接倒掉重做。"


🧋第 39 页:设置 key 与不设置 key 的区别


🌰 例子:

xml 复制代码
<div id="demo">
  <p v-for="item in items" :key="item">{{item}}</p>
</div>

<script>
const app = new Vue({
  el: '#demo',
  data: { items: ['a','b','c','d','e'] },
  mounted() {
    setTimeout(() => {
      this.items.splice(2, 0, 'f') // 插入f
    }, 2000)
  }
})
</script>

🧠 分析:

✅ 没有 key 的情况:

Vue 会尽量"复用"现有 DOM 节点, 使用"就地复用"策略,导致:

  • A、B 不变;
  • C 的位置被替换成 F;
  • 之后的 D、E 被错位更新。

🍵比喻:

"没给奶茶编号,员工靠杯子顺序配单。 结果你中途插队加了个新订单,全乱套了~😅"


✅ 有 key 的情况:

Vue 会精准识别每个元素:

  • 新的 F 会在 C 前插入;
  • 其他元素位置保持不变;
  • 减少不必要的重绘。

🍰比喻:

"给每杯奶茶编号后(key), 员工就能一眼定位:'哦,F 是新单,插在 C 前。'"


🧋第 40 页:key 的 Diff 效率分析


📊 Diff 对比示意图(白圈 = 新节点,黑圈 = 旧节点)

Vue 会遍历新旧节点:

  • 通过 key 来判断元素是否相同;
  • 相同 → patch(复用节点);
  • 不同 → remove + create(销毁重建)。

💡 使用 key 的好处:

  • 避免错误复用节点;
  • 大大减少 DOM 操作;
  • 提高 diff 性能。

🍵记忆口诀:

"key 是 Vue 的眼睛 👀, 没 key 就乱认人~"


❗但注意:设置 key ≠ 一定更快!

文档原话:

如果只是静态列表,不加 key 反而略快。 因为 Vue 无需比对 key,只是顺序复用。

🍰类比:

"如果队伍从不变位置(静态列表), 每次都检查身份证反而浪费时间~😅"


🧠 终极总结(第 36~40 页)

知识点 通俗记忆
v-show 拉门帘,显示隐藏切换快
v-if 拆房子重建,性能贵但干净
key 是啥 虚拟 DOM 的身份证
有 key 的好处 定位准,减少误操作
没 key 的问题 可能错位更新
new Date() key 强制重建组件(刷新)
何时用 key v-for 渲染动态列表时必须用
何时不用 key 静态结构简单时可省略

🧋一句口诀背完所有:

"频繁切换用 show,偶尔渲染选 if; 动态列表加 key,Vue 不乱配单子~🍵"

🧠 第 41 页:key 的底层原理分析


💡源码位置

css 复制代码
core/vdom/patch.js
function sameVnode(a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data)
  )
}

📘解释:

  • Vue 在 diff 算法 中,会先判断两个虚拟节点(VNode)是否是同一个。

  • "同一个"的标准主要是:

    1. key 相同;
    2. 标签 tag 相同;
    3. 都是注释节点;
    4. 数据结构一致。

👉 如果满足这些条件,Vue 就会复用旧节点,而不是重新创建 DOM。


🍰生活比喻:

"Vue 就像奶茶店员工配单:

  • 如果订单号(key)一样、杯型(tag)一样、口味数据也一样, 那就直接拿来复用旧的奶茶,不用重做。
  • 如果 key 不一样,那就是新顾客,得重新做一杯。"

🧠 第 42 页:updateChildren 的 diff 核心逻辑


scss 复制代码
function updateChildren(parentElm, oldCh, newCh) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode)
    } else {
      // 如果 key 不一样或找不到匹配节点
      createElm(newStartVnode)
    }
    newStartVnode = newCh[++newStartIdx]
  }
}

📘解释:

  • Vue 会循环对比"旧节点数组"和"新节点数组";
  • 若两个节点 key 一样 → 执行 patch(复用更新);
  • 若不同 → 创建新的 DOM 元素。

🍵小比喻:

"Vue 就像'奶茶复用机':

  • 找到同样编号的杯子 → 换内容即可;
  • 找不到编号 → 新做一杯放上去;
  • 找到旧的但不再用 → 扔掉(destroyed)。"

💡所以 key 的作用就是 "给 diff 算法一个锚点" ,让 Vue 不至于乱套。


🧋第 43 页:Mixin 是什么?(进入新知识点)


💡Mixin 的定义

Mixin 是"混入"的意思, 它让我们在多个组件中共享相同逻辑,而不用重复写代码。

比如定义一个小 mixin:

javascript 复制代码
var myMixin = {
  created() {
    console.log('mixin created!')
  },
  methods: {
    sayHi() {
      console.log('Hello from mixin!')
    }
  }
}

然后在组件中引入:

javascript 复制代码
Vue.component('MyComp', {
  mixins: [myMixin],
  created() {
    console.log('component created!')
  }
})

📘解释:

  • 当组件加载时,Vue 会先执行 mixin 里的 created 钩子
  • 再执行组件自己的 created;
  • methods、data、computed 等也会被合并。

🍰比喻:

"Mixin 就像奶茶店的'通用调料包'。 每个新店(组件)都能用同一包基础原料(逻辑), 不用每次都手写:'加糖、加冰、搅拌三下'。"


🧋第 44 页:局部混入 vs 全局混入


🧩 局部混入

javascript 复制代码
var myMixin = {
  created() { this.hello() },
  methods: {
    hello() { console.log('hello from mixin!') }
  }
}
​
Vue.component('componentA', {
  mixins: [myMixin]
})

🧠运行结果:

  • 当组件 componentA 被创建时:

    • Vue 自动调用 mixin 里的 created
    • 然后执行 hello()
    • 控制台输出 "hello from mixin!"

🍵生活比喻:

"就像奶茶分店有自己独立的调料台(局部 mixin), 每家店用同一份配方,但互不影响。"


🧩 全局混入

javascript 复制代码
Vue.mixin({
  created() {
    console.log("全局混入")
  }
})

📘解释:

  • 所有组件(包括第三方库)都会被这个混入影响;
  • 所以要谨慎使用,常用于插件、全局日志等。

🍰比喻:

"全局 mixin 就像公司强制要求所有奶茶都加一片薄荷叶🌿。 每家分店的奶茶都被改了味。"


🧋第 45 页:Mixin 使用注意事项 + 场景


⚠️ 注意事项

当组件与 mixin 都定义了相同的生命周期钩子,比如 created

  • Vue 会把它们合并为数组;
  • 先执行 mixin 的钩子,再执行组件自己的钩子。

但如果是普通属性冲突,比如 data、methods,同名时:

组件里的属性会覆盖 mixin 的。


💡使用场景

举例:你有两个组件都用到"显示/隐藏"的逻辑👇

javascript 复制代码
const Modal = {
  data() { return { isShowing: false } },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing
    }
  }
}

const Tooltip = {
  data() { return { isShowing: false } },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing
    }
  }
}

这俩重复代码多得像"两个奶茶师傅都在写同一份配方"😅 👉 所以我们把这段逻辑抽出来放进一个 mixin:

javascript 复制代码
const showMixin = {
  data() { return { isShowing: false } },
  methods: {
    toggleShow() { this.isShowing = !this.isShowing }
  }
}

然后两个组件都 mixins: [showMixin] ✨逻辑统一维护、代码复用度高。


🍰比喻总结:

概念 比喻 特点
局部 mixin 分店独立调料包 只影响当前组件
全局 mixin 总部统一配方 所有组件都被影响
生命周期冲突 先 mixin 后组件 不同钩子按顺序执行
使用场景 复用通用逻辑 避免重复写同样代码

🧠 最后一句口诀(背完就会):

"Mixin 就是逻辑打包糖, 局部小甜,全局慎放; 冲突先混后自家, 重复逻辑它帮忙~🍯"

🧋第 46 页:复用逻辑的 Mixin 实战例子


🍵 观察 Tooltip 组件

javascript 复制代码
const Tooltip = {
  template: '#tooltip',
  data() {
    return { isShowing: false }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing
    }
  }
}

📘说明: 这个组件内部有一个布尔值 isShowing 控制显示与隐藏,并有个方法 toggleShow() 来切换状态。


🍰生活比喻:

"Tooltip 就像一个小气泡提示框 💬。 isShowing 是'气泡开关'(显示或隐藏),toggleShow 就是'点击按钮弹出提示'。"


💡问题来了:

我们之前的 Modal 弹窗组件也写了一模一样的逻辑 ! 于是------代码重复警告⚠️!

所以我们可以抽取公共逻辑👇


🍰 提取出 Mixin

javascript 复制代码
const toggle = {
  data() {
    return { isShowing: false }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing
    }
  }
}

两个组件都引入它:

arduino 复制代码
const Modal = { template: '#modal', mixins: [toggle] }
const Tooltip = { template: '#tooltip', mixins: [toggle] }

📘说明:

  • toggle 就是 mixin;
  • mixins: [toggle] 表示组件注入该逻辑;
  • 每个组件都自动拥有 isShowingtoggleShow()

🍵生活比喻:

"就像所有门店都用'同一份标准开店手册'📖。 不用重复教:'怎么开灯、怎么关门', mixin 里已经写好了统一流程。"


🧋第 47 页:Mixin 源码入口分析


💻 Vue.mixin 的源码

javascript 复制代码
export function initMixin(Vue) {
  Vue.mixin = function (mixin) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

📘解释:

  • Vue 在内部给自己挂了一个静态方法 Vue.mixin()
  • 作用:把传入的 mixin 合并 到 Vue 的配置选项里;
  • 核心函数:mergeOptions()(在下一页讲)。

🍰生活比喻:

"Vue.mixin 就像是'公司总部文件柜 📂': 你交给它一个 mixin 配方,它会自动把内容合并到总配置中, 这样所有分店(组件)都能共享这套规则。"


🧋第 48 页:mergeOptions 核心逻辑


💻 核心代码(节选)

scss 复制代码
export function mergeOptions(parent, child) {
  if (child.mixins) {
    for (let i = 0; i < child.mixins.length; i++) {
      parent = mergeOptions(parent, child.mixins[i])
    }
  }

  const options = {}
  for (let key in parent) {
    mergeField(key)
  }
  for (let key in child) {
    if (!hasOwn(parent, key)) mergeField(key)
  }

  function mergeField(key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key])
  }

  return options
}

📘拆解说明: 1️⃣ 优先递归处理 mixins (嵌套的也会被逐层展开) 2️⃣ 先合并 parent(父配置)里的 key 3️⃣ 再补充 child(子配置)中的 key 4️⃣ 通过 mergeField 根据策略合并


🍵生活比喻:

"总部(parent)有一份配方,分店(child)带着新配方来加盟。 Vue 会:

  1. 先融合所有旧配方(递归 mixins);
  2. 逐项比对原料(key);
  3. 如果总部有就更新,没有就新增;
  4. 最终生成一份最新的'统一菜单'(options)。"

🧋第 49 页:几种选项的合并策略


💡 Vue 中有四种合并策略

类型 举例 合并方式
替换型 props、methods、inject、computed 后者覆盖前者
合并型 data 属性递归合并
队列型 生命周期钩子(如 created、mounted) 合并为数组,依次执行
叠加型 watch、components、directives 追加合并,不覆盖

💻 替换型源码示例

javascript 复制代码
strats.computed = function (parentVal, childVal) {
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  extend(ret, childVal)
  return ret
}

📘解释:

  • 如果父级没有,就直接用子级;
  • 如果都有,则用子级的同名项覆盖父级。

🍰比喻:

"总部有一个'调糖比例表', 分店自己改了一个'新甜度表', 那最终菜单以分店的为准~🍯。"


🧋第 50 页:合并型(以 data 为例)


💻 核心代码

javascript 复制代码
strats.data = function (parentVal, childVal) {
  return mergeDataOrFn(parentVal, childVal)
}
​
function mergeDataOrFn(parentVal, childVal) {
  return function mergedDataFn() {
    var parentData = parentVal.call(this)
    var childData = childVal.call(this)
    return mergeData(childData, parentData)
  }
}

📘解释:

  • 对于 data,Vue 不会简单替换;
  • 它会执行父、子 data() 两个函数并返回的对象;
  • 然后通过 mergeData() 把属性逐一合并;
  • 同名属性后者覆盖,未定义的则新增。

🍵生活比喻:

"总部配方里写着:糖=3、冰=1; 分店配方写:糖=2、加奶盖=1。 合并后得到:糖=2、冰=1、加奶盖=1。 这样既保留原配方,又能加新口味。"


💻 mergeData 函数

vbnet 复制代码
function mergeData(to, from) {
  for (let key in from) {
    if (!hasOwn(to, key)) {
      set(to, key, from[key])
    } else if (typeof to[key] === 'object' && typeof from[key] === 'object') {
      mergeData(to[key], from[key])
    }
  }
}

📘解释:

  • 遍历父/子 data;
  • 如果属性不存在,就添加;
  • 如果是对象,则递归合并;
  • 避免直接覆盖整个结构。

🍰比喻:

"Vue 合并 data 时特别细心,就像调奶茶师傅会一点点加料: 不会直接把整桶原料倒掉,而是只换掉不同口味的那一部分。"


🧠 总结口诀(第 46~50 页)

类型 举例 合并方式 类比
替换型 methods、computed 后者覆盖前者 新店菜单覆盖旧店
合并型 data 属性递归合并 原配方+新口味
队列型 生命周期钩子 依次执行 按顺序调试流程
叠加型 watch、components 合并追加 多人协作记录

✨一口气记住整章口诀:

"Mixin 配方交总部,mergeOptions 智能煮; 替换新菜旧不留,合并老汤添新料; 钩子排队轮流炒,叠加监听齐上阵~🍳"

🧋第 51 页:队列型合并策略


Vue 的"队列型"合并规则指的是: 👉 生命周期钩子(createdmounted 等)和 watch 监听器。

它们的特点是:

  • 不会互相覆盖;
  • 而是被合并成一个数组
  • 然后 依次执行

🍰举个例子: 如果父组件有一个 created,mixin 里也有一个 created

javascript 复制代码
var mixin = {
  created() { console.log("来自 mixin") }
}

new Vue({
  mixins: [mixin],
  created() { console.log("来自组件") }
})

输出结果:

dart 复制代码
来自 mixin
来自组件

Vue 内部会把它们放进一个数组 [mixinFn, componentFn],然后逐个执行。


🍵生活比喻:

"队列型就像点单排队系统: mixin 点了一杯'绿茶',组件又点了一杯'奶茶', Vue 会按顺序做------绝不会漏掉任何一杯!"


🧋第 52 页:生命周期与 watch 的源码实现


💻 mergeHook 源码

javascript 复制代码
function mergeHook(parentVal, childVal) {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal) ? childVal : [childVal]
    : parentVal
}

📘解释:

  • 如果父组件和子组件都有这个生命周期;
  • Vue 会把它们用 .concat() 拼起来;
  • 变成一个数组依次执行。

💻 merge watch

javascript 复制代码
strats.watch = function (parentVal, childVal) {
  if (!childVal) return Object.create(parentVal || null)
  const ret = {}
  extend(ret, parentVal)
  extend(ret, childVal)
  return ret
}

📘解释:

  • Vue 会把所有 watch 监听函数合并;
  • 如果 key 相同,则合并成数组;
  • Vue 在运行时会遍历执行所有 watcher。

🍰比喻:

"生命周期像多位厨师轮流做菜; 而 watch 像多位服务员都在盯同一个桌号 🍜, 谁先发现顾客有动静,谁就先提醒。"


🧋第 53 页:叠加型策略(component / directives / filters)


💻 代码:

javascript 复制代码
strats.filters = function mergeAssets(parentVal, childVal) {
  const res = Object.create(parentVal || null)
  if (childVal) {
    for (var key in childVal) {
      res[key] = childVal[key]
    }
  }
  return res
}

📘解释:

  • Vue 通过"原型链继承"的方式叠加;
  • 比如全局定义一个 uppercase 过滤器;
  • 子组件再定义一个 trim
  • 两个都会保留,可以互相访问。

🍰生活比喻:

"叠加型就像饮料'加料'系统: 总部菜单有'珍珠',分店又加了'布丁', 顾客能选两种,Vue 会一起提供,不覆盖~🥤"


💡Vue 四种选项合并策略总结表(第 53 页结尾)

类型 示例 合并方式 类比
替换型 props、methods、computed 后者覆盖前者 新菜单替换旧菜单
合并型 data 递归合并属性 老配方加新口味
队列型 生命周期、watch 拼成数组依次执行 排队做菜
叠加型 components、filters 原型链层层叠加 饮品加料系统

🧠口诀:

"替换不留情,合并要包容; 钩子排队转,叠加层层用。"


🧋第 54 页:修饰符(Modifiers)是什么


Vue 的修饰符是"对指令行为的微调按钮"。 让我们写事件或表单时更简单,不用手动处理各种麻烦细节。

在 Vue 中主要有几类:

  • 表单修饰符v-model
  • 事件修饰符@click.stop@submit.prevent
  • 鼠标/键盘修饰符
  • v-bind 修饰符

🍰生活比喻:

"修饰符就像你点奶茶时的'定制选项':

  • .lazy 像'少冰',
  • .trim 像'去奶盖',
  • .number 像'半糖'~ 全都是小调整,让体验更丝滑 🧋。"

🧋第 55 页:表单修饰符详解


1️⃣ .lazy

ini 复制代码
<input v-model.lazy="value">

📘解释:

  • 默认情况下,输入框内容在每次输入时就会更新;
  • .lazy 后,要在"失去焦点(blur)或 change"后才同步;
  • 适合用户输入完再提交的场景。

🍵比喻:

"懒一点的输入,像服务员'顾客说完才下单'。"


2️⃣ .trim

ini 复制代码
<input v-model.trim="value">

📘解释:

  • 自动去掉输入值首尾空格;
  • 中间空格不会去除;
  • 避免不小心多打空格导致匹配失败。

🍰比喻:

"trim 就像上菜前擦掉盘子边的奶油~干干净净。"


3️⃣ .number

ini 复制代码
<input v-model.number="age" type="number">

📘解释:

  • 自动把输入的字符串转成数字;
  • 如果转不了,就保留原始值;
  • 避免你每次都写 parseFloat()

🍵比喻:

"就像点单系统自动识别'一杯半糖奶茶'里的'半糖=0.5'。 Vue 自动帮你转换类型~"


🧁小结(第 51~55 页全章)

分类 功能 示例 比喻记忆
合并策略 管理配置融合 mergeOptions() 奶茶总部融合菜单
队列型 生命周期、watch 排队执行 created、watch 多人轮流做菜
叠加型 component、filters 原型链继承 饮品加料系统
修饰符.lazy 失焦同步 v-model.lazy 说完再下单
修饰符.trim 去空格 v-model.trim 上菜前擦干净
修饰符.number 自动转数字 v-model.number 自动识别糖度

🧠终极口诀:

"修饰符三兄弟,懒人、洁癖、数学迷; 合并四策略,总部菜单配方齐~🍹"

🧋第 56 页:事件修饰符(Event Modifiers)


事件修饰符是 Vue 提供的"事件行为快捷操作", 相当于给你写 event.stopPropagation()preventDefault() 这些烦人的逻辑打包成小药丸 💊。

常见的事件修饰符:

修饰符 作用 对应 JS 原生方法
.stop 阻止事件冒泡 event.stopPropagation()
.prevent 阻止默认行为 event.preventDefault()
.self 只在元素自身触发时才执行 if (event.target === this)
.once 事件只触发一次 listener.once = true
.capture 在捕获阶段触发 addEventListener(..., true)

🍰 举例讲解

1️⃣ .stop
ini 复制代码
<div @click="shout(2)">
  <button @click.stop="shout(1)">ok</button>
</div>

👉 点击按钮只会输出 1,不会触发外层的 2

🍵比喻:

"就像你在奶茶店点单区说话(子元素), 声音不会传到后台(父元素)那边去~"


2️⃣ .prevent
ini 复制代码
<form v-on:submit.prevent="onSubmit"></form>

阻止表单默认提交(不会刷新页面)。

🍰比喻:

"表单默认会'立刻送单',但 .prevent 会告诉它:等等,先加点料再送!"


3️⃣ .self
ini 复制代码
<div v-on:click.self="doThat"></div>

只在当前元素自己被点击时触发(子元素点击不触发)。

🍵比喻:

"只有点到'自己柜台'才接单,别人柜台不算。"


4️⃣ .once
ini 复制代码
<button @click.once="shout(1)">ok</button>

只触发一次。

🍰比喻:

"一次性活动券,用完就没~只能触发一次!"


5️⃣ .capture
ini 复制代码
<div @click.capture="shout(1)">
  <div @click.capture="shout(2)">
    <div @click.capture="shout(3)">
      <div @click.capture="shout(4)"></div>
    </div>
  </div>
</div>

输出顺序:1 2 3 4 👉 从外层往内层执行(捕获阶段)。

🍵比喻:

"就像开门迎客,从店门口到柜台, 每一层都先打个招呼再进去~😄"


🧋第 57 页:更多修饰符 ✨


6️⃣ .passive
ini 复制代码
<div v-on:scroll.passive="onScroll"></div>

📘说明:

  • .passive 表示事件监听不会阻止默认滚动;
  • 优化性能(尤其在手机端滑动时);
  • 不可与 .prevent 一起使用,否则浏览器会警告 🚨。

🍰比喻:

"被动滚动就像自动门~你滑动页面时,它自动放行,不等你确认。"


7️⃣ .native
ini 复制代码
<my-component v-on:click.native="doSomething"></my-component>

📘说明:

  • 用于监听组件根元素上的原生事件;
  • 因为普通的 @click 只能监听组件自定义事件。

🍵比喻:

".native 就像直接和'老板'说话,而不是让前台转达。"


🧋第 58 页:鼠标键盘修饰符


🖱 鼠标修饰符

ini 复制代码
<button @click.left="say(1)">左键</button>
<button @click.right="say(2)">右键</button>
<button @click.middle="say(3)">中键</button>

📘说明:

  • .left:左键点击;
  • .right:右键;
  • .middle:中键。

🍰比喻:

"左键下单、右键取消、中键加料 😆"


⌨️ 键盘修饰符

ini 复制代码
<input type="text" @keyup.enter="submit">
<input type="text" @keyup.keyCode="shout">

📘说明:

  • .enter:回车;

  • .esc:退出;

  • .ctrl / .alt / .shift:系统键;

  • 你也可以自己定义键码:

    ini 复制代码
    Vue.config.keyCodes.f2 = 113

🍵比喻:

"键盘修饰符就像快捷键~按下 ENTER 就等于'提交订单'!"


🧋第 59 页:v-bind 修饰符


Vue 的 v-bind 用来动态绑定属性值,而修饰符让绑定行为更"聪明"。

✨ 常见修饰符:

1️⃣ .sync ------ 实现双向绑定
xml 复制代码
<!-- 父组件 -->
<comp :myMessage.sync="bar"></comp>

<!-- 子组件 -->
this.$emit('update:myMessage', params)

📘说明:

  • .sync 会自动帮你监听子组件发出的 update:myMessage

  • 相当于写:

    ruby 复制代码
    <comp :myMessage="bar" @update:myMessage="bar = $event" />

🍰比喻:

".sync 就像一个'双向传送门'🌀, 父组件改,子组件也变;子改,父也同步。"


2️⃣ .prop
ini 复制代码
<input id="uid" title="title1" value="1" :index.prop="index">

📘说明:

  • .prop 会把绑定的值作为 DOM 属性(property) 而不是 HTML 特性(attribute);
  • 防止污染 HTML。

🍵比喻:

".prop 就像给员工上岗证(属性), 不直接写在外墙(HTML 结构)上。"


🧋第 60 页:camel 修饰符与修饰符应用场景


1️⃣ .camel
复制代码

📘说明:

  • 将属性名从 view-box 自动转换为 viewBox
  • 常用于 SVG 这种区分大小写的标签。

🍰比喻:

".camel 就像自动大写助手,把view-box改成viewBox, 不用你手动敲 shift。"


💡修饰符的应用场景总结(超实用)

修饰符 应用场景 类比记忆
.stop 阻止冒泡 拦截消息不外传
.prevent 阻止默认行为 "等我点完料再送单"
.self 只触发自身 "只听自己柜台的单"
.once 执行一次 一次性优惠券
.capture 捕获阶段触发 从门口到柜台打招呼
.passive 优化滚动 自动门
.native 监听原生事件 直接和老板说话
.left/.right 鼠标点击控制 点单/取消
.enter / .esc 键盘触发 提交 / 退出快捷键
.sync 双向绑定 父子心有灵犀同步更新
.camel 属性名驼峰化 自动首字母大写
.prop 属性绑定更安全 给员工上岗证

🌈 终极口诀(第 56~60 页)

"事件修饰符像调味料: 停止冒泡 .stop,拦截默认 .prevent, 只听自己 .self,一次体验 .once, 捕获打招呼 .capture,滚动更顺滑 .passive, 鼠标分左右,键盘有快捷~ 绑定属性 .prop,同步更新 .sync, 大写驼峰 .camel,Vue 世界更优雅~🧋✨"

🧋第 61 页:什么是 $nextTick


💡官方定义

在下次 DOM 更新循环结束之后执行延迟回调。 在修改数据后立即使用这个方法,获取更新后的 DOM。


🍰通俗理解

Vue 更新 DOM 是异步的, 当你修改数据时,Vue 不会立刻去改页面,而是:

  1. 把这次修改放进"任务队列";
  2. 等本轮事件循环结束后再统一更新 DOM。

💻 例子

ini 复制代码
<div id="app">{{ message }}</div>
const vm = new Vue({
  el: '#app',
  data: { message: '原始值' }
})

vm.message = '修改后的值1'
vm.message = '修改后的值2'
vm.message = '修改后的值3'

console.log(vm.$el.textContent) // 输出:原始值

📘解释:

  • 即使你改了三次 message,页面上还是"原始值";
  • 因为 Vue 还没"来得及"更新 DOM;
  • 它会在下一次"tick"(也就是下一帧)统一更新。

🍵比喻:

"Vue 就像奶茶店的出单机 🧾。 顾客改了好几次订单(加糖、去冰、换奶盖), 店员不会每改一次就重新打印,而是等顾客确认完后, 一次性打印最新的版本。📄"


🧋第 62 页:为什么需要 nextTick


💻 举个例子

ini 复制代码
for (let i = 0; i < 100000; i++) {
  num = i
}

📘如果 Vue 每次都立刻更新视图,就要刷新 10 万次! 性能爆炸 💥

所以 Vue 把这些更新"缓冲"起来, 等循环结束后,一次性更新 ------ 这就是 nextTick 的存在意义。


💡使用场景

如果你想在修改数据后立刻获取最新 DOM ,就要用 nextTick

javascript 复制代码
vm.message = '修改后的值'
console.log(vm.$el.textContent) // 原始值

Vue.nextTick(() => {
  console.log(vm.$el.textContent) // 修改后的值 ✅
})

📘Vue 会在更新完 DOM 后执行 nextTick 的回调函数。


🍰比喻:

"nextTick 就像告诉店员: '等奶茶完全做好后再给我看一下最终成品。' 不然你去太早,只能看到还没加料的半成品 🧋。"


🧋第 63 页:实例调用与 Promise 用法


💻 在组件内调用

kotlin 复制代码
this.message = '修改后的值'
this.$nextTick(() => {
  console.log(this.$el.textContent) // => 修改后的值
})

Vue 会自动绑定当前实例(this),不用写 Vue.nextTick()


💻 Promise 写法

kotlin 复制代码
this.message = '修改后的值'
await this.$nextTick()
console.log(this.$el.textContent) // 修改后的值

📘解释:

  • $nextTick 返回一个 Promise;
  • 所以你可以用 await
  • 比回调更优雅,常用于 async 函数。

🍵比喻:

"用 await this.$nextTick() 就像在外卖系统上点击'等待制作完成'。 系统通知你:饮品 OK ✅,这时再去取最安全。"


🧋第 64 页:源码实现原理(核心函数)


💻 源码解读

javascript 复制代码
export function nextTick(cb, ctx) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })

  if (!pending) {
    pending = true
    timerFunc()
  }

  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

📘关键点解释:

  1. 所有回调函数都压入 callbacks 队列;
  2. 设置一个 pending 标志,避免重复触发;
  3. 调用 timerFunc 异步执行(稍后会讲);
  4. 如果没传回调,则返回一个 Promise。

🍰比喻:

"Vue 建立了一个'代做任务清单 callbacks'🗒️, 顾客每点一个 nextTick,就往清单上加一项。 店员 pending = true 表示'正在忙',防止重复下单。"


🧋第 65 页:timerFunc 选择机制(降级方案)


💻 关键源码(简化版)

javascript 复制代码
if (typeof Promise !== 'undefined') {
  timerFunc = () => { Promise.resolve().then(flushCallbacks) }
} else if (typeof MutationObserver !== 'undefined') {
  timerFunc = () => {
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode('1')
    observer.observe(textNode, { characterData: true })
    textNode.data = '2'
  }
} else if (typeof setImmediate !== 'undefined') {
  timerFunc = () => { setImmediate(flushCallbacks) }
} else {
  timerFunc = () => { setTimeout(flushCallbacks, 0) }
}

📘解释: Vue 会自动根据运行环境选择"最快的异步执行方式":

  1. Promise.then(微任务,速度最快 🚀);
  2. MutationObserver(DOM 变化监听);
  3. setImmediate
  4. setTimeout(..., 0) (最后保底)。

🍵比喻:

"Vue 有四种'店员通知机制':

  • Promise:老板亲自打电话(最快)📞
  • MutationObserver:听墙角的小助理👂
  • setImmediate:传话筒📣
  • setTimeout:慢悠悠派人送信📬 谁能最快传达,就用谁!"

💡最后总结:

  • nextTick 就是 Vue 的"异步更新调度器";
  • 保证 数据变了 → DOM 更新后 → 再执行回调
  • 内部靠一整套 callbacks 队列 + timerFunc 异步触发机制 实现。

🧠终极口诀(第 61~65 页)

阶段 含义 生活比喻
修改数据 Vue 检测到变化 顾客修改奶茶配方
放入队列 不立即更新 DOM 出单机暂存订单
nextTick 回调 DOM 更新完执行 奶茶做好后通知顾客
timerFunc 异步触发机制 谁最快传消息谁上
callbacks 等待区 所有顾客的取单列表

🧋口诀:

"改数据不立更,nextTick 保真神; Promise 通知快,Mutation 助理勤; 出单齐更新,Vue 保效率稳~🍹"

🧩 第 66 页 --- flushCallbacks 与 Vue 实例挂载思考

📜 代码讲解:flushCallbacks

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

意思是:

  • 有个装任务的小盒子 callbacks
  • 当 Vue 需要执行所有任务时(比如更新 DOM),就: 1️⃣ 把盒子里的任务复制一份; 2️⃣ 清空盒子; 3️⃣ 然后一个个执行这些任务。

🌰 生活比喻: 就像你在奶茶店接了一堆外卖单(callbacks), 等到一锅奶茶煮好(DOM 更新)再一起出单。 这样不会每接一个单就去煮奶茶(节省性能)!


🧠 小结(这页总结三点):

  1. 把函数丢进任务队列(callbacks)
  2. 再把执行动作放到"微任务"或"宏任务"中
  3. 到时间后批量执行所有函数

🍵 第 67 页 --- Vue 构造函数入口分析

📜 代码:

java 复制代码
function Vue (options) {
  if (!(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

意思是:

  • 你必须用 new Vue() 来创建实例,否则 Vue 会警告你。
  • 然后执行 _init(options) 方法开始初始化。

🌰 比喻: 就像你去"开奶茶店",必须先注册营业执照(new Vue), 然后 _init() 就是装修+进货(配置 data、methods、props 等)。


下面几行是挂载不同功能模块:

scss 复制代码
initMixin(Vue)       // 定义 _init 方法
stateMixin(Vue)      // 定义 $set $get $delete $watch
eventsMixin(Vue)     // 定义事件系统 $on $emit
lifecycleMixin(Vue)  // 定义生命周期更新等
renderMixin(Vue)     // 定义渲染逻辑

➡️ Vue 就像一台组装机:每个 mixin 都是一个功能模块,比如:

  • stateMixin = 管理数据仓库
  • eventsMixin = 事件中心
  • renderMixin = 渲染引擎

🍰 第 68 页 --- _init() 初始化方法

scss 复制代码
Vue.prototype._init = function (options) {
  const vm = this
  vm._uid = uid++
  vm._isVue = true
  vm.$options = mergeOptions(...)
  
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
​
  initInjections(vm)
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
​
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

💡 重点流程顺序: 1️⃣ 创建实例(this) 2️⃣ 初始化生命周期、事件系统、渲染系统 3️⃣ 触发 beforeCreate 钩子(数据还没好) 4️⃣ 初始化依赖注入 + data/props/watch/methods 5️⃣ 触发 created 钩子(数据就绪) 6️⃣ 如果设置了 el,则挂载到页面上

🌰 生活比喻:

"装修奶茶店的全过程"

  • beforeCreate → 地基阶段,还没进原料(data)
  • created → 材料都进了,但店还没开门(DOM 未挂载)
  • $mount → 店门打开,顾客能看到页面(DOM 渲染)

🍡 第 69 页 --- 详细解读 + 结论

结论里讲了三点非常关键的记忆点:

  1. beforeCreate 阶段 → 数据(data、props)还没初始化好 🔹 比如店铺刚租下,还没进货。
  2. created 阶段 → 数据已经准备好,但页面还没生成 🔹 材料进货了,但奶茶还没倒进杯子(DOM 还没挂载)。
  3. mounted 阶段 → 页面挂载完成,可以操作 DOM 🔹 奶茶已经装好端给顾客。

最后一句:

bash 复制代码
initState 方法完成 props/data/method/watch/methods 的初始化

📖 意思是:initState 是数据系统的核心入口。


🍮 第 70 页 --- initState 的源码逻辑

scss 复制代码
export function initState (vm) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) initData(vm)
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch) initWatch(vm, opts.watch)
}

💡 翻译成人话: Vue 初始化状态时,会: 1️⃣ 初始化 props(外部传来的数据) 2️⃣ 初始化 methods(方法) 3️⃣ 初始化 data(本地数据) 4️⃣ 初始化 computed(计算属性) 5️⃣ 初始化 watch(监听属性)

🌰 比喻:

你开奶茶店准备营业前的五件事:

  • props → 供应商送的原料
  • methods → 店员的操作手册
  • data → 奶茶库存
  • computed → 自动计算售价、折扣
  • watch → 监控原料快没了自动提醒进货

🍬 总结回忆口诀(小可爱专属助记😆)

🧠 new Vue() 就像开奶茶店,步骤如下:

阶段 对应钩子 比喻
beforeCreate 店铺刚租下,还没进货 无法访问 data/props
created 材料进了,但店还没开门 可访问 data,但 DOM 未生成
mounted 店门打开,顾客看到页面 页面渲染完成

然后店铺的基础配置:

  • props → 外部供货
  • data → 内部库存
  • methods → 店员手册
  • computed → 自动计算器
  • watch → 库存报警器

🧋第 71 页 --- initData 初始化 data 阶段

💻 代码核心:

kotlin 复制代码
function initData (vm) {
  let data = vm.$options.data
  data = typeof data === 'function' ? getData(data, vm) : data || {}
  
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  
  while (keys.length) {
    const key = keys.pop()
    if (methods && hasOwn(methods, key)) warn('方法重名')
    if (props && hasOwn(props, key)) warn('属性重名')
    proxy(vm, '_data', key)
  }

  observe(data, true)
}

🌈 一步步解释:

1️⃣ 获取 data

  • 如果是函数,就执行它拿到结果;
  • 如果是对象,直接用;
  • 如果都不是,就给你空对象 {}

👉 比喻: "看店长有没有提供库存清单,有就用;没有就先准备个空货架。"


2️⃣ 检测重名冲突

  • 如果 data 里的变量名跟 propsmethods 重复,会警告你。

👉 "不能既是原料名(props)又是员工名(methods),会混乱!"


3️⃣ 代理数据到 vm 实例上

scss 复制代码
proxy(vm, '_data', key)

这一步就让你能直接通过 this.name 访问到 data.name

👉 "相当于在收银台加了快捷键,不用跑去仓库找货,直接点按钮就能调出原料。"


4️⃣ observe(data) 监听 data 的变化(响应式核心!)

👉 "给货架装上摄像头 📷,一旦奶茶材料数量变动,系统立刻刷新显示。"


📘结论总结:

  • 初始化顺序:props → methods → data
  • data 可以是函数或对象(组件内必须是函数)
  • 数据变化 → 自动触发视图更新(响应式)

🧋第 72 页 --- 小结 & 挂载准备阶段

🌈 小结内容帮你再压缩一下:

内容 含义
初始化顺序 props → methods → data
data 格式 组件必须是函数(防止多实例共享数据)
observe(data) 给数据加"监听器"
下一步 执行 vm.$mount(),进入页面挂载阶段

🍵 比喻一下:

"奶茶店的原料都准备好了(data、methods), 接下来要把菜单(template)打印出来,挂到橱窗(DOM)上展示!"


🧋第 73 页 --- $mount 方法:准备挂载

💻 代码:

ini 复制代码
Vue.prototype.$mount = function (el) {
  el = el && query(el)

  if (el === document.body || el === document.documentElement) {
    warn('不能挂载到 body 或 html')
    return this
  }

  const options = this.$options
  if (!options.render) {
    let template = options.template
    if (typeof template === 'string') {
      if (template.charAt(0) === '#') template = idToTemplate(template)
    } else if (template.nodeType) {
      template = template.innerHTML
    }
  }
}

🌈 解释:

1️⃣ 不能挂载到 或 → Vue 只能挂载到普通容器元素,比如 #app

🍰 比喻:

"菜单只能贴在柜台(div#app)上,不能贴在整栋建筑(body/html)上!"


2️⃣ 解析模板 template

  • 如果是字符串且以 # 开头,就去找对应的 DOM 元素内容;
  • 如果是节点对象,取它的 innerHTML
  • 最终都得到一个 HTML 模板字符串。

🍵 比喻:

"店长可能会说:菜单模板在 #menu 标签里,或者直接给你菜单 HTML。 Vue 会统一拿出来备用。"


🧋第 74 页 --- 模板编译核心:compileToFunctions

💻 代码:

kotlin 复制代码
const { render, staticRenderFns } = compileToFunctions(template, {...})
options.render = render
options.staticRenderFns = staticRenderFns
return mount.call(this, el, hydrating)

📘步骤: 1️⃣ 把 template 解析成 AST 语法树 (Abstract Syntax Tree); 2️⃣ 把 AST 转成 render 函数字符串 ; 3️⃣ 再生成真正的 render 函数。


🌰 比喻:

"Vue 拿到菜单模板 → 画成一棵菜单树(AST) → 再翻译成打印机能懂的打印指令(render 函数) → 打印机执行指令,就能把菜单挂上去了!"


📘小结:

  • template 最终会被转成 render 函数;
  • compileToFunctions() 是编译核心;
  • 千万别把 Vue 根实例挂在 bodyhtml 上。

🧋第 75 页 --- 最终挂载:mountComponent

💻 代码:

javascript 复制代码
Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

🌈 意思: 1️⃣ 找到要挂载的目标元素(比如 #app); 2️⃣ 调用 mountComponent,正式开始渲染组件。


🍰 比喻:

"店面装修好了(data 初始化完),菜单设计好了(template 编译完), 现在就把菜单贴上橱窗(挂载 render 到 DOM)。"


🌟 整体串起来(第 71~75 页)

阶段 源码关键点 通俗解释
initData 初始化 props / methods / data 准备好奶茶原料和员工
observe(data) 监听数据变化 给仓库装上摄像头
$mount 检查目标元素 选好要贴菜单的柜台
compileToFunctions 模板编译成 render 函数 设计菜单图纸,转成打印指令
mountComponent 渲染组件 把菜单打印贴出

🧠 终极口诀:Vue 实例挂载流程总结

"Vue 开店三步走 🧋" 1️⃣ 准备材料(data、props、methods) 2️⃣ 设计菜单(template → render) 3️⃣ 打印橱窗(mount → DOM)

🧋第 76 页 --- mountComponent:Vue 正式开始渲染组件


💻 代码片段:

scss 复制代码
export function mountComponent(vm, el, hydrating) {
  vm.$el = el
​
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode()
    warn('template or render function not defined.')
  }
​
  callHook(vm, 'beforeMount')
​
  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
​
  new Watcher(vm, updateComponent, noop, {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true)
​
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
​
  return vm
}

💡 通俗解释:

  • mountComponent 就是 Vue 把模板挂到页面的主函数。
  • 它会经历这些阶段: 1️⃣ 检查有没有 render 函数; 2️⃣ 触发 beforeMount 钩子; 3️⃣ 定义 updateComponent(渲染页面的方法); 4️⃣ 创建 Watcher ,负责"数据变化 → 触发页面更新"; 5️⃣ 如果首次挂载完成,触发 mounted

🌰 生活比喻:

"奶茶店准备营业啦!"

  • beforeMount:还没开门,员工在做最后检查。
  • Watcher:店长安排了一个"巡店员",专门盯着奶茶库存有没有变化。
  • updateComponent:一旦库存有变,巡店员就去重新贴菜单。
  • mounted:菜单挂上墙,奶茶店正式开业~🎉

🍰第 77 页 --- 渲染与更新的详细过程


💻 代码重点:

ini 复制代码
const vnode = vm._render()
vm._update(vnode, hydrating)

👉 Vue 渲染过程其实是两步: 1️⃣ vm._render():生成虚拟 DOM(VNode); 2️⃣ vm._update():把 VNode 转成真实 DOM 并挂到页面上。


🍵 Watcher 作用:

javascript 复制代码
new Watcher(vm, updateComponent, noop, {
  before() {
    callHook(vm, 'beforeUpdate')
  }
}, true)

Watcher 就像"监控摄像头"📹 当数据变化时,它会重新执行 updateComponent(),也就是重新生成并更新视图。


💡 生命周期对照:

钩子 时机 比喻
beforeMount 开门前检查 员工还在擦桌子
mounted 菜单贴上墙 开业成功
beforeUpdate 数据更新前 新菜单要上线前检查
updated 菜单已更新 顾客看到新价格了

🍵第 78 页 --- updateComponent 方法作用总结


📘解释:

  • updateComponent渲染和更新页面 的核心方法;

  • 一旦数据变化,会触发 beforeUpdate

  • 内部调用:

    • render() → 生成 VNode(虚拟节点树);
    • _update() → 把 VNode 转成真实 DOM。

🍰 比喻版

"updateComponent = 打印机"

  • render:设计好新菜单草稿(VNode);
  • update:把草稿打印出来贴上墙(DOM)。

当库存或价格变化,店长(Watcher)就命令打印机重新打印。


🍮第 79 页 --- _render 函数:生成 VNode


💻 代码:

javascript 复制代码
Vue.prototype._render = function () {
  const vm = this
  const { render, _parentVnode } = vm.$options
  let vnode
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, 'render')
    vnode = vm._vnode
  }
  return vnode
}

💡 通俗理解:

_render() 的任务很单纯:

根据 render 函数,生成一颗 虚拟 DOM 树(VNode Tree)

它并不会直接改动页面,而是先在内存中画好一张"草图"📝。


🍵 比喻:

"Vue 在厨房后面先画好菜单草图(VNode), 确认没问题后才交给打印机(_update)去贴到墙上。"


📘小细节:

  • render 函数由编译器生成;
  • vm.$createElement 是创建 VNode 的小助手;
  • _parentVnode 是父组件的虚拟节点;
  • 有错误时会用上次的旧 vnode 防止页面崩。

🧠第 80 页 --- _update 函数:VNode → 真实 DOM


📜 概要讲解:

_update 是渲染的最后一步: 把虚拟节点(VNode)通过 patch 转换成真实 DOM, 然后更新页面。

📘 _update 源码位置: src/core/instance/lifecycle.js


🌰 比喻:

"厨房打印菜单的时刻来了:

  • render 画好草图(VNode)
  • update 把图打印成实物(真实 DOM)
  • patch:检查旧菜单和新菜单的差异,只更新变动的部分(高效更新)。"

✨最终流程总结(第 76~80 页)

阶段 方法 职责 比喻
1️⃣ 初始化挂载 mountComponent 准备渲染 奶茶店准备开业
2️⃣ 渲染 vnode _render() 生成虚拟 DOM 设计菜单草图
3️⃣ 更新视图 _update() 把 vnode 变成真实 DOM 打印出菜单
4️⃣ 监听变化 Watcher 数据变化触发重绘 店长盯库存,菜单自动更新
5️⃣ 生命周期钩子 beforeMount / mounted / beforeUpdate / updated 各阶段触发回调 记录装修、开业、更新状态

🌸 一句话口诀(背诵模式)

"Render 出草图,Update 去贴图, Watcher 来盯图,数据动菜单动~"

🧩 第 81 页:_update 方法 ------ 把虚拟 DOM 变成真实 DOM!

📄 代码逻辑:

ini 复制代码
Vue.prototype._update = function (vnode) {
  const vm = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  vm._vnode = vnode
​
  if (!prevVnode) {
    // 第一次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, false)
  } else {
    // 更新时对比新旧 vnode
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}

📖 通俗解释: 这段代码干的活很像👇

"前端奶茶店的出品员"。

他负责:

  1. 第一次制作奶茶(patch(null, vnode))→ 做出第一杯;
  2. 之后如果顾客要求"加珍珠、少冰" → 他会对比旧的配方(旧 vnode)和新的要求(新 vnode),只改动必要的部分,而不是重做整杯(这就是 diff 更新!);

所以 _update 就是把虚拟 DOM(Vue 的"奶茶配方")变成真实 DOM(网页上的"成品奶茶")的关键工序 🍹


🧭 第 82 页:Vue 的渲染流程总结

这一页总结了整个 Vue 从创建实例到页面渲染的流程 🧱:

  • 挂载过程由 mountComponent 完成
  • 定义 updateComponent → 负责组件更新(相当于"再制作"逻辑)
  • 执行 render → 把模板转成虚拟 DOM
  • 调用 _update → 把虚拟 DOM 渲染成真实 DOM

👉 简单记法:

new Vue → render → update → patch

生活比喻:

像做奶茶:

  1. init(准备食材)
  2. render(根据配方调配)
  3. update(调整味道)
  4. patch(最后封杯出品)

⚙️ 第 83 页:diff 算法是干嘛的?

🧠 diff 就是 Vue 的"聪明比对器",用来比较两杯奶茶的不同。

✨ diff 是什么?

它是一种高效的"树节点对比算法",用来找出新旧虚拟 DOM 的不同地方,只更新必要的部分。

🔍 特点:

  1. 同层对比,不会跨层(就像对比同一层奶茶配料,不会跑去换整杯容器)
  2. 从两边往中间比(像夹心饼干,两边同时吃 😋)

🔄 第 84--85 页:diff 过程图解(超关键!)

来,我们一起像动画一样复盘这几张图 👇


🟢 第一次循环(P84 上半部分)

旧节点:A B C D 新节点:D C E A

  • 发现旧的尾巴 D = 新的头 D
  • ✅ 直接复用 D,不用重新创建
  • 然后旧的尾巴往前挪,新头往后挪(像两个指针靠近)

🧋类比:老板发现上次做的"珍珠奶茶 D"味道一样,就直接继续用那杯,不用重做~


🔵 第二次循环(P84 下半部分)

旧尾巴 C = 新头 C

  • ✅ 又对上了,C 也能复用
  • 再移动两个指针:旧的尾巴往前(变成 B),新的头往后(变成 E)

🟡 第三次循环(P85 上半部分)

旧的 B vs 新的 E

  • ❌ 不匹配,只能新建 E,并插入到刚创建的 C 后面。

🟠 第四次循环(P85 下半部分)

旧头 A vs 新头 A

  • ✅ 匹配成功!复用旧 A,移动指针。
  • 最终旧节点更新完毕 🎉

🎯 diff 总结口诀:

"头尾对比四步走, 复用就跳不匹配补; 同层比对从两边收, 最后更新 DOM 没烦恼。"

💡 换成小可爱记忆法:

步骤 比喻 发生了什么
第一次 奶茶 D 一样 直接复用
第二次 奶茶 C 一样 继续复用
第三次 新来了奶茶 E 新建
第四次 奶茶 A 一样 再复用,完成

💬 小结(P81--P85 精简总结表)

阶段 方法 功能 生活类比
初始化 _init 设置属性/事件等 准备食材
渲染 render 生成虚拟 DOM 写奶茶配方
更新 _update 将虚拟 DOM 转为真实 DOM 制作奶茶
diff patch 对比新旧差异,只改动部分 调整加料,不重做整杯

🧩 第 86--87 页:diff 算法的最后几轮循环

💡 背景:

前面几页讲了 diff 的前四次循环,现在进入第五、第六次。Vue 的 diff 会不断比对"旧节点数组"和"新节点数组",直到全部匹配或更新完。


🧠 图解复盘

✅ 第五次循环(P86 上半)

旧节点:A B C D 新节点:D C E A B F

  • 这时情况和上次一样,diff 发现:

    • 新旧节点的头尾都不一样;
    • 所以创建新的 B 节点(因为在新列表中它出现在最后)。
  • 把 B 插到 A 的后面(DOM 移动)。

  • 然后继续移动索引(旧的往前,新节点往后)。

🍵 比喻:

奶茶店菜单上最后新增了"布丁奶茶 B",就插在"红茶拿铁 A"后面,贴到墙上。


✅ 第六次循环(P86 下半)
  • 新节点的 startIndex 已经比 endIndex 大,代表新节点都处理完了;
  • 但旧节点还有没用的部分 → 把旧节点中剩下的(或新节点未创建的)F,直接创建并添加到末尾。

🍵 比喻:

菜单上最后一项"鲜奶 F"之前没贴上,现在全部对齐后补上去!


✅ 第七次循环(P87)

所有节点比对完成。

  • oldStartIndex > oldEndIndex → 退出循环;
  • diff 结束 🎉;
  • 新的 DOM 树完整更新完毕。

📍结论:

diff 就像两个指针在"旧菜单"和"新菜单"两边往中间走,一边比一边移动。 只要发现一样的,就复用;不一样的,就新建或删掉。


🧠 第 87 页下:diff 原理分析

代码位置:src/core/vdom/patch.js

当数据发生变化时:

  1. Vue 内部的 set() 方法会调用 Dep.notify()
  2. 通知所有依赖的 Watcher;
  3. Watcher 再调用 patch
  4. patch 会根据 diff 算法对真实 DOM 打补丁,更新对应的页面部分

🍰 比喻:

店长发现菜单数据(配方)变了,就通知出品员(Watcher), 出品员拿着笔(patch)去修改墙上的菜单(DOM),不用整面墙重画,只改必要的部分。


🧱 第 88 页:patch() 函数解析(一)

scss 复制代码
function patch(oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVNodeQueue = []

  if (isUndef(oldVnode)) {
    isInitialPatch = true
    createElm(vnode, insertedVNodeQueue)  // 没有旧节点,直接创建新节点
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVNodeQueue) // 一样 → 对比更新
    } else {
      oldVnode = emptyNodeAt(oldVnode)
      createElm(vnode, insertedVNodeQueue)
    }
  }
  return vnode.elm
}

💡 通俗解释:

1️⃣ 没有新节点(vnode) → 页面要删掉旧的内容,调用销毁钩子。

2️⃣ 没有旧节点(oldVnode) → 页面第一次渲染,直接 createElm() 创建所有 DOM。

3️⃣ 有新有旧,但结构一样(sameVnode) → 调用 patchVnode() 局部更新(高效)。

4️⃣ 有新有旧,但结构不同 → 删除旧的,创建新的。


🍵 生活比喻:

  • 没旧菜单 = 新店开业,整墙重画。
  • 旧菜单和新菜单结构一样 = 只改几项配方(比如"多加珍珠")。
  • 不一样 = 整块换掉,比如换掉整类饮品区域。

🧩 第 89 页:patch() 四种情况总结

📋 内容精华:

  • 没有旧节点 → 页面初次初始化,直接新建(createElm)。
  • 旧节点和新节点一样 → 复用 DOM,只局部修改(patchVnode)。
  • 旧节点和新节点不一样 → 删除旧节点,新建新节点。

👉 下一节讲的重点是 patchVnode()


🍰 奶茶店比喻总结

情况 比喻 Vue 行为
没旧节点 新店刚装修完,要贴新菜单 createElm()
旧节点和新节点一样 菜单样式一样,只改价目 patchVnode()
旧节点和新节点不同 菜单结构换了 删除旧节点 + 创建新节点

⚙️ 第 90 页:patchVnode() 源码详解

scss 复制代码
function patchVnode (oldVnode, vnode) {
  if (oldVnode === vnode) return
  const elm = vnode.elm = oldVnode.elm

  if (isTrue(oldVnode.isAsyncPlaceholder)) { ... }

  if (isTrue(vnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  const data = vnode.data
  const oldCh = oldVnode.children
  const ch = vnode.children

  if (isDef(data) && isDef(data.hook)) data.hook.prepatch(oldVnode, vnode)
  if (isDef(oldCh) && isDef(ch) && oldCh !== ch) {
    updateChildren(elm, oldCh, ch)
  } else if (isDef(ch)) {
    createElm(vnode)
  }
}

🧠 解释:

  • 相同节点(sameVnode) : → 不重建,只更新内容;
  • 不同节点: → 重新创建新的 DOM;
  • 静态节点: → 不变的跳过;
  • 有子节点时 : → 进入 updateChildren,对比子节点(就是前面 diff 那一堆循环的地方!)。

🍵 奶茶店比喻版:

patchVnode 就像出品员拿着旧菜单比对新菜单:

  • 一样的奶茶就保留;
  • 不同的重新做;
  • 静态项目(比如"店名 LOGO")不动;
  • 如果菜单里还有子菜单(比如"季节限定"小分类),就递归比对里面的条目。

🧁 最终总结(P86--P90)

阶段 核心函数 作用 奶茶店比喻
diff 比对 双端指针 从两边向中间比较新旧节点 老板比对旧新菜单
patch 整体更新入口 判断是否新建 / 更新 / 删除 出品员拿到新菜单决定改法
patchVnode 节点级别更新 精准修改或复用节点 菜单具体条目逐项调整
createElm 创建 DOM 初次渲染 从零贴菜单
updateChildren 子节点递归更新 diff 核心逻辑 检查每个奶茶分类下的小项目

🌸 一句口诀帮你记住整个流程:

"第一次 createElm, 第二次 patchVnode, 差异靠 diff 算, 菜单更新不用慌~✨"

💬 一、Vue 更新的幕后逻辑:patch 是什么?

一句话解释:

patch() 就是 Vue 更新页面的"主入口"。 它会比较「旧虚拟 DOM」和「新虚拟 DOM」,然后只改动有变化的地方。


🍵 想象一下:

你是 Vue 奶茶铺的店长。 每天要根据"新菜单"更新前台展示。

  • 旧菜单:

    markdown 复制代码
    1. 珍珠奶茶
    2. 红豆奶茶
    3. 椰果奶茶
  • 新菜单:

    markdown 复制代码
    1. 珍珠奶茶
    2. 草莓奶茶
    3. 椰果奶茶

你会怎么做? 不会傻乎乎地重印一整张菜单,而是: 🧋只换掉第 2 行的"红豆奶茶 → 草莓奶茶"。

这就是 Vue 的 patch() 所做的事:

精准更新、避免重绘、节省性能。


🧠 二、源码核心(简化版)

scss 复制代码
function patch (oldVnode, vnode) {
  // 如果没有新节点 → 销毁旧节点
  if (!vnode) {
    destroy(oldVnode)
    return;
  }
​
  // 第一次渲染:直接创建新节点
  if (!oldVnode) {
    createElm(vnode)
  } 
  // 有旧节点:开始比较新旧节点
  else if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode)
  } 
  // 新旧节点不同:删除旧的,创建新的
  else {
    removeVnode(oldVnode)
    createElm(vnode)
  }
}

🧋 三、生活比喻讲解(一步步理解 patch)

步骤 程序逻辑 奶茶铺类比
🥤1️⃣ 第一次渲染 页面上还没东西 → 全部创建 第一次开店,要新印整张菜单
🧾2️⃣ sameVnode 判断 比较新旧节点是否相同 比较两行菜单是不是"同一款奶茶"
✏️3️⃣ patchVnode 更新 如果是同一款,更新内容 只改名字、配料、价格,不重印整张
🗑️4️⃣ removeVnodes 删除 如果不同,删掉旧的 下架"红豆奶茶"
🧱5️⃣ createElm 新建 创建新节点 上架"草莓奶茶"

🪄 四、patchVnode:菜单细节更新员

ini 复制代码
function patchVnode(oldVnode, vnode) {
  if (oldVnode === vnode) return;
  const elm = vnode.elm = oldVnode.elm;

  // 如果是静态节点且相同,就复用
  if (vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }

  // 否则,更新文本、属性、子节点
  if (oldVnode.text !== vnode.text) {
    elm.textContent = vnode.text;
  } else {
    updateChildren(elm, oldVnode.children, vnode.children);
  }
}

💡 它做的事就像奶茶店更新菜单细节

更新类型 程序行为 现实例子
修改文本 textContent 把 "红豆奶茶" 改成 "草莓奶茶"
更新属性 调整节点属性 把 "限时特价" 标识加上
更新子节点 递归更新下级 把配料列表也一起更新

🍰 五、diff 算法登场:Vue 的比对法

Vue 在 updateChildren() 阶段使用了一种超高效的算法:

双端对比法(Four Pointers)

用四个指针,从新旧数组两头同时往中间夹:

复制代码
oldStart / oldEnd
newStart / newEnd

🌰 举例:菜单重排

旧菜单:[A, B, C, D, E] 新菜单:[D, C, E, A, B]

Vue 比对过程如下:

  1. 比较头:A vs D ❌
  2. 比较尾:E vs B ❌
  3. 发现 D 在新菜单最前面 → 把 D 移前
  4. 继续比较 C、E,一样的就跳过
  5. 剩下的 A、B 移到最后

💡 结论: Vue 不会重建所有节点,只会:

移动、重排、复用节点。

👉 节省性能、页面更快!


🧩 六、整个 patch 流程总结

阶段 发生的事 类比
1️⃣ 判断是否初次渲染 没有旧节点 → 创建所有元素 第一次开店要建新菜单
2️⃣ 比较节点是否相同 sameVnode() 判断是不是同一种奶茶
3️⃣ 相同节点更新内容 patchVnode() 改配料、改价格
4️⃣ 不同节点重建 createElm() + removeVnodes() 下架旧奶茶,上架新口味
5️⃣ 更新完收尾 insertedVnodeQueue 清空 更新菜单完毕,前台展示新菜单

🧠 七、小可爱也能背的"关键词口诀"💡

🧾 两张菜单一对比, 🍹 一样就更新, 🍓 不一样就重建, 🧱 DOM 省力不重画!


🧠 八、关键词详解(生动生活版)

关键词 Vue 的作用 奶茶铺类比
🧾 patch 整个更新入口,比较新旧节点并更新 DOM 店长检查旧菜单和新菜单的不同,决定改哪行
🧱 createElm 创建真实的 DOM 元素 打印并贴出新的奶茶菜单
🗑️ removeVnodes 删除旧节点 把"红豆奶茶"从菜单上撕掉
✏️ patchVnode 更新已有节点的细节 改菜单文字、更新价格、调整说明
🧩 sameVnode 判断两个虚拟节点是不是同一款 比较奶茶名字、编号、key 是否一样
🔁 diff 高效对比新旧子节点的算法 用"四指比较法"快速找出菜单顺序差异
🧮 updateChildren 递归更新子节点 菜单下的"配料列表"、"活动标签"等也要更新

🍵 九、总结一句话

Vue 的更新策略不是"全部推倒重来", 而是像一个聪明的奶茶店长 🍹: 只改变真正有变化的那几款, 既快又省,顾客(用户)体验超丝滑!✨

🌸 第 96--100 页核心主题

Vue 组件是什么?插件又是什么?它们有什么区别?怎么注册和使用?


一、组件是什么(14.1 节)

🧱 官方定义

组件(Component)是可复用的 Vue 实例, 拥有自己独立的模板(template)、数据(data)和逻辑(methods)。

🍵 通俗理解

"组件就是一块可以重复使用的小积木。"

在一个网页里:

  • 导航栏是一块组件;
  • 登录表单是一块组件;
  • 评论区也是一块组件。

它们都能单独开发、单独维护、单独使用。 最后再像拼积木一样拼在一起组成完整页面。


🧋 生活类比:奶茶铺的模块化装修

想象你要开一家奶茶店 🍹 你不可能每次都从头造桌子、做柜台、搭墙壁。

你会怎么做?

  • 桌子是一个"组件";
  • 点单区是一个"组件";
  • 菜单灯箱是一个"组件"。

这些"组件"拼在一起就变成整个店面。 👉 改桌子样式,不影响菜单灯箱。 👉 复用性高、省心又省力!


二、插件是什么(14.2 节)

⚙️ 官方定义

插件(Plugin)是为 Vue 添加全局功能的机制。

比如:

  • 注册全局方法;
  • 注册全局组件;
  • 添加自定义指令;
  • 混入 (mixin) 一些公共逻辑。

🧋 类比:奶茶铺的"万能加料机"

组件就像一张"菜单上的奶茶", 插件就像一台"可以加料的机器"。

比如这台机器能:

  • 自动给所有奶茶加冰(全局指令);
  • 自动计算折扣(全局方法);
  • 自动注册菜单模板(全局组件)。

📦 插件的特点是: 一次安装,处处生效。


三、两者区别(14.3 节)

对比项 组件 Component 插件 Plugin
定义 可复用 UI 模块 扩展 Vue 功能的机制
作用范围 局部使用 全局有效
注册方式 Vue.component() Vue.use()
例子 登录框、按钮、列表 ElementPlus、VueRouter、Vuex
类比 单独奶茶杯 奶茶机(影响所有奶茶)

💡 小记:

组件关注「视图层」; 插件关注「功能扩展层」。


四、组件与插件的注册

🧩 组件注册(14.3.1)

arduino 复制代码
Vue.component('my-button', {
  template: '<button>点我呀</button>'
})

💡 意思是: 定义一个名叫 my-button 的组件, 它会在模板中渲染为一个 <button>

"给菜单新增一款'点我按钮奶茶'。"


⚙️ 插件注册(14.3.2)

插件一般导出一个 install 方法,Vue 会自动调用它:

javascript 复制代码
const MyPlugin = {
  install(Vue, options) {
    // 注册全局组件
    Vue.component('my-button', { /* ... */ })
    // 添加实例方法
    Vue.prototype.$hello = function() {
      console.log('你好 Vue!')
    }
  }
}
Vue.use(MyPlugin)

💡 类比:

"把这台加料机(插件)安装到你的奶茶店(Vue 实例)里, 从此所有奶茶都能自动加冰 / 打招呼!"


五、注意事项(14.3.3)

Vue 的插件 install() 只能执行一次。 如果你多次 Vue.use(),Vue 会自动忽略后面的。

💬 "机器安装一次就够了,别重复插电了!"


六、使用场景(14.4 节)

  • 如果是"独立的 UI 模块" → 用 组件
  • 如果是"跨组件复用的功能" → 用 插件

🌰 举例:

场景 选谁? 为什么
登录弹窗、评论框 组件 每个页面单独渲染
统一注册接口方法 插件 全局通用
Element Plus 插件 内部帮你注册了一堆组件
Markdown 渲染器 插件 提供功能,不直接渲染视图

🧠 七、关键词总结(带生活类比)

关键词 含义 生活比喻
🧩 组件 (Component) 可复用的 UI 模块 一杯奶茶(独立售卖)
⚙️ 插件 (Plugin) 扩展 Vue 全局功能 奶茶机(加冰、打折的设备)
🏷️ Vue.component() 注册组件 把"奶茶杯"摆上菜单
🔧 Vue.use() 安装插件 插上"加料机"开关
💬 install() 插件入口方法 奶茶机启动时要先"初始化"
🌍 全局注册 所有组件都能用 所有奶茶都能加料
🧱 局部注册 当前页面能用 仅特定奶茶柜可用

🍰 八、快速记忆法(押韵小口诀)

🧱 组件像杯奶茶, ⚙️ 插件像台加料架。 局部展示是组件, 全局扩展靠插件!😆


🌟 九、额外内容:Vue 项目中的代理机制(15 节)

在第 15 节开头提到:

Vue 项目里用"代理(proxy)"解决前后端跨域。

你可以理解成: 当你在本地调接口时, Vue 会假装自己是个"中间人 🕵️‍♀️", 帮你把请求"中转"到服务器。

  • 你(前端) → Vue 开发服务器(代理) → 后端 API 这样浏览器就不会报跨域错误啦。

💡 类比:

顾客(前端)不能直接进厨房(后端), 店长(代理服务器)代为点单转达给厨师。


🧾 十、总结回顾

概念 功能 比喻
组件 局部 UI 模块 独立奶茶杯
插件 扩展全局功能 加料机
注册方式 Vue.component / Vue.use 上架奶茶 / 安装机器
作用范围 局部 / 全局 单店使用 / 所有店通用
代理 前后端通信桥梁 店长帮顾客转单

🎯 十一、记忆口诀(强化版)

组件拼页面,插件扩功能。 代理来沟通,前后不冲突。 Vue 模块化,复用超轻松!✨

🍰 第 101--105 页核心主题

  1. Vue 项目中代理是如何解决跨域的?
  2. 有哪些自定义指令?它们和函数/组件的区别?

🍓 一、什么是跨域?(15.1)


🧠 概念解释:

跨域是浏览器的一种"安全策略",叫 同源策略(Same-Origin Policy) 。 浏览器只允许访问相同协议、域名、端口的资源。

也就是说:

当前网页 访问地址 是否允许
http://localhost:8080 http://localhost:8080/api ✅ 同源
http://localhost:8080 http://localhost:8989/api ❌ 不同端口,跨域
a.com b.com/api ❌ 不同域名,跨域
a.com a.com ❌ 不同协议,跨域

🧋 生活类比:

假设你在 奶茶店 A(localhost:8080) , 想去 奶茶店 B(localhost:8989) 借冰块 🍧。

浏览器老板(安全管理员)说:

"不行!不同店的厨房不能随便串门!"

这时候,你就遇到 跨域问题 😭。


🍹 二、解决方案 1:CORS(后端开放访问)


📦 CORS(跨域资源共享)原理

服务器在响应头里加上:

makefile 复制代码
Access-Control-Allow-Origin: *

意思是:

"没事啦,谁都可以来喝我家的奶茶!"


💡 举例代码:

javascript 复制代码
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
  next();
});

💬 生活比喻:

奶茶店 B 门口挂个牌子:

"欢迎各路奶茶同行来借冰块!🍧"


🧠 三、解决方案 2:Proxy(前端反向代理)


🧩 Proxy 是什么?

在开发阶段,前端项目(Vue)自己扮演一个"中间人": 当你请求后端接口时,Vue 会替你转发请求。


💻 配置示例:

java 复制代码
// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:8989',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  }
};

💬 含义解释:

配置项 说明
/api 匹配前端请求前缀
target 要转发的后端服务器地址
changeOrigin: true 伪装请求来源(把"localhost:8080"改成"localhost:8989")
pathRewrite 可选,重写路径

🧋 类比说明:

想象你(前端)不能直接去隔壁奶茶店 B 借冰块, 于是你让"前台小妹(代理服务器)"帮你去拿。

  • 顾客 → 前台(Vue) → 后厨(后端)
  • 前台帮你去取冰,然后再转交给你。

顾客(浏览器)以为全程都在一个店里办事, 所以就不会触发"跨域警报"啦 🎉!


🍵 四、CORS 与 Proxy 的区别对比

项目 CORS Proxy
实现位置 后端服务器 前端开发服务器
改动方 后端配置 前端配置
适用阶段 生产环境 开发环境
例子 res.header('Access-Control-Allow-Origin','*') devServer.proxy
类比 店 B 主动开放 店 A 自己派人代取

👉 一句话记:

"CORS 是别人请你进门,Proxy 是你找人帮跑腿。"


🧱 五、自定义指令(Directive)


16.1 什么是指令

🧠 定义:

Vue 的 指令(Directive) 是一种特殊语法,用来直接操作 DOM 元素

常见的内置指令有:

  • v-ifv-forv-modelv-show...

🧋 类比说明:

如果"组件"是奶茶的配方, 那"指令"就是奶茶机上的"按钮"。

比如:

  • v-show 是"展示"按钮;
  • v-model 是"同步数据"按钮;
  • v-focus 是"自动聚焦输入框"的按钮。

🪄 16.1.1 自定义指令代码示例:

javascript 复制代码
Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

📖 用法:

css 复制代码
<input v-focus />

💡 含义:

当元素插入页面后,自动执行 focus(),让输入框获得焦点。


🧋 类比:

就像你在奶茶机上装了一个自动启动按钮, 每次新开机(DOM 挂载)时,自动执行操作~⚡


🧠 六、指令生命周期(Directive Lifecycle)

钩子函数 触发时机 比喻
bind 指令第一次绑定到元素时 按钮装上机器
inserted 元素插入 DOM 按钮被按下开始工作
update 元素更新 重复按下按钮执行新任务
unbind 指令解绑 拆掉按钮

例子:实现一个拖拽指令

ini 复制代码
Vue.directive('drag', {
  inserted(el) {
    el.onmousedown = function(e) {
      let disX = e.clientX - el.offsetLeft;
      let disY = e.clientY - el.offsetTop;
      document.onmousemove = function(e) {
        el.style.left = e.clientX - disX + 'px';
        el.style.top = e.clientY - disY + 'px';
      }
      document.onmouseup = function() {
        document.onmousemove = document.onmouseup = null;
      }
    }
  }
});

📖 使用:

css 复制代码
<div v-drag>拖我动动~</div>

💬 生活比喻:

给奶茶菜单贴上"可滑动"的标签, 顾客可以自由拖拽、移动菜单位置~📜


🧩 七、指令 vs 组件 vs 插件(总对比)

类型 作用 类比 使用范围
组件 组织 UI 一杯奶茶 局部
插件 扩展功能 加料机 全局
指令 操作 DOM 奶茶机按钮 局部或全局

🍬 八、小可爱记忆口诀 💡

🍹 组件拼页面, ⚙️ 插件扩功能, 🔘 指令动元素, 🧊 代理防跨域!


🌟 九、总结要点

模块 一句话总结 类比
CORS 后端加响应头放行 奶茶店门口贴"欢迎借冰"
Proxy 前端中间人转发请求 前台帮你去隔壁取冰
指令 操作 DOM 的语法糖 奶茶机上的开关按钮
自定义指令 手动造按钮 让奶茶机更智能化
生命周期 bind → inserted → update → unbind 安装 → 启动 → 使用 → 拆除

💬 小结一句话记:

Vue 是一个聪明的"奶茶工厂":

  • 组件是奶茶杯
  • 插件是加料机
  • 指令是操作按钮
  • Proxy 是跑腿员
  • CORS 是通行证

🌟 第 106--110 页核心主题

Vue 自定义指令进阶:从"自动聚焦"到"懒加载""复制文本"等实战应用。


🧋 一、复习一下:指令是什么?

Vue 的指令(Directive)就是 ------ "直接控制 DOM 元素行为的语法糖" , 比如让按钮自动聚焦、元素懒加载、复制文字、控制权限等。

📘 简单回顾例子:

javascript 复制代码
Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

💡 作用:当元素挂载到页面后,就自动获得焦点。 比如在登录页,输入框一出来就自动闪烁,等你输入账号。


🍰 类比:

这就像奶茶店的 自动开机功能

"每次店一开门,点单机就自己启动。" 不需要人工点击,非常智能。


🍹 二、为什么要自定义指令?

因为 Vue 的默认指令(v-if, v-show, v-model 等) 有时候无法满足"特定场景需求"。

比如:

  • 页面加载时自动聚焦(v-focus
  • 图片懒加载(v-lazy
  • 点击按钮自动复制文字(v-copy
  • 控制按钮权限(v-permission

这些都是"操作 DOM 层面"的功能, 用组件写太麻烦,用指令最直接!💪


🧠 三、指令的注册方式(第 106 页)

Vue 支持两种注册指令方式:

1️⃣ 全局注册

javascript 复制代码
Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

2️⃣ 局部注册

scss 复制代码
directives: {
  focus: {
    inserted(el) {
      el.focus();
    }
  }
}

💬 类比:

  • 全局注册 = 给所有奶茶机都装上"自动加冰"开关;
  • 局部注册 = 只给某一台机器(某个组件)安装这个功能。

🍓 四、指令生命周期钩子(第 107 页)

每个指令都有生命周期钩子(像组件一样):

钩子函数 触发时机 比喻
bind 第一次绑定到元素时 "安装按钮"
inserted 元素插入页面时 "按钮通电"
update 元素更新时 "重新触发开关"
unbind 元素移除时 "卸载按钮"

🧋 五、实战案例 1:图片懒加载(v-lazy)

(第 108 页)

当页面上有很多图片时,如果一次性全部加载,会让页面卡顿。 懒加载的思想就是:

"只在用户看到图片的时候,再去加载它。"


💻 代码示例:

ini 复制代码
Vue.directive('lazy', {
  inserted(el, binding) {
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value; // 真正加载图片
          observer.unobserve(el);
        }
      });
    });
    observer.observe(el);
  }
});

💬 使用:

ini 复制代码
<img v-lazy="'https://xxx.com/cat.jpg'">

🧋 生活比喻:

顾客靠近奶茶柜时(图片出现在视窗内), 奶茶机才开始"摇奶茶"出杯。 👀 你看到了,才会加载资源!

👉 节能高效,又不浪费"原料"(带宽)。


🍇 六、实战案例 2:复制文本(v-copy)

(第 109--110 页)

点击一个按钮,自动复制文字到剪贴板。


💻 代码示例:

ini 复制代码
Vue.directive('copy', {
  inserted(el, binding) {
    el.addEventListener('click', () => {
      const input = document.createElement('input');
      input.value = binding.value;
      document.body.appendChild(input);
      input.select();
      document.execCommand('copy');
      document.body.removeChild(input);
      alert('复制成功!');
    });
  }
});

💬 使用:

ini 复制代码
<button v-copy="'奶茶配方:珍珠 + 红豆 + 椰果'">复制奶茶配方</button>

🍹 类比说明:

当你点按钮时,相当于:

"服务员一键帮你把奶茶配方抄在小纸条上 ✍️!"

每次点击都能自动拷贝,不用手动选中复制。 是不是很贴心!💖


🍰 七、实战案例 3:按钮权限控制(v-permission)


💻 代码示例:

ini 复制代码
Vue.directive('permission', {
  inserted(el, binding) {
    const userRole = localStorage.getItem('role');
    if (binding.value !== userRole) {
      el.parentNode && el.parentNode.removeChild(el);
    }
  }
});

💬 使用:

ini 复制代码
<button v-permission="'admin'">删除用户</button>

💡 类比:

在奶茶铺:

  • 店长能用"删除订单"按钮;
  • 员工就看不到这个按钮。

Vue 指令帮你自动判断:

"当前用户有权限吗?没有就不渲染。"

💪 让你的系统"智能安全",再也不怕误删!


🧾 八、实战案例总结(第 110 页)

指令名 功能 生活类比
v-focus 自动聚焦输入框 奶茶机开门自启
v-lazy 懒加载图片 顾客靠近才摇奶茶
v-copy 一键复制文字 服务员帮抄配方
v-permission 权限控制按钮 店长专属操作区

🧠 九、小可爱记忆口诀 💡

🔘 指令是开关, 🍹 动 DOM 不慌。 🧊 懒加载省电, 📋 复制最方便, 🧱 权限有保障!


🎯 十、总结一句话

Vue 的自定义指令 = DOM 层的"自动化小助手"

就像奶茶铺的智能设备系统, 能自动摇奶、自动开机、自动贴标签。

组件管"外观", 指令管"动作", 合起来就是------

"既好看又聪明的前端奶茶铺!" 🧋✨

🌟 第 111--115 页核心主题

Vue 的过滤器(Filter)是什么、怎么用、有哪些应用场景、以及原理。


🧋 一、过滤器是什么(第 111--112 页)


📘 官方定义

Vue 过滤器(Filter)用于格式化输出内容, 比如把日期格式化、把价格加单位、把字符串首字母大写等。

在模板中可以通过 | 管道符号使用。


💬 通俗解释:

过滤器就是在"展示前处理一下数据的小筛子"。

比如:

  • 数据里是 1000,你希望页面显示为 ¥1,000
  • 数据是 2025-10-17T00:00:00Z,你希望显示成 2025年10月17日
  • 用户名是 nickbai,希望显示为 Nickbai

你不会去改数据本身,只是在展示时过滤加工一下


🧋 奶茶铺生活比喻:

想象你开奶茶店时------ 顾客点的原料(数据)都在仓库。 但在出杯时,你要:

  • 加冰(加单位)
  • 去糖(截取小数)
  • 加珍珠(加符号)

💡 过滤器(Filter)就像"出杯前的奶茶筛", 帮你把原料加工成更好看的样子端给顾客!


🍰 二、如何使用过滤器(第 112 页)


💻 语法:

javascript 复制代码
Vue.filter('过滤器名称', function(value) {
  // 处理逻辑
  return 处理后的值;
});

使用方式:

scss 复制代码
{{ message | capitalize }}

📦 示例 1:首字母大写

ini 复制代码
Vue.filter('capitalize', function(value) {
  if (!value) return '';
  value = value.toString();
  return value.charAt(0).toUpperCase() + value.slice(1);
});

💬 模板:

bash 复制代码
<p>{{ 'milk' | capitalize }}</p>

📤 输出:

复制代码
Milk

🧋 奶茶铺类比:

顾客点单写的是 "milk tea"。 你在出杯前觉得:

"这样太没气势了!" 于是给它首字母大写------Milk tea

过滤器帮你"自动修饰菜单的文字",更高端 😆


📦 示例 2:价格加单位

javascript 复制代码
Vue.filter('currency', function(value) {
  return '¥' + parseFloat(value).toFixed(2);
});

💬 模板:

css 复制代码
<p>{{ 12 | currency }}</p>

📤 输出:

复制代码
¥12.00

💡 生活比喻:

像奶茶出杯前自动贴上"¥"标签。 顾客更一目了然,不用自己想价钱。


🧠 三、过滤器分类(第 113 页)


Vue 过滤器分为:

类型 使用场景 示例
全局过滤器 所有组件都能用 Vue.filter(...)
局部过滤器 只在当前组件用 filters: {...}

💬 类比:

  • 全局过滤器 → 奶茶店所有分店统一出杯样式;
  • 局部过滤器 → 只在"珍珠奶茶区"定制特殊包装。

🍹 四、过滤器链式调用(第 113--114 页)


你可以同时使用多个过滤器:

css 复制代码
<p>{{ price | currency | uppercase }}</p>

📤 执行顺序:

从左到右,一个过滤结果再传给下一个。

💬 类比:

奶茶制作流程:

  1. 加奶;
  2. 加糖;
  3. 打上 Logo 贴纸。

结果:一杯又香又有品牌感的奶茶!✨

"过滤器链" = 奶茶出杯的多重加工流程 🧋


🍬 五、过滤器的小结(第 114 页)


  • 过滤器只影响显示,不改数据
  • 可在模板表达式中使用;
  • 可链式组合使用;
  • 可定义为全局或局部。

📦 小总结表:

功能 用法 示例
定义过滤器 Vue.filter(name, fn) Vue.filter('upper', val => val.toUpperCase())
使用过滤器 {{ msg upper }}
局部注册 filters: { upper() {...} } 组件内部使用
多层过滤 {{ msg trim

🧋 六、实战应用(第 115 页)


📦 示例:格式化时间

javascript 复制代码
Vue.filter('dateFormat', function(value) {
  if (!value) return '';
  const date = new Date(value);
  return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}`;
});

💬 模板:

bash 复制代码
<p>{{ '2025-10-17T00:00:00Z' | dateFormat }}</p>

📤 输出:

yaml 复制代码
2025-10-17

🍹 类比说明:

顾客下单时间是 "2025-10-17T00:00:00Z"(超级长英文时间)。 过滤器帮你:

"把订单时间格式化成好看的人类语言。"

👉 过滤器 = "前台打印小票的格式化机"。


🧠 七、过滤器的底层原理分析(第 115 页)


底层其实是:

Vue 在模板编译时,会自动把 {{ msg | filterName }} 转换成一个函数调用表达式,比如:

objectivec 复制代码
_this._f('filterName')(msg)

也就是 _f 是 Vue 内部的过滤器函数注册表。 执行顺序: 取值 → 调用过滤函数 → 输出结果。


💬 类比:

顾客点单 → 厨房取原料(数据) → 送到"加工机(过滤器)" → 处理成美观成品 → 端给顾客。


🎯 八、小可爱记忆口诀 🧃

🧩 过滤器像出杯机, 🍹 数据加料更美丽。 💬 不改原料动包装, ✨ 出杯展示更大方!


🌈 九、总结表格

知识点 一句话记忆 奶茶铺类比
Vue.filter 定义全局过滤器 总店统一出杯机
filters:{} 局部过滤器 分店专属出杯机
管道符" " 串联多个过滤器
原理 本质是函数调用 数据传入加工机输出
特点 不改数据,只改显示 不改奶茶,只改包装

💬 一句话收尾:

"Vue 的过滤器就是数据的化妆师💄------ 不动本体,只负责让它更漂亮地出现在顾客眼前。"

🌟 一、过滤器 Filter 的实战与原理(第 116--117 页)


前几页我们说过过滤器是"数据的化妆师 💄", 那这里展示的代码是 👉 "高级定制的化妆流程"


🧠 1️⃣ 全局过滤器与局部过滤器复习

ini 复制代码
Vue.filter('capitalize', function(value) {
  if (!value) return '';
  value = value.toString();
  return value.charAt(0).toUpperCase() + value.slice(1);
});

💬 用法:

css 复制代码
<p>{{ name | capitalize }}</p>

📤 输出:

复制代码
MilkTea → MilkTea

就像奶茶杯名字自动加大写首字母贴纸,「更显高级感~」😎


🧠 2️⃣ 多层过滤器链式调用

css 复制代码
<p>{{ money | currency | upper }}</p>

执行顺序是:

  1. 先经过"货币过滤器";
  2. 再经过"大写过滤器"。

💬 比喻:

奶茶出杯流程:先装杯(currency),再贴大写标签(upper)。

💡 每经过一个过滤器,数据就"更精致一点"。


🧩 3️⃣ Filter 原理解析

当你写:

scss 复制代码
{{ msg | capitalize }}

Vue 编译后会变成:

objectivec 复制代码
_this._f('capitalize')(msg)

也就是:

  • _f 代表 "找到过滤器函数";
  • 然后把 msg 这个值交给它加工。

💬 比喻:

顾客下单时,Vue 把原料交给"过滤机"处理,再端给前台。


🧋 小可爱记忆口诀:

🧃 过滤器三件事:

  1. 不改原料,只改外观。
  2. 多级叠加,层层修饰。
  3. 本质是函数,加工展示。

🍓 二、插槽 Slot(第 118--120 页)


这部分是超级重要的 Vue 知识点,几乎每个面试都会问:

"你知道 slot 是什么吗?它和组件通信有什么关系?"


🧋 一、Slot 是什么(第 118 页)


📘 定义:

Slot(插槽) 是 Vue 中用于 组件内容分发 的机制。 它让你在使用组件时,可以往组件内部塞点"自定义内容"。


💬 类比解释:

想象 Vue 组件是一台「游戏机 🎮」, Slot 就是上面那个「卡带插槽 🎮🕹️」。

你可以:

  • 把"格斗游戏卡带"插进去(显示 A 内容);
  • 换成"赛车游戏卡带"又能显示别的内容。

组件本身不变,内容可以换, 👉 这就是 Slot 的魅力!


🍰 二、Slot 的使用(第 119 页)


💻 示例:

1️⃣ 子组件 child.vue
xml 复制代码
<template>
  <div class="card">
    <slot></slot> <!-- 插槽位置 -->
  </div>
</template>
2️⃣ 父组件 parent.vue
xml 复制代码
<child>
  <h3>我是从父组件塞进来的内容</h3>
</child>

📤 页面效果:

xml 复制代码
<div class="card">
  <h3>我是从父组件塞进来的内容</h3>
</div>

🧋 生活比喻:

  • 子组件 <child> 是"奶茶杯模具";

  • <slot> 是留好的"倒料口";

  • 父组件往里倒不同的内容:

    • 倒珍珠 → 珍珠奶茶
    • 倒红豆 → 红豆奶茶

💡 Slot = 「奶茶机的加料口」!


🍹 三、命名插槽(第 120 页)


当一个组件有多个插槽时,可以给它们取名字:

子组件
xml 复制代码
<template>
  <header><slot name="header"></slot></header>
  <main><slot></slot></main>
  <footer><slot name="footer"></slot></footer>
</template>
父组件
xml 复制代码
<child>
  <template v-slot:header>👑 顶部标题</template>
  <p>🍹 中间主要内容</p>
  <template v-slot:footer>📞 底部版权</template>
</child>

📤 输出结果:

css 复制代码
<header>👑 顶部标题</header>
<main>🍹 中间主要内容</main>
<footer>📞 底部版权</footer>

🧋 类比说明:

想象这是 "三层奶茶机"

  • 第一层加奶盖(header),
  • 第二层装茶底(default),
  • 第三层加珍珠(footer)。

不同层的 slot 有不同名字, Vue 会自动帮你把"料"塞到对应层。😋


🍇 四、作用域插槽(scoped slot)


这是 Slot 的高级用法------ 让父组件拿到子组件内部的数据来渲染。


💻 示例:

子组件
xml 复制代码
<template>
  <slot :info="userInfo"></slot>
</template>
​
<script>
export default {
  data() {
    return { userInfo: { name: '小可爱', age: 18 } };
  }
}
</script>
父组件
xml 复制代码
<child v-slot="scope">
  <p>姓名:{{ scope.info.name }}</p>
  <p>年龄:{{ scope.info.age }}</p>
</child>

📤 输出:

复制代码
姓名:小可爱
年龄:18

🧋 类比解释:

这就像奶茶机不仅能倒料, 还能告诉前台"现在配方是什么"。

前台可以根据机器的数据(比如糖度、温度)决定展示内容。

💡 这就是 "作用域插槽"------ 把子组件的数据传递给父组件模板使用。


🍰 五、插槽的分类总结

类型 关键字 类比 说明
默认插槽 <slot> 奶茶加料口 塞内容
具名插槽 name="header" 三层奶茶机 按层分类
作用域插槽 v-slot="data" 智能奶茶机 能传数据给前台

🧠 六、小可爱记忆口诀 💡

🎮 组件是游戏机, 💽 slot 是卡带区; 🍹 内容自己塞, 🧠 还能传数据!


🎯 七、整体一句话总结

Slot 是 Vue 中 "组件的可插拔内容系统", 就像奶茶机的"加料口"或游戏机的"卡带槽", 你可以往组件里"塞"不同的内容,还能带参数传数据。


💡 一图记忆:

概念 功能 奶茶铺比喻
Filter 格式化显示 出杯机修饰标签
Slot 塞内容、传数据 奶茶机加料口
Scoped Slot 带参数插槽 智能加料机,会说配方
相关推荐
liangshanbo12151 天前
写好 React useEffect 的终极指南
前端·javascript·react.js
哆啦A梦15881 天前
搜索页面布局
前端·vue.js·node.js
_院长大人_1 天前
el-table-column show-overflow-tooltip 只能显示纯文本,无法渲染 <p> 标签
前端·javascript·vue.js
哆啦A梦15881 天前
axios 的二次封装
前端·vue.js·node.js
阿珊和她的猫1 天前
深入理解与手写发布订阅模式
开发语言·前端·javascript·vue.js·ecmascript·状态模式
yinuo1 天前
一行 CSS 就能搞定!用 writing-mode 轻松实现文字竖排
前端
snow@li1 天前
html5:拖放 / demo / 拖放事件(Drag Events)/ DataTransfer 对象方法
前端·html·拖放
浪裡遊1 天前
Nivo图表库全面指南:配置与用法详解
前端·javascript·react.js·node.js·php
漂流瓶jz1 天前
快速定位源码问题:SourceMap的生成/使用/文件格式与历史
前端·javascript·前端工程化
samroom1 天前
iframe实战:跨域通信与安全隔离
前端·安全