文章目录
- 一、前言
- 二、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
和Reflect
API。当创建一个响应式对象时,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>
注意:
如果为defineModel
prop 设置了一个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
和deactivated
onActivated/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道面试题以及答案,不看要遭老罪喽