我的Vue2/Vue3知识框架汇总

317 阅读18分钟

一、前言

基础一定要多看,很多看似复杂的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 的支持不同Vue3TypeScript 支持方面进行了改进,可以提供更好的类型推断和支持,使得在使用 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提供 refreactive 两个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 来表示操作是否成功,比如 setdeleteProperty等方法,这也和 Reflect 对象的静态方法相对应。
  • 现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在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 代理 targettarget.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 区分 refreactive 的原因就是 Proxy 无法对 原始值 做代理,所以需要一层 对象 作为包裹。
  • 使用 ref 创建的响应式引用在Vue模板中被自动解包。这意味着当在模板中使用 ref 创建的变量时,可以直接使用而不需要每次通过 .value 访问。如果使用 proxy 来处理基础类型,这种自动解包可能就无法实现,从而增加了模板中代码复杂性。

​ref 响应式原理

  • ref 生成响应式对象,一般用于基础类型
  • ref 内部封装一个 RefImpl 类,并设置 get/set 方法,当通过 .value 调用时就会触发劫持,从而实现响应式。
  • 当接收的是对象或数组时候,内部依然是用 reactive 去实现响应式,而 reactive 实现响应式的方法是ES6 的 ProxyReflect 。关于reactive 实现响应式下面也会介绍。

在这里插入图片描述

比如 ref({ a: 1 }) 本质是如何实现的? 在 Vue 3 中,ref 的本质实现依赖于 ES6 的 ProxyReflect 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 完成原本的操作(getset

借助 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 代替了两个钩子函数 beforeCreatecreated
  • beforeDestory 和 destoryed 更名为 onBeforeUnmountonUnmounted

在这里插入图片描述

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>

Suspense

官方文档对它的解释

<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

上面的意思是 <Suspense> 组件能够监听下面的异步组组件,在等待异步子组件完成渲染之前,可以去渲染一个loading的页面。 Suspense组件支持两个插槽:#default#fallback。如果#default插槽中有异步组件,那么就会先去渲染 #fallback中的内容,等到异步组件加载完成后就会将#fallback中的内容给干掉,改为将异步组件的内容渲染到页面上。

另外,Suspense组件目前是实验性的功能,生产环境使用需要谨慎!!

具体使用案例可参考:卧槽,牛逼!vue3的组件竟然还能“暂停”渲染!

三、Vue2篇

​1. 介绍一下MVVM模式,和MVC模式有什么区别

MVC 通过分离 ModelViewController 的方式来组织代码结构。

  • 其中 View 负责页面的显示逻辑,
  • Model 负责存储页面的业务数据,以及对相应数据的操作。
  • Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。 在这里插入图片描述

MVVM 分为 ModelViewViewModel

  • Model 代表数据模型,数据和业务逻辑都在 Model 层中定义;
  • View 代表 UI 视图,负责数据的展示;
  • ViewMode 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;

ModelView 并无直接关联,而是通过 ViewModel 来进行联系的,ModelViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。 这种模式实现了 ModelView 的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作 DOM。 在这里插入图片描述

2. Vue2响应式​

响应式原理

vue中采用 数据劫持 结合发布-订阅模式。通过 Object.defineProperty() 对vue传入的数据进行了相应的数据拦截,为其动态添加get()set() 方法。当数据变化的时候,就会触发对应的 set() 方法,当 set() 方法触发完成的时候,内部会进一步触发 watcher,当数据改变了,接着进行 虚拟dom 对比,执行render,后续视图更新操作完毕。

Vue2响应式的创建、更新流程

  • 当一个 vue 实例创建时,vue会遍历 data 中的所有属性,用 Object.defineProperty 给属性设置 getter/setter 方法, 并且在内部追踪相关依赖,在属性被访问和修改时分别调用 gettersetter
  • 每个组件实例都有相应的 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响应式原理的核心就是 ObserverDepWatcher

Observer 中进行响应式的绑定

  • 在数据被读的时候,触发 get 方法,执行 Dep 收集依赖,也就是收集不同的 Watcher
  • 在数据被改的时候,触发 set 方法,对之前收集的所有依赖 Watcher,进行更新。

Vue2 为什么不能监听数组下标的原因

  1. Vue2 使用 Object.definePrototype 做数据劫持实现数据双向绑定的。而数组的下标赋值并不会触发数组对象上的 set() 方法,因此无法直接监听数组下标的变化。
  2. Object.definePrototype 是可以劫持数组的。
  3. 真实情况是Object.definePrototype 本身可以劫持数组,而 Vue2 却没有用来劫持数组。
  4. 原因: a) Vue作者在 issue上说 不使用 Object.definePrototype 直接劫持数组是因为 性能代价和用户体验收益不成正比。 b)Object.definePrototype 是属性级别的劫持,如果使用它来劫持数组的话,一旦用户定义了一个极大数组,就会耗费极大的性能来遍历数组,以及监听每个下标变化的事情上,导致框架的性能不稳定,因此Vue2牺牲一些用户使用的便捷性,提供一个 $set 方法去修改数组,以最大程度保证框架的稳定性。

具体来说,Vue2 的响应式系统会在初始化时遍历对象的属性,并使用 Object.definePrototype 对每个属性添加 getset 方法。这样一来,当属性被访问或修改时,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

平时发送异步请求在哪个生命周期,并解释原因

createdbeforeMountmounted

因为在这3个钩子函数中, data 已经创建,可以将服务端返回的数据进行赋值。

推荐在 created 钩子函数中发送异步请求,因为

  • 能更快获取到服务端数据,减少页面加载时间,用户体验更好。
  • SSR不支持 beforeMountmounted 钩子函数,放在 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 方法的主要实现逻辑如下:

  1. 如果目标是数组,使用Vue中数组的splice()变异方法来更新指定位置的元素并触发响应式更新。(splice变异方法请看上方讲的Vue2重写数组方法源码)
  2. 如果目标对象已经包含了指定的属性,即为响应式,直接赋值。
  3. 如果目标对象没有指定的属性,即新添加的属性不是响应式,Vue会通过defineProperty方法进行响应式处理,并在新的属性上设置gettersetter,以便在属性被访问或修改时触发响应式更新。 总之,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(事件中心)
  • parentparent 或 root
  • attrs 和 listeners
  • provide 和 inject
  • Vuex

组件间通信的分类可以分成以下:

  • 父子关系的组件数据传递选择 props$emit 进行传递,也可以选择 ref
  • 兄弟关系的组件数据传递可选择 $bus,其次可以选择 $parent 进行传递。
  • 祖先与后代组件数据传递可选择 attrslisteners ,或者 provideinject
  • 复杂关系的组件数据传递可通过 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中是 activateddeactivated
  • 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给新增的属性去添加gettersetter
  • 目标是数组,直接调用数组本身的 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道面试题以及答案,不看要遭老罪喽