Appearance
Vue 3.0
导航目录
Vue 2 和 Vue 3 的区别
1. 响应式系统
Vue 2:
- 使用
Object.defineProperty实现响应式 - 缺点:无法检测对象属性的动态添加/删除,需用
Vue.set/Vue.delete;对数组的索引修改和长度变化不敏感。
Vue 3:
- 基于 Proxy 重构响应式系统
- 优势:支持动态属性增删、数组索引修改;性能更高;减少边界情况处理。
2. Composition API
Vue 2:
- 使用 Options API(选项式)(
data,methods,computed等分离的选项) - 缺点:逻辑分散,大型组件维护困难。
Vue 3:
- 引入 Composition API(组合式)(
setup函数 + 响应式函数) - 优势:
- 逻辑复用更灵活(类似 React Hooks);
- 代码按功能组织,而非分散到选项中;
- 更好的 TypeScript 支持。
3. 性能优化
Vue 3 改进:
- Tree-shaking 支持:核心库体积更小(约 10KB gzipped),未使用的功能不打包。
- 虚拟 DOM 重写:
- 编译时优化:标记静态节点,跳过 diff 过程;
- 动态节点标记(Patch Flags):仅追踪动态内容。
- SSR 优化:提升服务端渲染性能。
4. 新特性
- Fragment(片段):Vue 3 支持多根节点组件,而 Vue 2 要求组件必须单根节点。
- Teleport(传送门):将组件渲染到 DOM 任意位置(如模态框、通知)。
- Suspense(异步组件):优雅处理异步组件加载状态(显示占位内容)。
5. TypeScript 支持
Vue 2:通过 vue-class-component 支持,类型推断较弱。 Vue 3:完全用 TypeScript 重写,提供更完善的类型定义。
生命周期
Vue 3 的生命周期钩子与 Vue 2 相比有所变化,主要是通过组合式 API 调用:
js
Vue 2 -------> Vue 3
beforeCreate --------> setup(()=>{})
created --------> setup(()=>{})
beforeMount --------> onBeforeMount(()=>{})
mounted --------> onMounted(()=>{})
beforeUpdate --------> onBeforeUpdate(()=>{})
updated --------> onUpdated(()=>{})
beforeDestroy --------> onBeforeUnmount(()=>{})
destroyed --------> onUnmounted(()=>{})
activated --------> onActivated(()=>{})
deactivated --------> onDeactivated(()=>{})
errorCaptured --------> onErrorCaptured(()=>{})Vue3 渲染流程
Vue 3 的渲染过程可以分为两大核心部分:编译时(Compile-time) 和 运行时(Runtime)。
编译时 (Compile-Time)
编译时的工作主要发生在构建阶段(例如使用 vue-loader 或 Vite),它的任务是将开发者编写的模板(<template>)编译成渲染函数(render function)。
流程步骤如下:
解析 (Parse)
- 输入:原始的 HTML-like 模板字符串。
- 过程:解析器将模板字符串解析成一个树状结构,这个结构称为抽象语法树 (AST - Abstract Syntax Tree)。AST 的每个节点都代表了模板中的元素、属性、文本、指令等。
- 输出:模板 AST。
转换 (Transform)
- 输入:模板 AST。
- 过程:这是一个非常关键的优化步骤。Vue 的编译器会遍历 AST 并对其进行转换和优化。例如:
- 静态提升 (Static Hoisting):识别出永远不会改变的静态节点和静态属性,并将其提升到渲染函数之外。这样在每次重渲染时,Vue 可以直接复用旧的 VNode,完全跳过 Diff 过程。
- 补丁标志 (Patch Flags):为动态节点标记其需要更新的类型(如
TEXT,CLASS,PROPS等)。在运行时,Vue 可以根据这些标志进行更精准的 Diff,只比较需要变化的部分,极大提升性能。 - 树结构优化 (Tree Flattening):将动态子节点编译到一个扁平数组中,大大减少了运行时需要遍历的 VNode 数量。
- 输出:优化后的、携带了丰富元信息的 AST。
生成 (Generate)
输入:优化后的 AST。
过程:代码生成器将 AST 递归地转换为 JavaScript 代码字符串,这个字符串就是一个渲染函数。
输出:渲染函数的代码字符串。例如,一个简单的模板
<div>Hello</div>会被生成类似如下的代码:javascriptimport { createElementVNode as _createElementVNode } from "vue"; export function render(_ctx, _cache) { return _createElementVNode("div", null, "Hello"); }
运行时 (Runtime)
运行时是 Vue 在浏览器(或其它环境)中实际执行的过程。它基于 Vue 的核心库,负责执行渲染函数、处理响应式数据、更新 DOM 等。
流程步骤如下:
创建应用实例与响应式系统
- 使用
createApp创建应用实例。 - 组件中的
data、computed、props等会被 Vue 的响应式系统(reactive,ref)包裹,变成响应式对象。
- 使用
执行渲染器 (Render Effect)
- Vue 将一个组件的渲染函数包裹在一个副作用函数 (Effect) 中。这个特殊的 Effect 被称为渲染器。
- 依赖追踪 (Dependency Tracking):当首次执行或后续重执行渲染函数时,会访问响应式数据中的属性。这会触发属性的
get操作,响应式系统会精确地记录下“这个渲染 Effect 依赖了当前这个属性”。
生成虚拟 DOM (VNode)
- 执行渲染函数,它会调用一系列
createElementVNode,createTextVNode等运行时工具函数。 - 这些函数会根据当前的组件状态和 Props,创建并返回一个虚拟 DOM (VNode) 树。VNode 是一个普通的 JavaScript 对象,它轻量地描述了真实 DOM 应该长什么样。
- 执行渲染函数,它会调用一系列
挂载 / 打补丁 (Mount / Patch)
- 首次渲染 (Mount):
- 将上一步生成的 VNode 树传入
patch函数。 patch函数会递归地遍历 VNode 树,并根据 VNode 的描述,调用浏览器的 DOM API(如document.createElement,appendChild)来创建真实的 DOM 节点,并挂载到页面上指定的容器中(例如app.mount('#app'))。
- 将上一步生成的 VNode 树传入
- 更新渲染 (Update):
- 当组件依赖的响应式数据发生变化时,响应式系统会触发它收集的所有 Effect(包括渲染 Effect)重新执行。
- 渲染 Effect 再次执行,用新的数据生成一个新的 VNode 树。
- 将新的 VNode 树和旧的 VNode 树一起传入
patch函数。 patch函数会使用高效的 Diff 算法 对比两棵树的差异。得益于编译时提供的Patch Flags和静态提升,Diff 过程非常快速和精准。- 找出差异后,
patch函数只会调用必要的 DOM API,最小化地更新真实 DOM。这就是 Vue 性能高的关键所在。
- 首次渲染 (Mount):
卸载 (Unmount)
- 当组件需要被销毁时(例如
v-if为 false),Vue 会再次调用patch函数,但这次会传入一个空的 VNode。patch函数会递归地触发组件实例和 DOM 元素的卸载钩子,并移除真实的 DOM 节点。
- 当组件需要被销毁时(例如
reactive 数据响应式处理
reactive 是利用 ES6 的 Proxy 加上发布-订阅模式来处理响应式数据的。主要流程如下:
- reactive 在初始化时,首先判断目标是否是对象,不是对象就直接返回
- 如果已经被
reactive代理过了,直接返回代理对象 - 如果该对象已经被缓存过,直接取缓存的结果返回
- 如果以上都未命中,创建 Proxy 代理对象,并且进行缓存
js
function createReactiveObject(target, isReadonly, baseHandlers, proxyMap) {
if (!isObject(target)) {
// 不是对象就直接跳过
return target;
}
// 如果已经被代理过了,直接返回
// reactive(reactive(obj))
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target;
}
// 如果某一个对象已经被代理过,下次在进行代理,直接取缓存的结果
// let obj = { name: "hulei" };
// let state = reactive( obj );
// reactive( obj );
const existingProxy = proxyMap.get(target); // 看目标是否被代理过
// 如果代理过,直接返回代理过的对象
if (existingProxy) {
return existingProxy;
}
// 创建代理对象
let proxy = new Proxy(target, baseHandlers);
proxyMap.set(target, proxy); // 存到缓存中
return proxy;
}取值/订阅 get
- reactive 在取值时,会触发 get 操作,执行
track让当前对象的属性去收集渲染的 effect - 如果取出的值是一个对象,需要继续走 reactive 做代理
设置/发布 set
- reactive 数据在发生变化时,会触发 set 操作,执行
trigger通知对应的 effect 更新视图 - 如果是数组比较特殊,因为数组在 push 操作时,会触发二次 set 拦截:
- 第一次会给新的索引位置新增一个值
- 第二次改变数组的 length 长度(会被屏蔽掉)
- 当数组在 push 操作的时候,会触发 set 操作,因为索引是新加的,没有依赖收集过,所以需要通过数组的 length 触发 effect 更新视图
Vue 2 vs Vue 3 代理对比
Vue 3 优势:
- 针对的是对象来进行劫持,不用改写原来的对象
- 懒递归:如果是嵌套对象,当取值的时候才会代理,提升性能
- 可以对不存在的属性进行获取,也会走 get 方法
- Proxy 原生支持数组,无需特殊处理
Vue 2 局限性:
- 针对的是属性劫持,改写了原来对象
- 一上来就递归处理所有属性,性能较差
- 对数组的索引修改和长度变化不敏感
ref(可以让一个普通值具备响应式的能力,如果是一个对象还是利用的 reactive)
ref 是一个类,在实例化时会存储原始的值,通过属性访问器 set、get 来实现响应式的能力。主要流程如下:
- ref 内部是通过属性访问器来实现的,在初始化时会通过 createRef 创建 Ref 实例,会把原始的值存储在当前实例上,返回当前的 Ref 实例对象
- 当在访问实例上的 value 属性时(xxx.value),会触发依赖收集,并且返回原始的值
- 当在给 value 赋值时,如果值不等同,会触发当前的依赖执行,并且赋值给原始的值
js
const convert = ( v ) => ( isObject( v ) ? reactive( v ) : v );
class RefImpl {
public _value; // 原始的值
public __v_isRef = true; // 表示他是一个ref
constructor( public rawValue, shallow ) {
// 如果不是浅的,并且是对象,走reactive代理拿到结果
this._value = shallow ? this.rawValue : convert( this.rawValue )
}
get value() {
track( this, TrackOpTypes.GET, "value" );
return this._value
}
set value( newValue ) {
if ( hasChanged( this.rawValue, newValue ) ) {
this.rawValue = newValue;
this._value = newValue;
trigger( this, TriggerOrTypes.UPDATE, "value", newValue, this.rawValue );
}
}
}
let createRef = ( value, shallow ) => {
return new RefImpl( value, shallow );
}
// 可以把普通值变成响应式数据
function ref( value ) {
return createRef( value, false );
}ref 和 reactive 的区别
ref:
- 可以让普通的值具备响应式的能力
- 通过属性访问器 get、set 拦截处理更新
- 需要通过
.value访问和修改值
reactive:
- 只能代理对象,并且使用的 ES6 的 Proxy
- 直接访问和修改属性,不需要
.value - 对嵌套对象会自动进行代理
toRef 作用
toRef 可以解析取出 reactive 某一个属性,并且不会失去响应式的能力(也是利用属性访问器)。主要特点:
- toRef 是一个类,在实例化时会存储原始的 target 和 key
- 当在取 state.value 时,触发 get 取原始的值
- 当在给 value 赋值时,会触发 set 给原始的值赋值
js
let state = reactive({ name: "hulei" });
// 对象直接解析会失去响应式
let { name } = state;
// 通过toRef会通过get、set代理的方式去访问原对象上的属性,不会失去响应式的特性
let userName = toRef(state, "name");
console.log(userName.value);toRefs
toRefs 底层是调用的 toRef,给每一个属性绑定一个 toRef,实现同时解构多个值而不失去响应式能力。
js
import { reactive, toRefs } from "vue";
export default {
setup() {
const user = reactive({
name: "hu.lei",
});
// 如果不通过toRefs会失去响应式的能力
return {
...toRefs(user),
};
},
};js
export function toRef(targat, key) {
return new ObjectRefImpl(targat, key);
}
class ObjectRefImpl {
public __v_isRef = true;
constructor(public targat, public key) {}
get value() {
return this.targat[this.key];
}
set value(value) {
this.targat[this.key] = value;
}
}
export function toRefs(targat) {
let res = isArray(targat) ? new Array(targat.length) : {};
for (const key in targat) {
res[key] = toRef(targat, key);
}
return res;
}vnode 中的 shapeFlag 位运算
1. 位运算的核心作用
位运算在 shapeFlag 属性中实现了 多类型标记的合并与快速判断。通过按位操作:
- 合并类型:使用
|(按位或)将父节点类型和子节点类型合并到一个整数中 - 判断类型:后续使用
&(按位与)快速检查节点类型
2. 具体实现分析
(1) 节点初始类型标记
javascript
let shapeFlag = isString(type)
? ShapeFlags.ELEMENT // 普通HTML元素
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT // 有状态组件
: 0;假设枚举值:
javascript
const ShapeFlags = {
ELEMENT: 1, // 二进制 0001
STATEFUL_COMPONENT: 1 << 1, // 二进制 0010 (左移1位)
TEXT_CHILDREN: 1 << 2, // 0100
ARRAY_CHILDREN: 1 << 3, // 1000
SLOTS_CHILDREN: 1 << 4, // 10000
};(2) 子节点类型合并
javascript
function normalizeChildren(vnode, children) {
let type = 0;
if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN; // 1000
} else if (isObject(children)) {
type = ShapeFlags.SLOTS_CHILDREN; // 10000
} else {
type = ShapeFlags.TEXT_CHILDREN; // 0100
}
// 关键位运算:合并父节点和子节点类型
vnode.shapeFlag = vnode.shapeFlag | type;
}(3) 位运算示例
假设创建组件节点(STATEFUL_COMPONENT)带数组子节点:
javascript
初始 shapeFlag = STATEFUL_COMPONENT = 0010 (二进制)
子节点类型 = ARRAY_CHILDREN = 1000 (二进制)
合并操作:0010 | 1000 = 1010 (十进制10)3. 位运算的优势
(1) 高效类型检查
后续代码可通过位运算快速判断类型:
javascript
// 检查是否是组件
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// 0010 & 1010 = 0010 (true)
}
// 检查是否有数组子节点
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 1000 & 1010 = 1000 (true)
}
// 检查是否是文本节点 (0100 & 1010 = 0000 false)effect(渲染的 effect、计算属性的 effect、watch effect)
渲染的 effect
- 组件在挂载的时候,会把渲染的控制权交给 effect
- 组件在渲染的时候,会调用 render 生成 Vnode,如果有取值操作,会触发 get 拦截,收集当前的 effect
- 当响应式数据发生变化时,会触发 set 通知 effect 更新视图
watch effect
- 在调用 watch 时,首先会进行取值,取值的时候会收集 watch effect
- 当响应式数据发生变化时,会触发 set 通知 watch effect 执行监听的回调
Vue watch、watchEffect 监听
watch 工作原理:
- watch 在初始化时,会执行
doWatch,构建一个 effect,将控制权交给 effect - effect 会立即执行
watch 函数获取到监听的值,在取值的时候会触发 get,收集当前的 effect - 当响应式数据发生变化时,会触发 set 通知 effect 执行自定义的
schedular 函数中的 watch 监听的回调 - effect 更新是异步的,一个值同步修改多次,只会执行一次更新,利用事件循环机制
watchEffect
- 没有监听的回调函数
- 默认一定会执行一次
- 值发生变化会直接执行 watch 函数
js
/**
*
* @param source 需要监听的数据(可能是函数,有可以是值)
* @param cb 数据变化后的回调
* @param options 配置项
* @returns
*/
// watch( () => count, ( newValue, oldValue ) => {
// }, {
// deep: true,
// immediate: true
// } );
function watch(source, cb, options = {}) {
return doWatch(source, cb, options);
}
// 默认会执行一次
function watchEffect(source) {
return doWatch(source, null, {});
}
function doWatch(source, cb, options) {
// 如果不是函数包装成函数,方便后续处理
let sourceHandler = source;
if (!isFunction(source)) {
sourceHandler = () => source;
}
let oldValue = ""; // 老值
// 自定义effect执行器
let schedular = () => {
if (cb) {
// 或者最新的值
let newValue = runner();
// 如果新老值不一样,执行回调
if (hasChanged(oldValue, newValue)) {
cb(newValue, oldValue);
oldValue = newValue;
}
} else {
// watchEffect没有回调,直接执行source
sourceHandler();
}
};
let runner = effect(sourceHandler, {
lazy: true,
schedular: () => {
// 如果数据同一时间多次变更,会触发effect多次执行,我们只需要执行一次即可
queueJob(schedular);
},
});
// 是否默认执行一次watch回调
if (options?.immediate) {
schedular();
}
// 或者监听的值,同时触发收集当前的effect,当监听的值发生变化时通知effect执行回调
oldValue = runner();
}Vue computed 计算属性
计算属性是一个类,具有缓存特性。主要工作原理如下:
- 在初始化时,首先会初始化缓存的状态
dirty,会创建一个lazy的 effect(不会立马执行),并且把 getter 控制权交给 effect - 当在取
state.value时,触发 get 拦截,如果没有缓存会调用 effect 获取的计算属性的值,响应式数据会通过track收集当前的计算属性 effect,同时会让计算实例收集渲染的 effect - 当响应式数据发生变化时,触发 set 通知 effect 执行自定义的
schedular,在schedular中取消缓存,通过执行trigger触发渲染的 effect 更新视图
js
function computed( handler ) {
let getter;
let setter;
if ( typeof handler === 'function' ) {
getter = handler
setter = () => {
console.warn( `computed not set` );
}
} else if ( isObject( handler ) ) {
getter = handler.set
setter = handler.get
} else {
return console.warn( `computed It must be a function or an object` );
}
return new ComputedRefImpl( getter, setter );
}
class ComputedRefImpl {
public _value;
public effect;
public _dirty = true;
constructor( public getter, public setter ) {
this.effect = effect( getter, {
lazy: true,// 在取值的时候,才执行getter
schedular: () => {
// 1、计算属性响应式数据发生改变后,取消缓存
this._dirty = true
// 2、通知渲染的effect更新视图
trigger( this, TriggerOrTypes.UPDATE, "value", this._value );
}
} )
}
get value() {
// 是否被缓存
if ( this._dirty ) {
// 用户在取值的时候,会通过effect调用计算属性的getter方法,让响应式数据去收集计算属性effect
this._value = this.effect()
// 缓存值
this._dirty = false
}
// 计算属性收集渲染的effect
track( this, TrackOpTypes.GET, "value" );
return this._value
}
set value( newValue ) {
this.setter( newValue )
}
}Vue Props 处理
Vue 3 中 Props 的处理流程如下:
- 子组件在创建 createVNode 时,如果父组件有传递数据,会将数据存储在 Vnode props 对象中
- 子组件在创建组件实例对象时,会把自己接受的 props 数据,存储在实例 propsOptions 对象中
- 子组件在渲染的时候,会调用
initProps,如果父组件传递的数据在子组件中有定义,会存储在组件实例 props 中 - 如果是未接收的属性,将全部存储在组件实例attrs上
js
const VueComponent = {
props: {
name: String,
age: Number,
},
// ...
};
let app = createApp({
render: () => {
return h(VueComponent, {
name: "hulei",
age: 18,
add: "上海",
});
},
});
app.mount("#app");
// h=> createVNode( type, propsOrChildren, children );
function createVNode( type, props, children = null ) {
const vnode = {
__v_isVnode: true,
type, // 此时是一个(VueComponent)组件
props, // 子组件在创建createVNode时,如果父组件有传递数据,会将数据存储在Vnode props对象中
children,
component: null,
el: null,
key: props?.key,
shapeFlag,
};
normalizeChildren( vnode, children );
return vnode;
};
const instance = {
uuid: uuid++,
__v_isVNode: true,
vnode, // 组件对应的虚拟节点
subTree: null,
type, // 组件对象
propsOptions: vnode.type.props || {}, // 子组件在创建组件实例对象时,会把自己接受的props数据,存储在 实例propsOptionse对象中
ctx: {} as any,
props: {}, // 组件的属性
attrs: {},
slots: {},
setupState: {},
isMounted: false,
exposed: {},
parent,
provides:parent ? parent.provides : Object.create(null)
};js
// 解析vnode将数据挂载到组件实例上
function setupComponent(instance) {
let { props, children } = instance.vnode;
// 如果子组件有接收props,将会实例在props上,否则全部在实例attrs上
initProps(instance, props);
}
const initProps = (instance, userProps) => {
const attrs = {};
const props = {};
const options = instance.propsOptions || {}; // 子组件上接受的props
// 父组件传递的props
if (userProps) {
for (let key in userProps) {
const value = userProps[key];
// 如果子组件有接受作为props,否则作为attrs
if (key in options) {
props[key] = value;
} else {
attrs[key] = value;
}
}
}
instance.attrs = attrs;
instance.props = reactive(props);
};emits 原理
emits 的作用主要是让子组件能够调用父组件的函数。工作原理如下:
- 子组件在初始化时,非 props 的接收的属性,将全部存储在子组件实例attrs上
- 当子组件触发 emit 函数时,通过函数名称在子组件实例attrs上去查找,如果有就执行父组件的函数(其实就是发布/订阅模式)
js
// 构建setup 下上文content
// setup(props, content) {
// const { attrs, slots, emit, expose } = content
// }
<Com onEvent="fn" />;
emit("event");
function createSetupContext(instance) {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: (eventName, ...args) => {
// event => onEvent
let bindName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
const handler = instance.attrs[bindName];
if (handler) {
let handlers = Array.isArray(handler) ? handler : [handler];
handlers.forEach((handler) => handler(...args));
}
},
expose: () => {},
};
}expose 实例原理
expose 的作用是将子组件的属性或者函数暴露出去,让父组件可以通过 ref 去调用。工作原理如下:
- 当子组件在调用 expose 时,其实就是把需要提供出去的属性,存储在自己实例 exposed 属性上
- 父组件可以通过 ref 获取到子组件的实例,从而可以调用子组件暴露的函数和属性
js
setup(props, { attrs, slots, emit,expose }) {
const sava = ()=>{
alert('expose')
}
// 暴露自己的函数
expose({
sava
})
}
// 构建setup 下上文content
// setup(props, content) {
// const { attrs, slots, emit, expose } = content
// }
function createSetupContext(instance) {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: (eventName, ...args) => {
let bindName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
const handler = instance.attrs[bindName];
if (handler) {
let handlers = Array.isArray(handler) ? handler : [handler];
handlers.forEach((handler) => handler(...args));
}
},
expose(exposed) {
// 主要用于ref ,通过ref获取组件的时候 在vue里只能获取到组件实例,但是在vue3中如果提供了
// exposed 则获取的就是exposed属性
instance.exposed = exposed;
},
};
}slot 插槽原理
Vue 3 中 slot 插槽的工作原理如下:
- 模板在编译的时候,会把 slot 插槽转换成一个对象
- 如果是匿名插槽,
default会作为对象的属性;如果是具名插槽,插槽的名字作为属性 - 在构建虚拟节点的时候,如果子节点是一个对象,会把子节点标记成插槽
- 在创建组件实例的时候,如果 Vnode 上有children 子节点会执行
initSlots(instance, children) - 如果 children 是插槽类型,会把 children 对象存储在组件实例的 slots 上,并且 slots 会传递到上下文对象中
- 让组件在渲染的时候,可以直接调用上下文中的 slots 进行渲染
js
let { reactive, createApp, h } = VueRuntimeDOM;
const VueComponent = {
setup(props, { slots }) {
return () => {
return h("div", [
h("div", slots.default()),
h("div", slots.header()),
h("div", slots.main()),
h("div", slots.footer()),
]);
};
},
};
let app = createApp({
render: () => {
return h(VueComponent, null, {
default: () => {
return h("a", "default");
},
header: () => {
return h("a", "hello");
},
main: () => {
return h("a", "vue");
},
footer: () => {
return h("a", "hulei");
},
});
},
});
app.mount("#app");js
// 虚拟节点的子节点处理
function normalizeChildren(vnode, children) {
let type = 0;
if (children == null) {
} else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN;
} else if (isObject(children)) {
// 子节点是啥槽
type = ShapeFlags.SLOTS_CHILDREN;
} else {
type = ShapeFlags.TEXT_CHILDREN;
}
vnode.shapeFlag = vnode.shapeFlag | type;
}js
// 如果有插槽,将插槽存储在组件实例slots上
const initSlots = (instance, children) => {
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
// 包含插槽 则说明children 是插槽
instance.slots = children; // 将用户的children 映射到实例上
}
};
// 解析vnode将数据挂载到组件实例上
function setupComponent(instance) {
let { props, children } = instance.vnode;
initSlots(instance, children); // 插槽的解析
}js
// 构建setup 下上文content
// setup(props, content) {
// const { attrs, slots, emit, expose } = content
// }
function createSetupContext(instance) {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: (eventName, ...args) => {
let bindName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
const handler = instance.attrs[bindName];
if (handler) {
let handlers = Array.isArray(handler) ? handler : [handler];
handlers.forEach((handler) => handler(...args));
}
},
expose(exposed) {
// 主要用于ref ,通过ref获取组件的时候 在vue里只能获取到组件实例,但是在vue3中如果提供了
// exposed 则获取的就是exposed属性
instance.exposed = exposed;
},
};
}provide or inject
provide 和 inject 是 Vue 3 中用于跨组件通信的机制,类似于内容提供者和消费者,任何下级的组件都可以消费父组件提供的 provide。工作原理如下:
- 当一个组件在创建组件实例时,会存储自己的父组件是谁,如果父组件有 provides,就会将父组件的 provides 存储在当前组件实例 provides 上,最终会形成类似一个原型链
- provide:当组件中调用 provide 时,会去父组件实例上查找 provides,如果有会进行合并,存储在当前组件实例的 provides 上
- inject:当组件中调用 inject 时,会去父组件实例上查找 provides,如果有返回父组件提供的,如果没有返回默认值
js
// 组件实例
const instance = {
// ....
parent, // 标记当前组件的父亲是谁
provides: parent ? parent.provides : Object.create(null),
};js
import { currentInstance as instance } from "./component";
/**
* 内容提供者
* @param key 消费的key
* @param value 消费的值
* @returns
*/
export function provide(key, value) {
if (!instance) return;
let parentProvides = instance.parent && instance.parent.provides; // 父组件的provides
let currentProvides = instance.provides; // 当前节点的provides
if (currentProvides === parentProvides) {
currentProvides = instance.provides = Object.create(parentProvides);
}
currentProvides[key] = value;
}
/**
* 消费者
* @param key 内容提供者key
* @param defaultValue 默认值
* @returns
*/
export function inject(key, defaultValue) {
if (!instance) return;
// 取组件提供的provide,如果没有返回默认值
const provides = instance.parent?.provides;
if (provides && key in provides) {
return provides[key];
} else {
return defaultValue;
}
}Vue diff
Vue 3 的 diff 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 单指针的方式进行比较。优化策略有 3 种:头头、尾尾、乱序。
比较流程:
- 头头对比:新老虚拟节点进行头头对比,如果 tag、key 相同,复用老的节点,比较儿子节点,头指针索引递增
- 尾尾对比:如果 tag、key 相同,新老尾指针递减
- 如果头指针超过其中有任何一个尾指针,证明有一方已经对比完毕,停止循环
- 如果老的虚拟节点没有对比完全部删除
- 如果新的虚拟节点没有对比完,将新的插入(头指针索引递增)
乱序对比: 6. 如果新老虚拟节点头头、尾尾对比之后都没对比完成,可能中间的节点可以复用 7. 乱序对比:需要尽可能复用,用新的虚拟列表做成一个映射表 8. 遍历老的虚拟列表,如果 key 相同标记已复用,如果不相同,删除老的节点 9. 最后插入新的元素,移动复用的元素到正确的位置
1、头头对比

