文章目录
- 一、前言
- 二、Vue3篇
- Vue3 相对于 Vue2 做了哪些更新?
- Vue3响应式
- Vue3响应式特点
- Object.defineProperty 与 Proxy 的区别
- 什么是Proxy?
- 为什么需要 Reflect?(目标对象内部的this指向问题)
- Vue3 惰性响应式
- Proxy 只会代理对象的第一层,Vue3 如何处理的?
- Vue3 使用解构丢失响应式
- ref 和 reactive 定义响应式数据
- ref 响应式原理
- reactive 响应式原理
- ref中定义的变量被reactive引用,不需要用 .value 获取
- Composition API
- Vue3生命周期
- watch 与 watchEffect
- defineModel()
- 关于 v-model 和 defineModel() 区别
- 底层机制:
- 返回值
- 多个 v-model 绑定
- defineExpose()
- 常见使用场景:
- 注意事项
- 三、Vue2篇
- 1. 介绍一下MVVM模式,和MVC模式有什么区别
- 2. Vue2响应式
- 响应式原理
- Vue2响应式的创建、更新流程
- Vue2 响应式的缺点
- v-model是什么?实现原理?
- Vue响应式 Observer、Dep、Watcher 的关系
- Vue2 为什么不能监听数组下标的原因
- Vue2 如何对数组进行改写实现数组响应式的?
- 为什么 Vue3 用 proxy 代替了 Vue2 中的 Object.defineProperty
- 3. Vue2生命周期
- 生命周期及使用场景
- 父子组件 生命周期 的执行顺序
- 平时发送异步请求在哪个生命周期,并解释原因
- DOM 渲染在哪个周期中就已经完成
- 项目中哪些生命周期比较常用,有哪些场景?
- 4.Vue.set ()
- 什么时候用set()?
- 它的原理?
- 5. Vue.use 安装一个插件
- 1)概念
- 2)原理
- 3)源码
- 6. 虚拟DOM 和 Diff算法
- 7. vue中key的作用
- 8. vue组件间通信方式
- 9. watch 和 computed 的区别
- 10. 对keep-alive的理解,keep-alive 产生的生命周期有哪些?
- 11. nextTick用法、原理、Vue的异步更新原理
- 12. v-for 与 v-if
- 两者同时使用优先级问题
- 两者如果同时使用如何解决?
- 13. Vue.set 和 Vue.delete
- 什么时候用set()? 它的原理?
- 什么时候用delete()? 它的原理?
- 四、Pinia
- 五、性能优化
- 大型虚拟列表
一、前言
基础一定要多看,很多看似复杂的bug,实际都是基础问题,不要把时间浪费在修复基础问题的bug上。
本文以知识点总结为主,将技术点连成线,再结成网,不管面试官问啥,都能扯一扯啦~
最近总感觉知道的越多之后,不知道的更多;还有许多需要学习的,总结这篇文章之后,也该需要制定个以后的学习计划了。然后这篇关于vue2/vue3的文章也算是一个阶段性的总结吧,有了对vue框架的理解,相信再上手框架的学习也会有帮助。
在整理此文过程中的提升确实要比平时做版本迭代需求来得快,因为平时短时间内学习补充的东西太多,不系统性整理的话会很乱,所以输出文档,形成自己的总结,方便整理和查看,加深记忆。
二、Vue3篇
Vue3 相对于 Vue2 做了哪些更新?
总的来说 vue3 相比 vue2 更小(打包体积)、更快(响应式、diff算法优化)、更友好(composition API,对TypeScript 支持)。
从框架层面
- 响应式的优化:使用
Proxy代替Object.defineProperty,可以监听数组下标的变化和对象属性的新增和删除;因为Proxy 可以对整个对象进行拦截和代理。可以拦截对象的读取、赋值、删除等操作。- 虚拟DOM的优化:
a)静态节点提升
vue3 增加静态节点直接复用;静态提升就是不参与更新的静态节点,只会创建一次,之后每次渲染时候直接复用。
b)虚拟节点静态标记
在对比vnode的时候。只会比较patchFlag发生变化的节点,大大减少了对比 Vnode 时需要遍历节点的数量。对于没有变化的节点做静态标记,在渲染的时候直接复用。
c)优化效果
vue3 的渲染效率不再和模板大小成正比,而是和模板中动态节点的数量成正比。- diff算法的优化:vue3使用
最长递增子序列优化了对比的流程,使得虚拟dom生成速度提升200%。- 代码打包体积的优化:vue中许多的API都可以被
Tree shaking,它是基于 ES6 Moudle ,主要是借助 ES6 模块的静态编译思想,在编译时就能确定模块的依赖关系,未被使用或者引用,删除对应代码。
从API层面
- composition API:组合式API,方便逻辑组织和逻辑复用,相同的业务的数据方法写在同一块代码区域,不至于太分散。
vue2中可以用mixin来复用代码,但也存在问题;比如:方法或属性名会冲突、代码来源也不清楚。- Fragments:
vue3 中组件的 template 下可以包含多个根节点,内部会默认添加Fragments, vue2 中组件的 template 下只能包含一个根节点。- Teleport传送门:可以让子组件能够在视觉上跳出父组件(如父组件 overflow:hidden)
- v-memo:新增指令可以缓存html模板;v-memo 仅用于性能至上场景中的微小优化,应该很少需要。最常见的情况可能是有助于渲染海量 v-for 列表 。比如 v-for 列表不会变化的就缓存,简单说就是用空间换时间。
从兼容性层面
Vue3 不兼容 IE11,因为 IE11 不兼容 Proxy
从其它层面
- 生命周期不同。
- 对 TypeScript 的支持不同:
Vue3在TypeScript支持方面进行了改进,可以提供更好的类型推断和支持,使得在使用 TypeScript 进行开发时更加舒适和可靠- vue3
v-if优先于v-for生效:不会再出现vue2中 v-for/v-if 混用的情况;但是把 v-if 和 v-for 同时用在一个元素上 vue 中会给我们报警告。- 自定义指令钩子函数名称不同
a) vue2钩子函数使用bind、inserted、update、componentUpdated、unbind
b)vue3钩子函数使用created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted
Vue3响应式
Vue3响应式特点
为了解决 Vue2 响应式的问题,Vue3 改用 Proxy 结合 Reflect 实现响应式系统。
- 支持监听
对象和数组的变化。 - 对象嵌套属性只代理第一层,运行时递归,用到时才代理,也不需要维护特别多的依赖关系,提高了性能和效率。
- 目前能拦截对象的13种方法,动态属性增删都可以拦截,新增数据结构全部支持。
- Vue3提供
ref和reactive两个API来实现响应式。
Object.defineProperty 与 Proxy 的区别

