Vue

面试
Vue

响应式原理

数据劫持和发布订阅模式;

过程

使用 DefineProperty 劫持所有 Data 对象的属性,使其成为响应式属性;一旦属性有了自定义的 getter 和 setter,它就可以在被访问或修改的时候执行自定义的逻辑(响应式);

创建一个 Dep 类(管理属性的依赖关系),每个属性有一个对应的 dep 实例,用于管理该属性的所有依赖(订阅者);同时,dep 实例还会维护一个 Subscribers 数组,其中存储了所有订阅该属性的订阅者;

在属性的 getter 方法中,调用 depend 方法,作用是收集依赖(谁用了这个属性,谁就是这个属性的依赖,会被添加到 Subscribers 数组里),当属性改变时,这些依赖就需要被通知到;

在属性的 setter 方法中,调用 dep.notify 方法,作用是通知更新(遍历数组,调用每个订阅者的 update 方法来更新视图);

这样就实现了响应式;

Vue3 响应式原理

1、使用 proxy 实现响应性

Vue3 使用 Proxy 来代理用户的数据对象,和 DefineProperty 相比,Proxy 可以以更细粒度和更自然的方式拦截对象的各种操作,包括属性读取、属性设置、删除属性等操作。

2、基于 refreactive 的 API

ref 和 reactive 的底层依赖于 Proxy。

ref:Vue 会把提供的值包裹在一个对象里,并使用 Proxy 对该对象进行代理。

reactive:创建响应式对象时,每个属性的读取和修改都会被这个 Proxy 拦截。

3、Effect 依赖跟踪

Vue 的响应系统中,当一个响应式对象被读取时,系统会自动跟踪这个操作,并记录当前执行的响应式函数,当对象的属性被修改时,所有依赖于这个属性的响应式函数会重新执行。

4、优化批处理更新

当多个状态发生更改时,Vue 将更新合并到一起异步执行,减少重复渲染,提高性能。

虚拟 DOM

用【一个 JS 对象来表示 DOM 节点】,避免操作真实的 DOM;数据变化时,通过虚拟 DOM 的 diff 算法【计算出新旧虚拟 DOM 的差异】,然后更新需要更新的试图;

优点

减少 DOM 操作,专注数据和状态;

虚拟 DOM 相当于一个中间层,框架在这个层面上进行各种优化,比如局部更新、异步渲染,同时也可以给框架提供一些新的特性和功能;

Diff 算法

对比新旧虚拟 DOM 树,找到哪些节点发生了变化,共有三种情况:

1、老节点里新树不存在,删掉这个节点

2、新节点里老树不存在,新增这个节点

3、节点都存在,进一步比对这个节点的属性和子节点是否变化,子节点继续递归进行对比,层层深入,找出所有变化的地方。把需要变化的地方反应到真实的 DOM 上,这样就完成了页面的更新。

Vue3 虚拟 DOM

Diff 算法优化

Vue2 中,更新组件时,Vue 会进行全组件树的重新渲染和虚拟 DOM 重建,然后通过 Diff 算法比较新旧 DOM 树,以确定哪些部分需要在真实的 DOM 上进行更新。Vue3 采用了 block tree 的结构,在处理更新的时候,可以精确地跳过不需要更新的静态节点,只对动态节点进行更新。

在编译时标记静态节点(纯文本、不会更改的 DOM 结构),并在运行时跳过处理

MVVM

M:数据模型,对应 data 中的数据;

V:视图模板,对应 template 中的 HTML;

VM:Vue 实例对象,包括数据绑定、DOM 更新机制等;

MVVM 是一种架构模式,优点是:

1、分离视图和业务逻辑(VM 作为中间层,减少 V 和 M 的耦合)

2、复用性更好,提升开发效率

3、数据双向绑定,VM 层实现 V 和 M 的自动更新,开发者只需要关注业务逻辑;

组件传值

父子:props;provide/inject;emit/on;refs;

同级:event-bus;vuex;

Vue3 组件传值

props

父:<ChildComponent :message="parentMessage" >

子:const props = defineProps(['message'])

refs