2、尾尾对比

3、乱序对比

生命周期 (life cycle)
Vue 3 的生命周期是通过发布/订阅模式来实现收集和触发的。主要特点:
- Vue 3 生命周期必须在 setup 中使用,并且生命周期 hook 存储在当前组件的实例上
- 组件在初始化时,如果有 setup 函数,会存储当前组件的实例,并且会导出
- 在执行 setup 函数时,如果有钩子函数,会进行分类,并且生命周期 hook 会存储在当前组件的实例上
- 当组件渲染到某一个阶段时,会从当前组件的实例上去查找对应的生命周期 hook 并且执行
内部执行流程:
- 首先调用 createHook 将函数进行分类,并且返回生命周期钩子函数
- 当生命周期钩子函数在执行时,会传递一个 hook 函数,还有一个默认值(当前组件实例)
- 此时会调用 injectHook 进行收集,并且会传递三个参数(生命周期类型、hook 函数、当前组件实例)
- injectHook 内部会看当前实例上,有没有同类型的 hook,如果没有先创建再新增,如果有直接新增
- 因为 hook 在执行时,始终需要获取到当前组件的实例,这里 hook 在保存的时候会进行 AOP 拦截,在 hook 执行前,设置正确的组件实例,当 hook 执行完成后,将当前的实例设置为 null
js
import { currentInstance, setCurrentInstance } from "./component";
const enum LifeCycles {
BEFORE_MOUNT = "bm", // 组件渲染前
MOUNTED = "m", // 组件渲染后
BEFORE_UPDATE = "bu", // 组件更新前
UPDATED = "u", // 组件更新后
}
// 生命周期只能在setup中使用
// 因为只有在调用setup时才会生成组件的实例,currentInstance,并且生命周期挂载组件实例上
// 订阅
function createHook( lifeCycle ) {
/**
* hook v钩子类型
* currentInstance 当前组件的实例
*/
return ( hook, target = currentInstance ) => {
injectHook( lifeCycle, hook, target );
};
}
function injectHook( lifecycle, hook, target ) {
if ( !target ) return;
// 一个组件同一类型生命周期可以多个
const hooks = target[lifecycle] || ( target[lifecycle] = [] );
const wrap = () => {
setCurrentInstance( target );
hook();
setCurrentInstance( null );
};
hooks.push( wrap );
}
// 发布
export function invokeArrayFns( fns ) {
fns.forEach( ( fn ) => fn() );
}
export const onBeforeMount = createHook( LifeCycles.BEFORE_MOUNT );
export const onMounted = createHook( LifeCycles.MOUNTED );
export const onBeforeUpdate = createHook( LifeCycles.BEFORE_UPDATE );
export const onUpdated = createHook( LifeCycles.UPDATED );keepAlive
Vue 3 的 KeepAlive 组件通过缓存组件实例实现组件状态保留,其核心原理如下:
一、核心机制
缓存对象
- 内部维护一个
Map缓存对象:key → VNode/ComponentInstance - 默认使用组件的
name+props生成唯一key,也可手动指定key
- 内部维护一个
LRU 缓存策略(最近最少使用)
- 当缓存数量超过
max(默认 10)时,自动移除最久未使用的实例 - 每次访问缓存时,将当前实例移到最新位置
- 当缓存数量超过
二、工作流程
组件激活(切换显示)