defineProperty 原本是对象内部(DefineOwnProperty)的基本操作之一,是用来定义属性描述符的。proxy 是针对对象内部所有的基本操作,都可以进行拦截。
什么是Proxy?
- Proxy 是ES6中的方法,并不是所有的浏览器都支持(比如IE11)。
- Proxy 用于创建一个 目标对象 的代理,在对 目标对象 的操作之前提供了拦截,可以对外界的操作进行 过滤 和 改写。这样我们可以不直接操作目标对象,而是通过操作对象的
代理对象来间接操作对象。- Proxy 直接代理整个目标对象,并且返回一个新的
Proxy对象。
var proxy = new Proxy(target, handler);
//new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
为什么需要 Reflect?(目标对象内部的this指向问题)
- 起因是因为 目标对象内部的
this关键字会指向 Proxy 的代理对象。 - 使用
Reflect可以修正 Proxy 的this指向问题。 - Proxy 的一些拦截方法要求返回
true/false来表示操作是否成功,比如set、deleteProperty等方法,这也和Reflect对象的静态方法相对应。 - 现阶段,某些方法同时在
Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
下面是一个例子,由于this指向的变化,导致 Proxy 无法代理目标对象。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
上面代码中,一旦 proxy 代理 target,target.m() 内部的 this 就是指向 proxy,而不是 target。
Vue3 惰性响应式
Vue2对于一个深层嵌套的对象,需要递归遍历这个对象,给每个属性都添加响应式。
Vue3中使用Proxy并不能监听到 对象深层次 内部属性的变化,只能代理第一层,因此它的处理方式是在getter中去递归响应式,不需要维护特别多的依赖关系;这样做的好处是真正访问到的内部属性才会变成响应式,减少性能消耗。
Proxy 只会代理对象的第一层,Vue3 如何处理的?
- 判断当前
Reflect.get()的返回值是否是Object,如果是则通过reactive方法做代理,这样就实现了深度观测,可以确保在访问嵌套对象属性时也能够获得响应式的特性。- 检测数组的时候可能触发了多个
get/set,那么如何防止多次触发呢?我们可以判断key是否是当前被代理的target自身属性。
Vue3 使用解构丢失响应式
- Vue3 响应式数据使用
ES6解构出来的是一个引用对象类型时,它还是响应式的,如果解构出来是基本数据类型,响应式会丢失。- 因为 Proxy 只能监听对象的第一层,深层对象的监听 vue 是通过
reactive方法再次代理,所以返回的引用还是一个Proxy对象;而基本类型就是值。- 为了避免丢失响应式,可以使用
toRefs函数可以保持它们的响应式绑定。
比如下面的例子:
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
// 这个 ref 和源属性已经“链接上了”
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
ref 和 reactive 定义响应式数据
- Vue3 区分
ref和reactive的原因就是Proxy无法对原始值做代理,所以需要一层对象作为包裹。- 使用
ref创建的响应式引用在Vue模板中被自动解包。这意味着当在模板中使用ref创建的变量时,可以直接使用而不需要每次通过.value访问。如果使用proxy来处理基础类型,这种自动解包可能就无法实现,从而增加了模板中代码复杂性。
ref 响应式原理
- ref 生成响应式对象,一般用于
基础类型。- ref 内部封装一个
RefImpl类,并设置 get/set 方法,当通过.value调用时就会触发劫持,从而实现响应式。- 当接收的是对象或数组时候,内部依然是用
reactive去实现响应式,而reactive实现响应式的方法是ES6 的Proxy和Reflect。关于reactive实现响应式下面也会介绍。