父组件无法直接使用子组件里的内容,子组件可以通过 defineExpose 显式暴露出去,然后父组件再使用 refs 调用

emit/on

const {emit} = defineEmits(['customEvent'])

emit('customEvent', 'data from child')

Computed 和 Watch

computed (计算)

根据其他值来计算一个新值;这个值会缓存起来,当计算值发生变化会返回新的内容;

watch (监听)

监听单个属性的变化,触发回调函数里的操作;

深度监听:

默认 - 监听对象引用地址;

deep:true - 监听对象内属性内部的变化;

DefineProperty 的局限性:只能劫持对象的直接属性,而不能自动地劫持这些属性的内部属性,这意味着深层的数据变更不会触发视图的更新,除非手动设置 deep:true。

Vue3 Computed 和 Watch

computed

import { ref, computed } from "vue";
const count = ref(1);
const doubled = computed(() => count.value * 2);

watch

import { ref, watch } from "vue";
const count = ref(0);
watch(
	count,
	(newValue, oldValue) => {
		console.log(`count changed from ${oldValue} to ${newValue}`);
	},
	{
		immediate: true,
		deep: true,
	}
);

在 Vue3 里,Proxy 可以在对象级别进行拦截,而不是属性级别,这意味着无论数据结构多复杂,Vue 都可以监听到任何级别的改变。

Object 和 Array

Vue 对 Object 的观测

解决方式:Vue.setVue.delete

Vue.set 原理

对于已创建的对象,新添加的属性不具备响应式,Vue.set 可以判断这个属性是不是新添加的属性,是不是具备响应式,如果不是,则用 DefineProperty 给这个属性添加响应式

Vue 对 Array 的观测

DefineProperty 不能监听 Array 的 Push、Pop、Splice 等方法,这些方法直接修改原数组,不会触发属性的 Set,所以 Vue 对这些方法进行了封装,让它们可以触发属性的 Set 方法;

但是直接【修改数组下标】或者【获取 length】不会被检测到;

解决方式:使用 Vue.set;splice;

Key

相当于给节点提供唯一 ID,让 Vue 可以跟踪节点;Diff 的时候通过比较新旧 DOM 树的 Key 属性,精确地确定那些节点是新增、更新、删除的,提高 Diff 的效率;

在列表或循环中:如果没有 Key 属性,Vue 无法判断节点的变化情况,可能会出现性能问题;有 Key 属性,Vue 可以确保正确重用和就地更新节点;

异步组件

动态 import:

const AsyncComponent = () => import('./AsyncComponent.vue')

NextTick

有时候数据变了页面还没来得及渲染,比如,数据变化后立即进行一些 DOM 操作,这时候就需要 NextTick 方法,确保页面更新了才会执行 NextTick 里的回调函数;

原理:利用事件循环机制,把回调函数推迟到下一个事件循环中执行;

Vue.use

插件安装,调用 plugin 对象的 install 方法,install 方法里面就是插件的主要功能;

export default {
	install(Vue) {
		Vue.prototype.hello = () => alert("hello");
	},
};
import Plugin from "./plugin";
Vue.use(Plugin);

new Vue()

1、初始化生命周期、事件、render 函数

2、调用 beforeCreate 钩子函数

3、初始化 state,包括 data、props、computed

4、调用 created 钩子函数

5、调用 vm.$mount 挂载渲染

Vuex

状态管理;应用中的所有状态都可以在 store 中管理;

和全局对象的区别

1、状态是响应式的;

2、不能直接改变状态,只能显式提交 mutations,如果是异步就 dispatch actions,然后再提交 mutations;

核心概念

state - 存储应用里的状态数据

getter - 从 state 里派生出一些状态,比如过滤、计数之类的操作,它像 store 的计算属性

computed 里加如 mapState 或者 mapGetters 获得 state

mutations - 通过 this.$store.commit(’mutationsA’),触发 mutationsA 的执行,同步修改状态

actions - 通过 this.$store.dispatch(’actionsA’, product),触发 actionsA 的执行,它会执行一些异步操作,然后再 commit 一个 mutations 来修改状态

methods 里加入 mapMutations 和 mapActions,改变 state