组件失活(切换隐藏)

三、关键技术点
虚拟节点标记
- 给 VNode 打上
__isKeepAlive: true标记 - 渲染器识别此标记时执行特殊缓存逻辑
- 给 VNode 打上
生命周期钩子
javascriptonActivated(() => { // 组件激活时调用(从缓存恢复) }); onDeactivated(() => { // 组件失活时调用(进入缓存) });缓存管理函数
typescript// 伪代码实现 const cache = new Map(); const keys = new Set(); // LRU 队列 function pruneCacheEntry(key) { const cached = cache.get(key); // 执行卸载钩子并销毁实例 unmount(cached); cache.delete(key); keys.delete(key); }
四、缓存控制参数
| 参数 | 作用 | 示例 |
|---|---|---|
include | 仅缓存匹配组件 | :include="['TabView']" |
exclude | 排除缓存组件 | :exclude="['Admin']" |
max | 最大缓存实例数(LRU 自动淘汰) | :max="5" |
js
import { onMounted, onUpdated } from "./apiLifecycle";
import { getCurrentInstance } from "./component";
import { ShapeFlags } from "@vue/shared";
function resetFlag(vnode) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
vnode.shapeFlag -= ShapeFlags.COMPONENT_KEPT_ALIVE;
}
if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
vnode.shapeFlag -= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
}
}
export const KeepAlive = {
__isKeepAlive: true,
props: {
max: {},
},
setup(props, { slots }) {
// dom操作api都在instance.ctx.renderer上面
const instance = getCurrentInstance();
let { createElement, move } = instance.ctx.renderer;
const keys = new Set(); // 缓存组件的key
const cache = new Map(); // 缓存组件的映射关系
const pruneCacheEntry = (vnode) => {
const subTree = cache.get(vnode);
resetFlag(subTree); // 移除keep-alive标记
cache.delete(vnode);
keys.delete(vnode);
};
let storageContainer = createElement("div");
// 有缓存
instance.ctx.active = (n2, container) => {
move(n2, container);
};
instance.ctx.deactivate = (n1) => {
// 组件卸载的时候 会将虚拟节点对应的真实节点,移动到容器中
move(n1, storageContainer);
};
let pendingCacheKey = null;
const cacheSubTree = () => {
cache.set(pendingCacheKey, instance.subTree);
};
onMounted(cacheSubTree);
onUpdated(cacheSubTree);
return () => {
let vnode = slots.default();
// 不是组件就不用缓存了
if (!(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)) {
return vnode;
}
let comp = vnode.type;
// 组件的名字 找 name,找key ,找组件本身
let key = vnode.key == null ? comp : vnode.key;
pendingCacheKey = key;
let cacheVnode = cache.get(key);
if (cacheVnode) {
// 走到缓存里需要干什么?
vnode.component = cacheVnode.component; // 复用组件
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE; // 组件走缓存的时候不要初始化
} else {
keys.add(key);
let { max } = props;
if (max && keys.size > max) {
// 删除第一个元素 ,在最后增加
// next 返回的是一个对象 {value,done}
pruneCacheEntry(keys.values().next().value);
}
// 获取到了虚拟节点
}
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE; // 用来告诉这个vnode稍后卸载的时候 应该缓存起来
// 组件还是会重新创建, 会走mountComponent
return vnode;
};
},
};js
// -------------------组件----------------------
// 走缓存
function processComponent(n1, n2, container, parent) {
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// 在第一次卸载的时候,我们已经将dom元素移动到内存中,这次渲染我们再将他拿回来
parent.ctx.active(n2, container);
} else {
// 初次渲染
mountComponent(n2, container, parent);
}
} else {
console.log("组件更新");
}
}js
// 删除老的节点
function unmount(n1, parent) {
let { shapeFlag } = n1;
// 被缓存的组件不会真实卸载,而是存储在内存中,下次渲染的时候,直接在内存中取
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
parent.ctx.deactivate(n1);
} else {
// 如果是组件 调用的组件的生命周期等(组件情况)
hostRemove(n1.el);
}
}nextTick
Vue 中视图更新是异步的,使用 nextTick 方法可以保证用户定义的逻辑在更新之后执行,可用于获取更新后的 DOM。主要特点:
- 多次调用 nextTick 会被合并,避免重复执行
- 在调用 nextTick 时,不会立马执行回调,而是存储在队列中
- 同步任务全部执行完毕后,批量更新,DOM 更新的任务也是调用的 nextTick
js
// 简化版 Vue 3 的 nextTick 源码
const schedulerQueue = [];
let isFlushing = false;
function flushSchedulerQueue() {
isFlushing = true;
const currentQueue = schedulerQueue.slice();
schedulerQueue.length = 0;
isFlushing = false;
for (const job of currentQueue) {
job();
}
}
function queueJob(job) {
if (!schedulerQueue.includes(job)) {
schedulerQueue.push(job);
if (!isFlushing) {
Promise.resolve().then(flushSchedulerQueue);
}
}
}
function nextTick(callback) {
queueJob(callback);
}
// 示例用法
nextTick(() => {
console.log("Next tick callback");
});
console.log("End of main script");nextTick 一定会拿到更新之后的 DOM 属性吗
答案是不确定的,这取决于 nextTick 的调用时机,是否在响应式数据发生改变之后。
原理:
- 响应式数据发生改变后,会触发视图异步更新,底层也是调用的
nextTick加入到队列中 - 如果 nextTick 调用时机在响应式数据发生改变之前,会在视图更新之前执行,所以获取
domRef.value.innerHTML,获取到的还是老的值
js
const domRef = ref(null);
const state = ref(0);
nextTick(() => {
console.log(domRef.value.innerHTML); // 0
});
state.value = 1;
nextTick(() => {
console.log(domRef.value.innerHTML); // 1
});vue2 为什么只能有一个根节点,vue3 可以多根节点?
因为vue2vdom 是一个单根树形结构描述当前视图结构,patch 方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也是会转换成 vdom,所以也必须满足单根节点要求因为vue3引入了 fragment 概念,这是一个抽象的节点,如果发现组件是多根的会自动创建一个 fragment 节点,把多根节点视为自己的 children。在 patch 时,如果发现这是一个 fragment 节点,则直接遍历 children 创建或更新
Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异?
- 主要是
减少DOM的操作,响应式数据发生改变时,组件会生成新的虚拟 DOM,对比两株虚拟 DOM 树的变更差异将更新补丁作用于真实 DOM,以最小成本完成视图更新。
v-if&v-show
v-if如果条件不成立,不会渲染当前指令所在的 DOM,并且条件为false时组件内部的逻辑不会执行v-show不管条件是否成立,都会渲染当前指令所在的 DOM,映射到 DOM 就是 displayblock、none,都会执行组件内部逻辑
js
// template
<div v-if="true">if的ast</div>
<div v-show="true">show的ast</div>
// template=>ast
(_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
true
? (_openBlock(), _createElementBlock("div", _hoisted_1, "if的ast"))
: _createCommentVNode("v-if", true),
_withDirectives(_createElementVNode("div", null, "show的ast", 512 /* NEED_PATCH */), [
[_vShow, true]
])
], 64 /* STABLE_FRAGMENT */))
}v-if、v-for 优先级
- 在 Vue2 中解析时,
先解析v-for,在解析v-if。会导致先循环后在对每一项进行判断,浪费性能 - 在 Vue3 中 vif 的优先级高于 v-for。
key 的作用
- vue 中 key 的主要作用就是,数据在发生变化后,可以通过 key 和 tag 节点类型,对比新老 Vnode,可以复用老的节点
- 尽量不要使用索引到做 key
Vue.use 是干什么的
- 主要是提供 vue 插件功能,添加全局的组件、指令等等,
- 在提供插件的时候,需要注册一个对象,并且需要有
install函数
Vue 组件 data 为什么需要是一个函数
根实例对象 data 可以是对象也可以是函数单例,不会产生数据污染情况组件实例对象 data 必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染.所以需要通过工厂函数返回全新的 data 作为组件的数据源
js
function Vue() {}
Vue.extend = function (options) {
function Sub() {
this.data = this.constructor.options.data;
}
Sub.options = options;
return Sub;
};
let Child = Vue.extend({
data: { name: "hl" },
});
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name); // hl
child1.data.name = "lh";
console.log("child1", child1.data.name); // lh
console.log("child2", child2.data.name); // lhv-once 的使用场景有哪些
v-once是 vue 中内置指令,只渲染元素和组件一次,随后的重新渲染,元素/组件及其所有的子节点将被当作为静态节点直接跳过更新对比。这可以用于优化更新性能- vue3.2 之后,增加了 v-memo 指令,通过依赖列表的方式控制页面渲染
双向绑定实现原理
- 在 vue 中双向绑定靠的的
v-model指令 - 1、模版在解析的时候,如果有 v-model 属性,会转换成
input输入事件,同时把绑定的值,赋值给 input value 属性 - 2、当用户输入内容时,会触发 input 输入事件来更新绑定的数据
Vue 中.sync 修饰符的作用
TIP
vue3 中 sync 被移除
- 在有些情况下,我们可能需要对一个 prop 进行双向绑定,这时可以使用
.sync来实现。v-mode1 默认只双向绑定一个属性,这里就可以通过.sync 修饰符绑定多个属性
自定义指令
- 方便开发人员对指令的扩展
指令的生命周期
- bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中),
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前,指令的值可能发生了改变,也可能没有。
- componentupdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用
- unbind: 只调用一次,指令与元素解绑时调用。
Vue 中使用了哪些设计模式
- 单例模式-单例模式就是整个程序有且仅有一个实例 Vuex 中的 store。工厂模式-传入参数即可创建实例(createElement)
- 发布订阅模式-订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
- 观察者模式- watcher&dep 的关系
- 代理模式-代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用
- 中介者模式。中介者是一个行为设计模式通过提供一个统一的接口让系统的不同部分进行通信。 vuex。策略模式- 策路模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案, mergeOption。外观模式-提供了统一的接口,用来访问子系统中的一群接口。
Vue 中的性能优化有哪些
- 数据层级不易过深,合理设置响应式数据
- 通过 Object.freeze()方法冻结属性
- 使用数据时缓存值的结果,不频繁取值。
- 合理设置 Key 属性
- v-show 和 v-if 的选取
- 控制组件粒度 -> Vue 采用组件级更新
- 采用函数式组件->函数式组件开销低
- 采用异步组件-> 借助 webpack 分包的能力
- 使用 keep-alive 缓存组件 v-once
- 分页、虚拟滚动、时间分片等策略
单页应用首屏加载速度慢的怎么解决
- 使用路由懒加载、异步组件,实现组件拆分,减少入口文件体积大小(优化体验骨架屏)
- 抽离公共代码,采用 splitChunks 进行代码分割。
- 组件加载采用按需加载的方式。
- 静态资源缓存,采用 HTTP 缓存 (强制缓存、对比缓存) 、使用 localStorage 实现缓存资源。
- 图片资源的压缩,雪碧图、对小图片进行 base64 减少 http 请求。
- 打包时开启 gzip 压缩处理 compression-webpack-plugin 插件
- 静态资源采用 CDN 提速。终极的手段
- 使用 SSR 对首屏做服务端渲染。