比如
ref({ a: 1 })本质是如何实现的?
在 Vue 3 中,ref 的本质实现依赖于 ES6 的Proxy和ReflectAPI。当创建一个响应式对象时,Vue 会将其包装在一个代理对象中,并通过代理对象拦截其属性的读取和赋值操作( get/set 方法),从而实现了响应式更新的功能
借助 Vue3 ref 源码来看下,源码地址:/vue3/packages/reactivity/src/ref.ts
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
可以看到,这个对象有 _value 属性和 value 访问器属性。_value 属性存储了原始对象,而 value 属性是访问器属性,它会触发响应式更新。当访问 value 属性时,会调用 trackRefValue 函数开始追踪响应式依赖。当 value 属性被赋值时,会调用 set 函数进行响应式更新,并触发 triggerRefValue 函数通知相关依赖进行更新。
reactive 响应式原理
reactive代理整个对象,一般用于引用类型。reactive函数利用Proxy对象实现了对普通对象的代理,并通过拦截对象的访问和修改操作,实现了数据的响应式更新。- 在代理对象中,当
访问对象属性时,会触发get处理函数。在这个函数中,会收集当前属性的依赖,并返回当前属性的值。这里的依赖是指在模板中引用了该属性的地方,Vue 3 会自动跟踪这些依赖。- 在代理对象中,当
修改对象属性时,会触发set处理函数。在这个函数中,会更新属性的值,并通知所有依赖该属性的地方进行更新。这里的更新是指重新计算引用该属性的部分内容,并将结果显示在页面上。- 使用
Proxy拦截数据的访问和修改操作,再使用Reflect完成原本的操作(get、set)
借助 Vue3 reactive源码来看下,源码地址:/vue3/packages/reactivity/src/reactive.ts
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
ref中定义的变量被reactive引用,不需要用 .value 获取
const myName = ref<string>('我是铁锤')
const myState = reactive({
name: myName,
age: 18
})
console.log(myState.name, 'ref中定义的变量被reactive引用,不需要用 .value获取')
上面代码中,在 reactive 对象中引用 ref 定义的 myName 时,不需要使用 .value 获取,是因为 Vue3 在内部自动解包了 ref 对象;这是 Vue3 设计的一个便利之处。
Composition API
Vue3生命周期
- 基本上就是在 Vue2 生命周期钩子函数名基础上加了
on setup代替了两个钩子函数beforeCreate和created- beforeDestory 和 destoryed 更名为
onBeforeUnmount和onUnmounted

