🧭 一、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 的作用:
- 当数据变 → 视图自动变(比如后厨改了糖度,前台菜单显示也更新)
- 当用户操作视图 → 数据也自动更新(顾客在点单界面选了"去冰",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-model
、v-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 页)
props
------ 父传子$emit
------ 子传父ref
------ 父访问子实例EventBus
------ 兄弟传递消息parent
或root
------ 访问父或根组件attrs
与listeners
------ 透传属性/事件provide
与inject
------ 跨层级传值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页:attrs
、listeners
、provide
、inject
🧩 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-if
和 v-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-if
与v-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-if
、v-else
、v-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)是否是同一个。
-
"同一个"的标准主要是:
- key 相同;
- 标签 tag 相同;
- 都是注释节点;
- 数据结构一致。
👉 如果满足这些条件,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!"
- Vue 自动调用 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]
表示组件注入该逻辑;- 每个组件都自动拥有
isShowing
与toggleShow()
。
🍵生活比喻:
"就像所有门店都用'同一份标准开店手册'📖。 不用重复教:'怎么开灯、怎么关门', 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 会:
- 先融合所有旧配方(递归 mixins);
- 逐项比对原料(key);
- 如果总部有就更新,没有就新增;
- 最终生成一份最新的'统一菜单'(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 的"队列型"合并规则指的是: 👉 生命周期钩子(created
、mounted
等)和 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
:系统键; -
你也可以自己定义键码:
iniVue.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 不会立刻去改页面,而是:
- 把这次修改放进"任务队列";
- 等本轮事件循环结束后再统一更新 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
})
}
}
📘关键点解释:
- 所有回调函数都压入
callbacks
队列; - 设置一个
pending
标志,避免重复触发; - 调用
timerFunc
异步执行(稍后会讲); - 如果没传回调,则返回一个 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 会自动根据运行环境选择"最快的异步执行方式":
- Promise.then(微任务,速度最快 🚀);
- MutationObserver(DOM 变化监听);
- setImmediate;
- 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 更新)再一起出单。 这样不会每接一个单就去煮奶茶(节省性能)!
🧠 小结(这页总结三点):
- 把函数丢进任务队列(callbacks)
- 再把执行动作放到"微任务"或"宏任务"中
- 到时间后批量执行所有函数
🍵 第 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 页 --- 详细解读 + 结论
结论里讲了三点非常关键的记忆点:
beforeCreate
阶段 → 数据(data、props)还没初始化好 🔹 比如店铺刚租下,还没进货。created
阶段 → 数据已经准备好,但页面还没生成 🔹 材料进货了,但奶茶还没倒进杯子(DOM 还没挂载)。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 里的变量名跟
props
或methods
重复,会警告你。
👉 "不能既是原料名(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 根实例挂在
body
或html
上。
🧋第 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)
}
}
📖 通俗解释: 这段代码干的活很像👇
"前端奶茶店的出品员"。
他负责:
- 第一次制作奶茶(
patch(null, vnode)
)→ 做出第一杯; - 之后如果顾客要求"加珍珠、少冰" → 他会对比旧的配方(旧
vnode
)和新的要求(新vnode
),只改动必要的部分,而不是重做整杯(这就是 diff 更新!);
所以 _update
就是把虚拟 DOM(Vue 的"奶茶配方")变成真实 DOM(网页上的"成品奶茶")的关键工序 🍹
🧭 第 82 页:Vue 的渲染流程总结
这一页总结了整个 Vue 从创建实例到页面渲染的流程 🧱:
- 挂载过程由 mountComponent 完成
- 定义
updateComponent
→ 负责组件更新(相当于"再制作"逻辑) - 执行
render
→ 把模板转成虚拟 DOM - 调用
_update
→ 把虚拟 DOM 渲染成真实 DOM
👉 简单记法:
new Vue → render → update → patch
生活比喻:
像做奶茶:
- init(准备食材)
- render(根据配方调配)
- update(调整味道)
- patch(最后封杯出品)
⚙️ 第 83 页:diff 算法是干嘛的?
🧠 diff 就是 Vue 的"聪明比对器",用来比较两杯奶茶的不同。
✨ diff 是什么?
它是一种高效的"树节点对比算法",用来找出新旧虚拟 DOM 的不同地方,只更新必要的部分。
🔍 特点:
- 同层对比,不会跨层(就像对比同一层奶茶配料,不会跑去换整杯容器)
- 从两边往中间比(像夹心饼干,两边同时吃 😋)
🔄 第 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
当数据发生变化时:
- Vue 内部的
set()
方法会调用Dep.notify()
; - 通知所有依赖的 Watcher;
- Watcher 再调用
patch
; 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 奶茶铺的店长。 每天要根据"新菜单"更新前台展示。
-
旧菜单:
markdown1. 珍珠奶茶 2. 红豆奶茶 3. 椰果奶茶
-
新菜单:
markdown1. 珍珠奶茶 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 比对过程如下:
- 比较头:A vs D ❌
- 比较尾:E vs B ❌
- 发现 D 在新菜单最前面 → 把 D 移前
- 继续比较 C、E,一样的就跳过
- 剩下的 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 页核心主题
- Vue 项目中代理是如何解决跨域的?
- 有哪些自定义指令?它们和函数/组件的区别?
🍓 一、什么是跨域?(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-if
、v-for
、v-model
、v-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>
📤 执行顺序:
从左到右,一个过滤结果再传给下一个。
💬 类比:
奶茶制作流程:
- 加奶;
- 加糖;
- 打上 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>
执行顺序是:
- 先经过"货币过滤器";
- 再经过"大写过滤器"。
💬 比喻:
奶茶出杯流程:先装杯(currency),再贴大写标签(upper)。
💡 每经过一个过滤器,数据就"更精致一点"。
🧩 3️⃣ Filter 原理解析
当你写:
scss
{{ msg | capitalize }}
Vue 编译后会变成:
objectivec
_this._f('capitalize')(msg)
也就是:
_f
代表 "找到过滤器函数";- 然后把
msg
这个值交给它加工。
💬 比喻:
顾客下单时,Vue 把原料交给"过滤机"处理,再端给前台。
🧋 小可爱记忆口诀:
🧃 过滤器三件事:
- 不改原料,只改外观。
- 多级叠加,层层修饰。
- 本质是函数,加工展示。
🍓 二、插槽 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 | 带参数插槽 | 智能加料机,会说配方 |