modules - 把 store 分割成模块

为什么在 actions 里进行异步操作?

1、因为 mutations 必须同步执行,这样才能保证每个 mutation 执行完成后对应一个新的状态,devtool 才可以快照保存下来,方便调试。

2、mutation 和 action 接受的参数不一样,action 里的 commit 参数本身的含义就是提交,相当于它是有一个顺序的,对代码的可维护性也有约束。

双向绑定和 VUEX 是否冲突

立足点不同,单向数据流是统一的 state,目的是追溯 state 的改变

双向绑定更多的是组件内部的数据。

Router

前端路由的本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面;

hash 和 history 模式

hash:#后面的字符就是 hash,用 window.location.hash 读取,onhashchange 事件监听。

history:H5 的 History API,可以通过 pushState;replaceState 对浏览器历史记录栈进行修改,可以通过 popState 事件监听到状态变更。

访问二级页面的时候,做刷新操作,会出现 404 错误,那么就需要和后端人配合让他配置一下 apache 或是 nginx 的 url 重定向,重定向到你的首页路由上就好了。

route 和 router

route - 路由信息对象,path、params、query、name

router - 路由实例,路由跳转方法、钩子函数

路由钩子函数

全局

beforeEach;afterEach;路由变化前后;

  • before - 验证用户是否有权限访问
  • after - 用于记录用户的访问记录

组件

beforeRouteEnter;beforeRouteLeave;进入路由前后

  • enter - 获取数据或进行异步操作
  • leave - 提示用户是否保存未提交的数据

Vue2 → Vue3

1、性能

a. 虚拟 DOM 算法重写,减少内存消耗(静态子树,减少不必要的比较和更新操作)

b. Tree-shaking 支持,减小应用体积(未使用的模块不会被包含在最终的打包文件中)

c. Fragment、Teleport、Suspense 等新组件;

2、Composition API:解决 Option API 里的代码组织和逻辑复用的问题;

3、TypeScript 支持;

4、响应式系统优化:Proxy 代替 DefineProperty,可以直接观察更多种数据结构,比如 Map、Set 等;

5、其他

a. 多根节点组件;

b. 支持 ES6 Module;

移除的 API

过滤器

推荐使用计算属性或函数方法来达到同样的效果

v-on:keyup.enter 等修饰符

改为@keyup.enter 语法(监听回车事件)

全局方法移除

Vue.set 和 Vue delete 不再需要,reactive 和 ref 可以对数组和对象进行监听

Vue.prototype.$bus 移除

可以使用新的 mitt 库,或创建自定义事件总线

Vue3 响应式

Proxy 结合 Reflect

Reflect 的作用:修正 Proxy 的 this 指向问题

JS 里的 this 在不同上下文中可能指向不同的对象,为了让 Proxy 能够正确处理上下文,所以采用了 Reflect(Reflect 方法可以明确处理上下文)。

另外,Reflect 提供了很多底层操作,可以更精细地控制 Proxy 对象的行为。

惰性响应式

Vue2 的响应式在创建时会被代理和追踪

Vue3 的响应式对象只有在首次访问某个属性时才会进行追踪

Vue3 对 Object 的处理

使用 Reactive 对一个对象进行代理时,它会递归地对对象的所有嵌套属性进行代理。

使得任何属性的访问和修改都能触发代理的拦截器,同时,Vue3 还使用了 Reflect.get 来判断当前属性的值是否为对象。如果是对象则再次使用 Reactive 进行递归代理。

Vue3 对 Array 的处理

Vue3 对数组的原型上的方法也进行了重写,因为 push pop 等方法操作响应式数组对象时会隐式地访问和修改数组的 length 属性,需要让这些方法间接读取 length 时禁止进行依赖追踪。

Vue3 响应式对 Set 和 Map 的处理

无法直接拦截,包装在 Proxy 中实现响应式处理,通过 toRaw 方法返回原始对象

defineProperty 也无法拦截,可以通过手动调用 $forceUpdate() 更新视图

Vue3 解构丢失响应式

引用类型保持响应式,基本类型丢失响应式(无法被 Proxy 对象监听)

解决方式:toRefs 函数