watch 与 watchEffect
- watch 作用是对传入的某个或多个值的变化进行监听;接收两个参数,第一个参数可以是不同形式的“数据源”,第二个参数是回调函数,回调函数接收两个参数新值 newval 和旧值 oldVal;也就是说第一次不会执行,只有变化时才会重新执行。
- watchEffect 是传入一个立即执行函数,所以默认第一次也会执行一次;不需要传入监听内容,会自动收集函数内的数据源作为依赖,在依赖变化的时候又会重新执行该函数,如果没有依赖就不会执行;而且不会返回变化前后的新值和老值。
- watch加
Immediate: true也可以立即执行。
官方文档 watchEffect()
defineModel()
关于 v-model 和 defineModel() 区别
之前我们实现双向绑定都是在组件上使用 v-model ,从 Vue 3.4 开始,官方推荐的实现方式是使用 defineModel() 宏。
举个例子简单说明下使用v-model 和子组件中使用defineModel()的实际被编译成的代码区别:
// 父组件使用`v-model` 被编译为
<Context
:modelValue="count"
@update:modelValue="$event => (count = $event)"
/>
// 子组件中使用`defineModel()`被编译为
const props = defineProps<{ modelValue: number }>(),
emit = defineEmits<{ 'update:modelValue': [value: number] }>()
总结:
v-model实现原理: 1)v-bind绑定响应数据; 2)触发input事件监听并传递数据defineModel() 实现原理:1)它应该为子组件定义了一个包含 modelValue 的 props; 2)一个自定义事件 update:modelValue。
底层机制:
defineModel 是一个便利宏。编译器将其展开为以下内容:
- 一个名为
modelValue的 prop,本地 ref 的值与其同步; - 一个名为
update:modelValue的事件,当本地 ref 的值发生变更时触发。
返回值
defineModel() 返回的值是一个
ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:
- 它的
.value和父组件的v-model的值同步;- 当它被子组件变更了,会触发父组件绑定的值一起更新。
多个 v-model 绑定
//父组件
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
//子组件
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
注意:
如果为defineModelprop 设置了一个default值且父组件没有为该 prop 提供任何值,会导致父组件与子组件之间不同步。
官方文档 组件 v-model
defineExpose()
Vue3中的defineExpose()用于在子组件中暴露 数据(ref或reactive定义数据) 或 方法,以便父组件或其他组件通过 ref 访问子组件的实例,并调用子组件中暴露的数据和方法。
常见使用场景:
- 暴露数据和方法;
- 使用TypeScript时,defineExpose()还可以提供类型安全。确保在引用数据或方法时不会出现类型错误。
注意事项
- defineExpose() 应该在
setup()函数内的最后调用,确保所有需要暴露的内容都已经准备好。- 尽量避免暴露过多的内部状态或方法,遵循单一责任原则。
- 使用 defineExpose() 可以帮助你更清晰地定义组件的 API,同时也需要小心避免过度暴露导致的封装性问题。
案例:
// 子组件
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
const increment = () => {
b.value++;
};
defineExpose({
a,
b,
increment
})
</script>
// 父组件
<template>
<MyChild ref="MyChild" />
<button @click="incrementChild">Increment Child Count</button>
<p>Child Count: {{ childCount }}</p>
</template>
<script setup>
import MyChild from './child '
const childComponent = ref(null);
const childCount = ref(0);
const incrementChild = () => {
childComponent.value.increment();
childCount.value = childComponent.value.count;
};
</script>
三、Vue2篇
1. 介绍一下MVVM模式,和MVC模式有什么区别
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。
- 其中
View负责页面的显示逻辑, Model负责存储页面的业务数据,以及对相应数据的操作。Controller层是View层和Model层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller中的事件触发器就开始工作了,通过调用Model层,来完成对Model的修改,然后Model层再去通知View层更新。

MVVM 分为 Model、View、ViewModel。
Model代表数据模型,数据和业务逻辑都在 Model 层中定义;View代表 UI 视图,负责数据的展示;ViewMode负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。
这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作 DOM。

