响应式
Object.defineProperty
Object监听
在计算属性中直接修改可以吗?不可以会发出警告⚠,通过setter修改会触发响应式
Array
push、pop、shift、unshift、
splice
、sort、reverse
虚拟DOM与Diff算法
VNode
在Vue.js中,VNode(虚拟节点)是Vue用于描述DOM节点的轻量级JavaScript对象
。VNode是Vue实现高效渲染和更新的核心机制之一。通过VNode,Vue可以在内存中进行DOM操作,避免直接操作真实DOM,从而提高性能。
VNode的基本结构
VNode是一个普通的JavaScript对象
,通常包含以下属性:
tag
: 标签名,如div
、span
等。data
: 包含节点属性、事件监听器等信息的对象。children
: 子节点数组,每个子节点也是一个VNode。text
: 文本节点的内容。elm
: 对应的真实DOM节点。key
: 用于优化列表渲染的唯一标识符。
{
tag: 'div',
data: {
attrs: {
id: 'app'
},
on: {
click: handleClick
}
},
children: [
{
tag: 'span',
text: 'Hello World'
}
],
elm: null,
key: 'uniqueKey'
}
VNode的创建
在Vue中,VNode通常通过createElement
函数(也称为h
函数)创建。createElement
函数接收标签名、属性对象和子节点作为参数,返回一个VNode对象。
import { createElement } from 'vue';
const vnode = createElement('div', {
attrs: {
id: 'app'
},
on: {
click: handleClick
}
}, [
createElement('span', null, 'Hello World')
]);
VNode的渲染
VNode最终会被渲染为真实DOM节点
。Vue通过patch
函数将VNode与真实DOM进行比较,并根据差异进行更新
。patch
函数会递归地处理VNode的children
,确保整个DOM树与VNode树保持一致。
function patch(oldVnode, vnode) {
// 比较新旧VNode,更新真实DOM
}
VNode的优化
Vue通过VNode的key
属性来优化列表渲染。当列表中的元素顺序发生变化时,Vue会根据key
来识别哪些元素可以复用,从而减少不必要的DOM操作。
const list = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' }
];
const vnodes = list.map(item => createElement('li', { key: item.id }, item.text));
VNode的生命周期
VNode在创建、更新和销毁时会触发相应的生命周期钩子。这些钩子可以用于执行一些自定义逻辑,如数据获取、事件绑定等。
const vnode = createElement('div', {
hook: {
insert(vnode) {
console.log('VNode inserted into DOM');
},
update(oldVnode, vnode) {
console.log('VNode updated');
},
destroy(vnode) {
console.log('VNode destroyed');
}
}
});
通过理解VNode的结构和生命周期,可以更好地掌握Vue的渲染机制,并编写出更高效的Vue应用。
Diff算法
Vue2&Vue3 Diff算法
Vue中Diff算法比较两棵虚拟 DOM 树:旧的 VNode 和新的 VNode
。通过这种方式,Vue 能够找出发生变化的部分,并仅对这些部分进行真实 DOM 的更新,从而提高性能。
patch
同层比较
,不是同类标签直接替换
,同类标签且新旧虚拟节点相同就直接返回,不同的话就:
文本节点替换
:如果VNode是文本节点,如果不同则把oldVNode里文本替换
跟VNode的文本一样新增
子节点:新的VNode中有而旧的oldVNode中没有子节点,就在旧的oldVNode中新增。删除
子节点:新的VNode中没有而旧的oldVNode中有子节点,就从旧的oldVNode中删除。
更新
节点:新的VNode和旧的oldVNode中都有子节点,就以新的VNode为准,更新旧的oldVNode。更新
流程(updateChildren
):
双端比较
+key复用
Vue3中BlockTree如何理解?
模板编译
在标签中除了写一些原生HTML的标签,我们还会写一些变量插值
,或者写一些Vue指令,如v-on、v-if等。而这些东西都是在原生HTML语法中不存在的
,不被接受的。
Vue会把用户在标签中写的类似于原生HTML的内容进行编译,把原生HTML的内容找出来,再把非原生HTML找出来,经过一系列的逻辑处理生成渲染函数
,而render函数会将模板内容生成对应的VNode。
AST
模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树AST;
优化阶段:遍历AST,找出其中的静态节点,并打上标记;
代码生成阶段:将AST转换成渲染函数;
模板解析
模板解析其实就是根据被解析内容的特点使用正则等
方式将有效信息解析提取出来,根据解析内容的不同分为HTML解析器,文本解析器和过滤器解析器
。而文本信息与过滤器信息又存在于HTML标签中,所以在解析器主线函数parse中先调用HTML解析器
parseHTML 函数对模板字符串进行解析,如果在解析过程中遇到文本或过滤器信息则再调用相应的解析器进行解析
,最终完成对整个模板字符串的解析。
JSON.parse
HTML解析器
模板编译阶段主线函数parse会将HTML模板字符串转化成AST,而parseHTML是用来解析模板字符串的
,把模板字符串中不同的内容出来之后,4个钩子函数
把提取出来的内容生成对应的AST
。
一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点
,最终完成将整个模板字符串转化成AST
,4个钩子函数
把提取出来的内容生成对应的AST
:
1. 当解析到开始标签时调用start函数生元素类型
的AST节点(createASTElement)
2. 当解析到结束标签时调用end函数;
3. 当解析到文本时调用chars函数
生成文本类型
的AST节点(判断是否为动态文本,动-
>创建动态文本类型的AST节点
,静
->创建纯静态文本类型的AST节点
)
4. 解析到注释时调用comment函数生成注释类型的AST节点
;
解析器内维护了一个栈
,用来保证构建的AST节点层级与真正DOM层级一致
,栈还可以用于检测模板字符串中是否有未正确闭合
的标签
文本解析器
文本解析器的作用就是将HTML解析器解析得到的文本内容进行二次解析
,解析文本内容中是否包含变量,如果包含变量,则将变量提取出来进行加工
,为后续生产render函数做准备
。主要做了三件事:判断传入的文本是否包含变量、构造expression和构造tokens
优化阶段
静态标记
主要做了一下两件事:
- 在AST中找出所有静态节点并打上标记;
- 在AST中找出所有静态根节点并打上标记;
从根节点开始,先标记根节点是否为静态节点
,然后看根节点如果是元素节点,那么就去向下递归它的子节点
,子节点如果还有子节点那就继续向下递归
,直到标记完所有节点
什么才能算静态节点?
v-pre
slot
插槽(Slot)是 Vue.js 中用于组件内容分发的一种机制。它允许父组件向子组件传递模板片段
,子组件可以在其模板中使用 标签来接收这些内容。插槽的主要作用是增强组件的灵活性和可复用性
。
作用域插槽允许子组件向父组件传递数据,父组件可以根据这些数据自定义渲染内容
。子组件在 <slot> 标签上绑定数据
,父组件通过 v-slot 指令
接收这些数据。
template标签编译
什么才算静态根节点?
代码生成阶段
代码生成通俗讲就是生成render函数字符串
,Vue自己根据模板内容生成render函数
的过程,根据模板对应的抽象语法树AST生成一个函数,通过调用这个函数就可以得到模板对应的虚拟DOM。
根据AST生成render函数
深度遍历AST,
滴滴二面
生成render函数的过程其实就是一个递归的过程,从顶向下依次递归AST中的每一个节点,根据不同的AST节点类型创建不同的VNode类型.
调用generate
函数并传入优化后得到的ast
,在generate函数内部先判断ast是否为空,不为空则调用genElement(ast, state)函数创建VNode
,为空则创建一个空的元素型div的VNode
.然后将得到的结果用with(this){return ${code}}包裹返回.
genElement函数逻辑很清晰,就是根据当前 AST 元素节点属性的不同从而执行不同的代码生成函数
。虽然元素节点属性的情况有很多种,但是最后真正创建出来的VNode无非就三种
,分别是元素节点,文本节点,注释节点
。
JSON.stringify()
生命周期
初始化阶段(new Vue())
Vue 的初始化阶段是实例从创建到挂载前的关键过程,涉及生命周期函数、事件、依赖注入(
injections)和状态(
state)
的初始化。以下是详细解析:
一、生命周期函数的初始化
在 new Vue()
实例化时,Vue 内部通过 _init
方法启动初始化流程,依次执行以下步骤:
1. 合并配置项
根据 new Vue(options)
传入的选项,合并全局配置
与实例配置,形成最终的 $options
对象。
2. 初始化生命周期钩子
• beforeCreate
:在数据观测(data
/props
)和事件初始化前调用,此时无法访问实例的响应式数据和方法。
• created
:数据观测完成,data
、methods
、computed
等已初始化,但 DOM 未挂载。
二、事件系统的初始化
通过 initEvents(vm)
处理:
1. 事件监听器注册
• 初始化实例的 _events
对象,存储父组件通过 v-on
绑定的事件。
• 调用 updateComponentListeners
更新事件监听,例如 $on
、$emit
等方法的绑定。
2. 原生事件与自定义事件分离
• 原生 DOM 事件(如 click
)在挂载阶段通过 addEventListener
处理。
• 自定义事件(如组件通信)通过 Vue 的事件中心管理
。
浏览器原生事件是由父组件处理
,而自定义事件是在子组件初始化的时候由父组件传给子组件
,再由子组件注册到实例的事件系统
中
三、依赖注入(Injections)的初始化
通过 initInjections(vm)
和 initProvide(vm)
处理:
1. 解析 inject
依赖
• 遍历 inject
选项,向上查找
父组件通过 provide
提供的依赖。
• 使用 resolveInject
函数解析依赖链,确保层级穿透
。
2. 响应式处理
• inject
的值通过 defineReactive
设置为非响应式(toggleObserving(false)
),避免子组件意外修改父级数据
。
• provide
的初始化在 data
之后,允许依赖 data
中的属性。
四、状态(State)的初始化
通过 initState(vm)
处理,依次初始化以下选项:
1. props
• 校验类型并设置响应式,通过 defineReactive
将每个 prop
绑定到实例的 _props
对象。
• 代理 props
到实例,支持通过 this.propName
直接访问。
2. methods
• 将方法直接绑定到实例,并通过 bind
确保 this
指向当前实例。
3. data
• 合并 data
函数返回的对象,并通过 observe
将其转为响应式数据。
• 代理 data
到实例的 _data
属性,支持通过 this.key
访问。
4. computed
• 为每个计算属性创建 Watcher
,并定义 getter
/setter
,实现惰性求值和缓存。
5. watch
• 遍历 watch
选项,为每个属性创建 Watcher
,支持深度监听和立即执行。
五、初始化顺序与关键点
1. 顺序逻辑
beforeCreate → initInjections → initState → initProvide → created
• inject
初始化在 data
前,确保 data
中可使用注入的值。
• provide
初始化在 data
后,允许依赖 data
的响应式数据。
2. 响应式系统的启动
• 在 initState
中,data
和 props
通过 observe
转为响应式对象,建立依赖收集和派发更新的机制。
3. 生命周期钩子的触发时机
• beforeCreate
在 inject
和 state
初始化前调用,适合插件初始化。
• created
在 state
初始化后调用,适合数据请求和事件监听。
模板编译阶段
挂载阶段
销毁阶段
在理清实例方法、全局API之前先理清一下VueComponent、vc、Vue、vm的关系:
实例方法
发布订阅
// 创建一个简单的发布订阅者管理类
class EventEmitter {
constructor() {
this.events = {}; // 用于存储事件的字典,键为事件名,值为订阅者数组
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = []; // 如果该事件尚未被订阅,则初始化一个空数组
}
this.events[eventName].push(callback); // 将回调函数添加到订阅者数组中
return this; // 支持链式调用
}
// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
// 移除指定的回调函数
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
// 如果订阅者数组为空,则删除该事件
if (this.events[eventName].length === 0) {
delete this.events[eventName];
}
}
return this; // 支持链式调用
}
// 发布事件
emit(eventName, ...args) {
if (this.events[eventName]) {
// 遍历订阅者数组,并执行每个回调函数
this.events[eventName].forEach(callback => {
callback.apply(this, args);
});
}
return this; // 支持链式调用
}
// 可选:监听一次后自动取消订阅
once(eventName, callback) {
const onceCallback = (...args) => {
callback.apply(this, args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
return this; // 支持链式调用
}
}
// 使用示例
const eventBus = new EventEmitter();
// 订阅者1
function subscriber1(price) {
console.log('订阅者1收到消息:当前价格已降至' + price + '元');
}
// 订阅者2
function subscriber2(price) {
console.log('订阅者2也收到消息:价格更新为' + price + '元');
}
// 订阅事件
eventBus.on('priceUpdate', subscriber1);
eventBus.on('priceUpdate', subscriber2);
// 使用once方法监听一次
eventBus.once('specialOffer', (offer) => {
console.log('只接收一次的特别优惠:' + offer);
});
// 发布事件
eventBus.emit('priceUpdate', 99); // 订阅者1和订阅者2都会收到消息
eventBus.emit('specialOffer', '买一赠一'); // 只有一个订阅者会收到这个特别优惠的消息
// 取消订阅
eventBus.off('priceUpdate', subscriber1);
eventBus.emit('priceUpdate', 88); // 只有订阅者2会收到消息
过滤器(弃用)
指令篇
内置组件
keep-alive
核心作用:缓存非活动组件状态,避免重复渲染
- 基础用法:
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
- 特性说明:
- 通过
include
/exclude
属性指定缓存规则:<keep-alive :include="['HomePage', /^Base/]"> <router-view /> </keep-alive>
- 使用
max
属性限制最大缓存数(LRU策略) - 激活/停用时会触发特有的生命周期钩子:
activated() { // 组件激活时执行(首次创建不会触发) }, deactivated() { // 组件停用时执行 }
- 典型场景:
- 标签页切换保持状态
- 表单内容临时保存
- 复杂组件二次加载优化
LRU
class LRUCache {
constructor(capacity) {
this.cache = new Map();
this.capacity = capacity;
}
get(key) {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
put(key, val) {
this.cache.delete(key);
this.cache.set(key, val);
if (this.cache.size > this.capacity)
this.cache.delete(this.cache.keys().next().value);
}
}
Transition组件
核心作用:为元素/组件添加过渡动画效果
- 基本结构:
<transition name="fade">
<div v-if="show">内容</div>
</transition>
- 过渡类名(以name="fade"为例):
- 进入阶段:
.fade-enter-from
(初始状态).fade-enter-active
(激活状态).fade-enter-to
(结束状态)
- 离开阶段:
.fade-leave-from
.fade-leave-active
.fade-leave-to
- 动画示例:
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
- 高级配置:
- 使用
mode
属性控制过渡顺序:<transition mode="out-in"> <!-- 元素 --> </transition>
- 支持JavaScript钩子函数:
beforeEnter(el) {}, enter(el, done) {}, afterEnter(el) {}
三、组合使用示例
<router-view v-slot="{ Component }">
<transition name="slide">
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
注意事项:
- 缓存组件可能引发内存占用问题,需合理设置max值
- 过渡动画应尽量使用CSS3属性以获得最佳性能
- 被keep-alive缓存的组件不会触发
mounted
之后的销毁钩子 - 过渡组件支持与第三方动画库(如GSAP)配合使用
通过合理运用这两个组件,可以有效提升应用流畅度和用户交互体验。