2. Vue2响应式
响应式原理
vue中采用 数据劫持 结合发布-订阅模式。通过 Object.defineProperty() 对vue传入的数据进行了相应的数据拦截,为其动态添加get() 与 set() 方法。当数据变化的时候,就会触发对应的 set() 方法,当 set() 方法触发完成的时候,内部会进一步触发 watcher,当数据改变了,接着进行 虚拟dom 对比,执行render,后续视图更新操作完毕。
Vue2响应式的创建、更新流程
- 当一个 vue 实例创建时,vue会遍历
data中的所有属性,用Object.defineProperty给属性设置getter/setter方法, 并且在内部追踪相关依赖,在属性被访问和修改时分别调用getter和setter。 - 每个组件实例都有相应的
watcher程序实例,它会在组件渲染过程中进行 依赖收集,之后当响应式数据发生变化时,其setter方法会被调用,会通知watcher重新计算,观察者Watcher自动触发更新render当前组件,生成新的虚拟 DOM 树。 - Vue框架会遍历并对比
新旧虚拟DOM 树中的每个节点差异,并记录下来,最后将所有记录的不同点,局部更新到真实DOM树上。
Vue2 响应式的缺点
Object.defineProperty 在劫持对象和数组时的缺陷:
- 无法检测到对象属性的添加和删除。
- 监听对象的多个属性,需要遍历该对象,为对象所有的key添加响应式;如果对象层级较深,还需要递归遍历,性能不好。
- 无法检测数组元素的变化(增加/删除),需要进行数组方法的重写。
- 无法直接通过
.length改变数组的长度。- 不支持Map、Set等数据结构。
v-model是什么?实现原理?
vue中的 v-model 可以实现数据的双向数据绑定。它是一个语法糖。利用 v-model 绑定数据后,即绑定了数据,又添加了一个 input 事件监听。
实现原理:
v-bind绑定响应数据- 触发
input事件监听并传递数据
代码示例
<input v-model="text"></input>
// 等价于
<input :value="text" @input="text=$event.target.value"/>
Vue响应式 Observer、Dep、Watcher 的关系
Vue响应式原理的核心就是 Observer、Dep、Watcher
Observer 中进行响应式的绑定
- 在数据被读的时候,触发
get方法,执行Dep收集依赖,也就是收集不同的Watcher。 - 在数据被改的时候,触发
set方法,对之前收集的所有依赖Watcher,进行更新。
Vue2 为什么不能监听数组下标的原因
- Vue2 使用
Object.definePrototype做数据劫持实现数据双向绑定的。而数组的下标赋值并不会触发数组对象上的set()方法,因此无法直接监听数组下标的变化。 Object.definePrototype是可以劫持数组的。- 真实情况是:
Object.definePrototype本身可以劫持数组,而 Vue2 却没有用来劫持数组。 - 原因:
a) Vue作者在 issue上说 不使用 Object.definePrototype 直接劫持数组是因为 性能代价和用户体验收益不成正比。
b)Object.definePrototype 是属性级别的劫持,如果使用它来劫持数组的话,一旦用户定义了一个极大数组,就会耗费极大的性能来遍历数组,以及监听每个下标变化的事情上,导致框架的性能不稳定,因此Vue2牺牲一些用户使用的便捷性,提供一个$set方法去修改数组,以最大程度保证框架的稳定性。
具体来说,Vue2 的响应式系统会在初始化时遍历对象的属性,并使用 Object.definePrototype 对每个属性添加 get 和 set 方法。这样一来,当属性被访问或修改时,Vue就能捕捉到并触发视图更新。
在Vue2中,为了监听数组的变化,劫持重写了几个数组方法来触发视图更新。但是直接通过下标赋值的操作是无法被vue监听到的。
Vue2 如何对数组进行改写实现数组响应式的?
重写数组方法,手动派发更新
可以先看下源码:
// 获取数组的原型
const arrayProto = Array.prototype
// 创建一个新对象并继承了数组原型的属性和方法,将其原型指向 Array.prototype
// 为什么要克隆一份呢?因为如果直接更改数组的原型,那么将来所有的数组都会被我改了。
export const arrayMethods = Object.create(arrayProto)
// 会改变原数组的方法列表;为什么只有7个方法呢?因为只有这7个方法改变了原数组
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 重写数组事件
methodsToPatch.forEach(function (method) {
// 缓存原始方法
const original = arrayProto[method]
// 创建响应式对象
def(arrayMethods, method, function mutator(...args) {
// 首先 还是使用原生的 Array 原型方法去操作数组
const result = original.apply(this, args)
// 然后 再做变更通知,如何变更的呢?
// 1. 获取 Observer 对象实例
const ob = this.__ob__
// 2.如果是新增元素的操作,比如push、unshift或者增加元素的splice操作
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 3.新加入的元素需要做响应式处理
if (inserted) ob.observeArray(inserted)
// 4.让内部的dep派发更新
if (__DEV__) {
// 通过 Observer 对象实例上 Dep 实例对象去通知依赖进行更新
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method
})
} else {
// 派发更新
ob.dep.notify()
}
// 返回原生数组方法的执行结果
return result
})
})
/**
* Observe a list of Array items.
*/
observeArray(value: any[]) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false, this.mock)
}
}
简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知渲染 watcher,执行 update。
除此之外可以使用 set() 方法,Vue.set() 对于数组的处理其实就是调用了 splice 方法。
为什么 Vue3 用 proxy 代替了 Vue2 中的 Object.defineProperty
Vue3 在设计上选择使用 Proxy 代替 Object.defineProperty 主要是为了提供更好的 响应式 和 性能。
- Object.defineProperty的劫持是基于
属性级别的,在初始化时需要遍历对象所有的属性key添加响应式,如果对象层级较深会多次调用observe()递归遍历,导致性能下降,增加初始化时间。 - 通知更新过程需要维护大量
dep实例和watcher实例,增加内存消耗。 - Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在Vue2中,从性能/体验的性价比考虑,就弃用了这个特性。为了解决这个问题,只能通过劫持重写了几个数组方法,触发这几个方法的时候会
observe数据,如果有新的值,就调用observeArray对新的值进行监听,然后手动调用notify(),通知渲染watcher,执行update(),视图自动进行更新。 - 动态新增、删除对象属性无法拦截,只能用特定的
$set/$delete来通知响应式更新。 - 相比之下,Proxy 可以对整个对象进行拦截和代理。提供了更强大的拦截能力,可以拦截对象的读取、赋值、删除等操作。Vue3利用 Proxy 的特性,可以更方便的实现响应式系统。
- Proxy 可以直接拦截对象的读取和赋值操作,无需在每个属性上进行劫持。这样就消除了属性级别的开销,提高了初始化性能;另外 Proxy 还可以拦截
新增/删除属性,使响应式系统更加完备。
3. Vue2生命周期
生命周期及使用场景
总共分为 8 个阶段:创建前/后,挂载前/后,更新前/后,销毁前/后。
1)创建阶段
- beforeCreate(创建前):执行一些初始化任务,此时不能访问props、methods、data、computed、watch上的方法和数据。
- created(创建后):在实例创建完成后被立即调用,此时实例已经初始化完成。实例上配置的props、methods、data、computed、watch等都配置完成。但DOM元素尚未挂载,适合进行数据初始化和异步操作。
2)挂载阶段
- beforeMount(挂载前):在挂载前被调用,相关的render函数首次被调用;实例已完成以下配置:
编译模板,把data里面的数据和模板生成html;此时虚拟DOM已创建,但还未渲染到真实DOM中。 - mounted(挂载后):在实例挂载到DOM后被调用。实例已经成功挂载到DOM中,可执行DOM操作 和 访问DOM元素。
3)更新阶段
- beforeUpdate(更新前):数据更新前被调用。此时虽然响应式数据更新了,但是真实DOM没有被渲染。
- updated(更新后):数据更新后被调用。此时数据已经更新到DOM,适合执行DOM依赖的操作。
4)销毁阶段
- beforeDestroy(销毁前):实例销毁前被调用。这里实例仍然完全可用,this仍能获取到实例。可用于清理定时器、取消订阅、解绑事件等清理操作。
- destroyed(销毁后):实例销毁后被调用。这一阶段,实例和所有相关的事件监听器和观察者都已经被销毁。
父子组件 生命周期 的执行顺序
创建过程自上而下,挂载过程自下而上。
加载渲染过程:
父组件 beforeCreate
父组件 created
父组件 beforeMount
子组件 beforeCreate
子组件 created
子组件 beforeMount
子组件 mountd
父组件 mountd
子组件更新过程:
父组件 beforeUpdate
子组件 beforeUpdate
子组件 updated
父组件 updated
父组件更新过程:
父组件 beforeUpdate
父组件 updated
销毁过程:
父组件 beforeDestroy
子组件 beforeDestroy
子组件 destroyed
父组件 destroyed
平时发送异步请求在哪个生命周期,并解释原因
created,beforeMount,mounted
因为在这3个钩子函数中, data 已经创建,可以将服务端返回的数据进行赋值。
推荐在 created 钩子函数中发送异步请求,因为
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好。
- SSR不支持
beforeMount,mounted钩子函数,放在created中有助于一致性。
DOM 渲染在哪个周期中就已经完成
mounted
注意:mounted不会承诺所有的子组件也都一起被挂载。如果希望等到整个视图都渲染完毕再操作一些事情,可使用 $nextTick 替换掉 mounted。
项目中哪些生命周期比较常用,有哪些场景?
created获取数据mounted操作 dom元素beforedestroy销毁一些实例、定时器、解绑事件
4.Vue.set ()
什么时候用set()?
Vue2 在两种情况下修改数据,是不会触发视图更新的。但是打印数据层已经更新。
- 在实例创建之后添加新的属性到实例上(给响应式对象新增属性)
- 通过更改数组下标来修改数组的值
它的原理?
export function set(
target: any[] | Record<string, any>,
key: any,
val: any
): any {
// 首先判断set的目标是否是undefined和基本类型如果是undefined或基本类型就报错,
// 因为用户不应该往undefined和基本类型中set东西,
if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${target}`
)
}
if (isReadonly(target)) {
__DEV__ && warn(`Set operation on key "${key}" failed: target is readonly.`)
return
}
// 获取Observer实例
const ob = (target as any).__ob__
// traget 为数组
if (isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splice()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
if (ob && !ob.shallow && ob.mock) {
observe(val, false, true)
}
return val
}
// target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
if ((target as any)._isVue || (ob && ob.vmCount)) {
__DEV__ &&
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// target 本身就不是响应式数据,直接赋值
if (!ob) {
target[key] = val
return val
}
// 进行响应式处理
defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock)
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ADD,
target: target,
key,
newValue: val,
oldValue: undefined
})
} else {
ob.dep.notify()
}
return val
}
以上代码中,$set 方法的主要实现逻辑如下:
- 如果目标是
数组,使用Vue中数组的splice()变异方法来更新指定位置的元素并触发响应式更新。(splice变异方法请看上方讲的Vue2重写数组方法源码)- 如果目标对象已经包含了指定的属性,即为响应式,直接赋值。
- 如果目标对象没有指定的属性,即新添加的属性不是响应式,Vue会通过
defineProperty方法进行响应式处理,并在新的属性上设置getter和setter,以便在属性被访问或修改时触发响应式更新。
总之,Vue2中$set方法对数组和对象的处理本质上的一样的,对新增的值添加响应然后手动触发派发更新。
5. Vue.use 安装一个插件
1)概念
- Vue是支持插件的,可使用
Vue.use()来安装插件。 - 如果插件是一个
对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。install 方法调用时,会将Vue作为参数传入。 - 该方法需要在调用
new Vue()之前被调用。 - 当
install方法被同一个插件多次调用,插件将只会被安装一次。
2)原理
Vue.use() 原理并不复杂,它的功能主要就是两点:安装Vue插件、已安装不会重复安装。
- 先声明一个数组,用来存放安装过的插件,如果已安装就不重复安装;
- 然后判断
plugin是不是对象,如果是对象就判断对象的install是不是一个方法,如果是就将参数传入并执行install方法,完成插件的安装; - 如果
plugin是一个方法,就直接执行; - 最后将
plugin推入上述声明的数组中,表示插件已经安装; - 最后返回
Vue实例。
3)源码
可以将源码和上面的原理对照一起看。
export function initUse(Vue: GlobalAPI) {
Vue.use = function (plugin: Function | any) {
const installedPlugins =
this._installedPlugins || (this._installedPlugins = [])
// 如果已经安装过,就返回Vue实例
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
if (isFunction(plugin.install)) {
plugin.install.apply(plugin, args)
} else if (isFunction(plugin)) {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
6. 虚拟DOM 和 Diff算法
Vue源码学习 - 虚拟Dom 和 diff算法
7. vue中key的作用
- key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,diff 操作可以更高效。
- 如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单“就地复用”此处的每个元素。
- 不建议使用 index 作为 key 值,因为在数组中key的值会跟随数组发生改变(比如在数组中添加或删除元素、排序),而key值改变,diff算法就无法得知在更改前后它们是同一个DOM节点。会出现渲染问题。
8. vue组件间通信方式
vue中的8种常规通讯方案:
- 通过 props 传递
- 通过 $emit 触发自定义事件
- 使用 ref
- EventBus(事件中心)
- $parent 或 $root
- attrs 和 listeners
- provide 和 inject
- Vuex
组件间通信的分类可以分成以下:
- 父子关系的组件数据传递选择
props与$emit进行传递,也可以选择ref。- 兄弟关系的组件数据传递可选择
$bus,其次可以选择$parent进行传递。- 祖先与后代组件数据传递可选择
attrs和listeners,或者provide和inject。- 复杂关系的组件数据传递可通过
Vuex存放共享的变量。
vue组件之间的传值方法(父子传值,兄弟传值,跨级传值,vuex)
9. watch 和 computed 的区别
-
computed :基于其依赖的
响应式数据(data,props)进行计算得出结果的属性。并且computed的值有缓存,只有他依赖的属性值发生改变,下一次获取computed的值时才会重新计算computed的值。必须同步。 -
watch :更多的是
观察作用,无缓存性;类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。支持同步/异步。
运用场景:
- 当需要进行数值计算,并依赖于已有的
响应式数据进行计算得出结果的场景,应该使用computed,因为可以利用computed的缓存特性,避免每次获取值时都要重新计算。- 当需要在数据变化时
执行异步或者开销较大的操作时,应使用watch。
10. 对keep-alive的理解,keep-alive 产生的生命周期有哪些?
keep-alive是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。- 用
keep-alive包裹动态组件时,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。keep-alive还运用了LRU(最近最少使用)算法,通过传入max属性来限制可被缓存的最大组件实例数;最久没有被访问的缓存实例被销毁,以便为新的实例腾出空间。
实现原理
在vue的生命周期中,用keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated钩子函数,命中缓存后执行 activated 钩子函数
两个属性 include 和 exclude
- include => 值可以为以英文逗号分隔的字符串、正则表达式或数组;只有名称匹配的组件会被缓存。
- exclud => 值可以为以英文逗号分隔的字符串、正则表达式或数组;任何名称匹配的组件都不会被缓存。
它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。
产生的生命周期
用来得知当前组件是否处于活跃状态。
- Vue3中是
onActivated()和onDeactivated()- Vue2中是
activated和deactivatedonActivated/activated调用时机为首次挂载, 以及每次从缓存中被重新插入时;onDeactivated/deactivated调用时机为从 DOM 上移除、进入缓存,以及组件卸载时调用。- 如果没有
keep-alive包裹,没有办法触发activated生命周期函数。
具体使用案例可熟读 KeepAlive 官方文档
11. nextTick用法、原理、Vue的异步更新原理
Vue源码学习 - 异步更新队列 和 nextTick原理
12. v-for 与 v-if
两者同时使用优先级问题
- 在 Vue2 中,
v-for的优先级高于v-if,一起使用的话,会先执行循环再判断条件;并且会带来性能方面的浪费(每次都会先循环渲染再进行条件判断),所以不应该将它俩放在一起;- 在Vue3 中,
v-if的优先级高于v-for;因为v-if先执行,此时v-for未执行,所以如果使用v-for定义的变量就会报错;
两者如果同时使用如何解决?
- 如果条件出现在循环内部,我们可以提前过滤掉不需要
v-for循环的数据;- 条件在循环外部,
v-for的外面新增一个模板标签template,在template上使用v-if
13. Vue.set 和 Vue.delete
什么时候用set()? 它的原理?
在两种情况下修改数据,Vue是不会触发视图更新的。
- 在实例创建之后,添加新的属性到实例上(给响应式对象新增属性)
- 直接更改数组下标来修改数组的值
set() 的原理
- 目标是
对象,就用defineReactive给新增的属性去添加getter和setter;- 目标是
数组,直接调用数组本身的splice方法去触发响应式。
什么时候用delete()? 它的原理?
同set()
四、Pinia
- 完整的 typescript 的支持;
- 足够轻量,压缩后的体积只有1.6kb;
- 去除 mutations,只有 state,getters,actions(这是我最喜欢的一个特点);
- actions 支持同步和异步;
- 没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割;
- 无需手动添加 store,store 一旦创建便会自动添加;(通过使用
defineStore函数来创建 store 类,一旦创建 store 类,Pinia 会自动为你生成 store 实例,并将其添加到全局 store 容器中。)
五、性能优化
可参考:
前端性能优化——包体积压缩82%、打包速度提升65%
前端性能优化——首页资源压缩63%、白屏时间缩短86%
大型虚拟列表
无论一个框架性能有多好,渲染成千上万个列表项都会变得很慢,因为浏览器需要处理大量的 DOM 节点。
但是,我们并不需要立刻渲染出全部的列表。在大多数场景中,用户的屏幕尺寸只会展示这个巨大列表中的一小部分。我们可以通过 列表虚拟化 来提升性能,这项技术使我们只需要渲染用户视口中能看到的部分。
本文还不完善,vue本身也是持续更新的,所以我打算有时间就会总结一些。
「2022」寒冬下我的面试知识点复盘【Vue3、Vue2、Vite】篇
12道vue高频原理面试题,你能答出几道?
金三银四,我为面试所准备的100道面试题以及答案,不看要遭老罪喽



















