【前端面经】Vue篇(积极响应更新)

57 阅读6分钟

【概念集】

Vue2的响应式原理

Vue2采用数据劫持结合观察者(订阅-发布者)模式的方法,通过Object.defineProperty()来劫持各个属性的setter、getter、dep,以及Watcher实现依赖手机、派发的过程。

  1. vue将data初始化为一个Observe,并对每个数据绑定setter、getter,data中的每个key,都有独立的dep(依赖收集器);
  2. compile解析模板指令,将模板中的变量替换为数据,初始化渲染页面视图,并将指令对应节点绑定update(),向dep添加监听;
  3. mount时,实例化一个Watcher,将收集器的目标指向当前Watcher,待属性dep.notice()通知,调用自身update(),触发compile的回调;
  4. MVVM作为数据绑定的入口,整合ObserverCompileWatcher,达到数据变化触发视图更新,视图交互变化触发数据Model变更的双向绑定效果。

$set的原理

  • 如果目标是数组,直接使用数组的 splice 方法触发响应式;

  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( 给对象属性采用 Object.defineProperty 动态添加 gettersetter

vue的data为什么必须是一个函数?

主要使用终于js的特性所导致的。在components中,data作为一个函数返回值的形式定义,使组件在复用过程中都是一个新的数据对象,相当于每个组件实例都有自己的私有数据空间

Vue如何监听到数据的变化

  1. 响应式原理
  • 在data中定义一个属性,vue会遍历这些属性并使用Object.defineProperty() 将其转换为getter/setter,使其具有相应性质;
  • 当组件被创建时,vue会编译模板为虚拟DOM渲染函数,当访问数据属性时,则触发属性的getter;
  • 当数据属性发生变化时,vue会触发更新流程,重新运行渲染函数。
  1. 如何监听数据的变化
  • data:响应变化;
  • computed:依赖的数据缓存更新;
  • watch:异步更新;
  • vuex:mutations、store 常用更新处理。

Vue3的响应式原理

Vue3引入了ES6的Proxy对象来实现响应式,通过代理data对象来拦截属性的读写,提供了更强大和灵活的响应式能力。

Vue3还引入了 Composition API,允许使用更函数式的方法来组织组件逻辑,包括使用 refreactive API 来创建响应式数据。

  • Proxy 对象代理Proxy对象能够拦截目标对象的各种属性操作,实现对数据的监听;
  • 响应性处理:通过Proxy代理的对象访问属性时,触发 getter,设置属性时,触发setter;
  • 多层属性嵌套与动态属性监听:Vue3.x的响应式系统能够处理多层属性嵌套的情况,无需手动进行深度监听,对于动态添加的属性,也能够自动进行监听;
  • 数组监听:由于Proxy对象能够直接监听动态添加的属性,所以能够自动监听数组所有增删改的方法;
  • 作为单独模块使用:Vue3.x的响应式系统被封装为一个独立的模块,可以单独使用或者与其他库继承,提供了更多的灵活性和可定制性;
  • 核心函数与APIreactivereftoRefseffectcomputedwatchwatchEffect等;
  • track/ trigger机制:Vue3.x使用了底层的track/ trigger机制来收集依赖和触发更新,当数据发生变化时,trigger函数会遍历所有收集到的依赖,并触发它们的更新;
  • Reflect对象:内置对象,提供了一系列与 Proxy handlers 相对应的方法。Vue3.x在实现响应式系统时使用了Reflect对象来调用目标对象的方法,以确保操作的正确性。

Vue3升级了哪些功能

  • createAPP()
  • emit属性:在子组件中声明emits options父组件的绑定事件;
  • 生命周期:使用 setup 整合 beforeCreatecreated 钩子, destory 修改为 unmount
  • 多事件处理:在点击事件中写入多个处理函数,逗号分割;
  • Fragment:可存放多个根节点;
  • 移除.sync
  • 异步组件写法:需要从Vue 引入 defineAsyncComponent,使用这个函数包裹 import() 引入异步组件;
  • 移除filter,双括号中用 | 分割;
  • Teleport:把组件的嵌套层级提高;
  • Suspense:用来加载异步组件未成功时的一些loading,主要实现原理:具名插槽;
  • Composition API

Vue2和Vue3的区别

  • 响应式:Proxy替代Object.defineProperty
  • Vue3全部由ts重构,对ts更友好
  • 自定义渲染器
  • composition API
  • Vue3可以存在多个根节点

Vue3为什么用proxy改写数据劫持

  1. 更强的的拦截能力:相比较Object.definePropertyProxy是ES6中新引入的特性,有多达13中拦截方法,允许我们自定义行为来拦截和操作对象的增删改等,使得响应式系统能更精确追踪更新;
  2. 支持动态增删元素,并且能保持响应式跟踪,消除了属性级别的劫持开销(Proxy返回的是一个对象,可直接操作);
  3. 性能优化Proxy初始化时只对通过get访问到的属性进行响应式处理,即懒观察或者懒代理,减少不必要操作。
  4. 代码简洁和可维护性Proxy提供了更高级别的抽象,可以简化代码,易于适应变化和扩展需求,提高可维护性。

Vue3相对于Vue2的优势

  1. 性能更好;
  2. 体积更小
  3. 更好的ts支持
  4. 更优的代码组织
  5. 更好的逻辑抽离
  6. 更好的新功能

MVVM的理解

MVVM:是一种简化用户界面的事件驱动编程方式。核心就是ViewModel层,负责转换Model中的数据对象,向上与视图层进行双向数据绑定,向下与Model层通过接口请求进行数据交互,承上启下。

v-model的原理

v-model:语法糖,在表单元素上创建双向数据绑定,在内部为不同的输入元素使用不同的属性并抛出不同的事件。

image.png

虚拟DOM

虚拟DOM,即用JS模拟一棵DOM树,放在浏览器内存中。

当需求变更时,虚拟DOM使用diff算法进行新旧比较,将变更结果放在队列中,patch算法,将差异应用到真实的dom树上,减少页面dom操作和渲染。

优点

  1. 保证性能下限:虚拟DOM需要适配任何上层API可能产生的操作,所以它属于普适的,所以性能不是最优的;
  2. 无需手动操作DOM
  3. 跨平台:虚拟DOM实际上是JS对象,而DOM与平台强相关,因此,虚拟DOM可以更方便进行跨平台操作,比如服务器渲染,weex开发等;

缺点

  1. 无法进行极致优化:首次渲染大量DOM时,由于多了一层计算,会比innerHTML插入慢。

对diff算法的理解

diff算法,也称为“虚拟DOM差异算法”,是Vue实现高效DOM更新机制的核心部分。即:比较新旧两棵虚拟DOM树,将差异应用到实际DOM上,从而避免不必要的DOM操作,提高性能。

  • 双端比较:同层节点;
  • 深度有限遍历:先比较当前节点的子节点,再比较兄弟节点;
  • key比较:不同则直接创建新DOM;
  • 优化策略:同层比较,先头后尾,复用策略等;

Vuex

全局状态管理系统,用于多个组件中的数据共享、缓存等。每一个Vuex应用的核心就是store(仓库)。

响应式的;无法持久化,内部核心原理是创造一个全局实例new Vue()

Vue的编译流程

  1. parse阶段:使用正则对template模板进行解析,将标签、指令、属性等转化为AST抽象语法树;
  2. optimize阶段:遍历AST,对静态节点进行标记和提升,优化runtime的性能;
  3. generate生成阶段:将AST转化为render函数字符串。

Vue生命周期

8个生命周期,分别为4个阶段(创建、载入、更新、销毁)的前后。

  • new Vue()实例化初始配置(created阶段,vue实例数据对象data可使用,$el还未有);
  • 生命周期钩子函数注册;
  • 不同阶段调用触发(- beforeMounte阶段,data$el可使用,但是还是虚拟DOM节点,data.message还未替换;);
  • 原型链和继承:通过Vue的组件系统来管理、访问;
  • 对于异步组件,加载完成后触发特定钩子

父子组件什么周期执行顺序

  • 加载渲染:父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
  • 更新:父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
  • 销毁:父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

在哪个生命周期内调用异步请求

建议created钩子函数中调用:

  • 能更快获取到服务端数据,减少页面loading时间;
  • ssr不支持beforeMountmounted钩子函数,所以放在created能保持一致性;

Vue3生命周期

  • Options APIbeforeDestory 改为 beforeUnmountdestory 改为 unmounted; 其它沿用Vue2 生命周期;
  • Composition API:setup 相当于整合了 beforeCreatecreated。其它生命周期分别是卸载 setup中的函数
    • onBeforeMount()
    • onMounted()
    • onBeforeUpdate()
    • onUpdated()
    • onBeforeUnmount()
    • onUnmounted()

使用

  • 不建议共有,会引起混乱;
  • 小型项目、业务逻辑简单,用 Options API;
  • 中大型项目、逻辑复杂,用 Composition API;

宏任务和微任务

宏任务和微任务都是异步任务的分类,在JS引擎中执行方式和优先级不同。

宏任务

主线程上执行的代码块,包括:主代码块、定时器、UI渲染、时间等,这些任务会按照他们在代码中的顺序被添加至宏任务队列,等待执行;

微任务

更小、更轻量级的任务,通常不会阻塞主线程的执行,包括:Promise的then和catch,async中的await、MutationObserve监听器等。

当宏任务执行完毕后,js会立即执行所有已添加到微任务队列中的任务。

为什么优先解决微任务?

微任务具有更高的执行优先级。

在JS事件循环中,每当一个宏任务执行完毕后,js会立即执行所有已添加到微任务队列中的任务,然后再去执行下一个宏任务。这样能确保微任务尽快得到处理,从而提高应用程序的响应速度和性能。

此外,优先解决微任务还能避免一些潜在问题。例如:宏任务中执行了异步操作(如定时器或者网络请求),并且该异步操作的回调函数被添加到了微任务队列。那么如果这个回调函数依赖于宏任务中的某些数据或者状态,那么优先解决微任务可以确保这些回调函数能够在正确的上下文执行。

总之,在编写代码时,需要注意避免在宏任务中进行耗时操作,以免影响其它任务的执行。

vue的nextTick原理,为什么是先用promise,mutationObserver再用setTimeout和setInterval

概念

$nextTick():会尝试使用多种策略来确保在DOM更新循环结束后,调用异步方法执行延迟回调(微任务优先)。

使用策略

使用什么策略,由浏览器的兼容性和执行效率考虑,Vue也会缓存最佳效率,重复使用。

  • Promise.resolve().then():同步代码执行完毕后立即执行该微任务(不可靠);
  • MutationObserve:监听DOM变化的接口(不是所有浏览器支持);
  • setImmediate:与setTimeout类似,但会在回调函数放入一个单独的队列中,在下一轮事件循环开始前执行(浏览器支持,会尝试使用它提高执行效率);
  • setTimeout:以上不可用,会回退使用setTimeout(可能会延迟,但没办法啊~);

$nextTick()使用场景

$nextTick()主要是下下次DOM更新循环结束后执行回调函数,用处理DOM更新后的操作获取到DOM最新的状态。使用场景:

  • 异步更新DOM;
  • 更新后的DOM操作:获取DOM的尺寸等;
  • 批量更新优化:对多个数据变化进行批量处理,提高性能。

设计模式有哪些,发布者订阅者模式和观察者模式区别,发布者可以直接调订阅者方法吗

设计模式

  1. 工厂模式:传入参数即可创建实例(eg:虚拟DOM根据参数的不同返回基础标签 VNode 和组件 VNode );
  2. 单例模式:整个程序仅有一个实例(eg:vuex 和 vue-router 的插件注册方法 install ,判断如果系统存在实例就直接返回掉);
  3. 观察者模式:响应式数据原理;
  4. 策略模式:指对象的某个行为,在不同场景中有不同的实现方案(eg:选项的合并策略);
  5. 代理模式proxy

SPA单页面、多页面应用

SPA应用:只有一个主页面,初始化的时候加载相应的HTML、JS和CSS。一旦页面加载完成,页面不会因为用户的操作而进行重新加载或跳转,仅利用路由机制实现HTML内容的变化。

优点

  1. 用户体验好、快,内容的改变不会重加载页面;
  2. SPA相对对服务器压力小;
  3. 架构清晰

缺点

  1. 初次加载耗时长;
  2. 前进、后退路由管理需自己建立堆栈管理;
  3. SEO难度大;

多页面应用:一个应用多个页面,页面跳转即整页刷新。不利于SEO,导航不可用,初次加载耗时少,但是页面复杂度高。

Vue的单项数据流(props数据流向)

概念:指父组件通过props传递数据给子组件,且父级 prop 的数据更新会流向子组件中,但不会逆向传递。

作用:避免子组件意外修改父组件状态的情况,维护了应用数据流的可追踪和可维护性。

Vue组件通讯

  • props$emit
  • $on$off$emit:自定义;
  • $refs
  • $parent$child
  • vuex
  • provideinject:常用于组件库编写

$on$emit

  1. 基于发布订阅模式;
  2. $on订阅者, 用于收集事件依赖;
  3. $emit发布者, 根据传入的event在vm_events找到对应的事件,并执行invokewithErrorHandling()(通过 handler.apply(context, args) 和 handler.call(context) 的形式执行对应的方法);

vue-router

路由钩子函数导航守卫。允许在路由发生变化前后处理权限验证、数据预加载等;

分类

  • 全局守卫
    • beforeEach:全局前置守卫,在路由跳转前触发;
    • beforeResolve:全局解析守卫,在路由开始解析之前触发,在beforeEach和组件内的beforeRouterEnter之后;
    • afterEach:全局后置守卫,在路由跳转之后触发。
  • 路由独享守卫
    • beforeEnter:进入路由前触发,只在某个路由下有效;
  • 组件内的守卫
    • beforeRouterEnter:在渲染组件的对应路由被确认前被调用;
    • beforeRouterUpdate:在当前路由被改变,但是该组件仍然被复用时调用。例如带有动态参数的路由,组件的实例已经存在,对这个这一件调用;
    • beforeRouterLeave:导航离开该组件的对应路由时调用。

调用顺序

beforeEach → beforeEnter → beforeResolve→ beforeRouterEnter → beforeRouterUpdate → afterEach

vue-router组件复用导致路由参数失效怎么办

  • 监听路由变化;
  • 使用 :key 属性阻止复用:会影响性能;
  • 动态路由匹配
  • 使用query代替params:query会附加唉URL的查询字符串中

hash模式和history模式实现原理

  1. hash模式:基于location.hash实现的,它的值就是#后面的内容。
  2. history模式:由HTML5共的History API来实现的。主要特性:
  • pushState()replaceState()来操作URL变化;
  • 使用popstate事件监听url变化,从而对页面进行渲染/跳转;;
  • pushState()replaceState()不会触发popstate事件,需要手动触发页面渲染/跳转;

【属性集】

keep-alive

keep-alive:开发中缓存不活动的组件实例使用,使组件切换过程中,将状态保留。

原理

keep-alive内部定义了一个map,缓存创建过的组件实例,返回的渲染函数会查找是否存在。

  • 由于componentis属性是一个响应式数据,因此只要变化,便会执行render
  • vue3 中结合 vue-router 时变化较大,之前是 keep-alive 包裹 router-view,现在需要反过来用 router-view 包裹 keep-alive
  • exclude优先级高于include;
  • 对应钩子函数
    • beforeRouter:进入路由的时候都会执行;
    • activated:在keep-alive缓存的组件被激活的时候执行(如果需要在每次进入页面获取最新的数据,需要在activated钩子获取最新的数据,承担原来的created钩子中获取数据的任务);
// max:限制缓存组件的最大数量
<keep-alive include='' exclude='' max=3></keep-alive>

LRU(Least Recently Used) 算法

LRU,即最近最少使用的缓存机制。以关键字key操作。缓存容量达到上限的时候,它应该在写入新数据之前删除最久未使用的数据值,提供新空间;

v-for和v-if可以混合使用吗

可以,但是不建议使用。

v-for的优先级会高于v-if,即先渲染后判断,会增加不必要的性能消耗。更好的解决方案是使用computed处理数据再使用。

v-key的使用

Vue在渲染和更新时会用diff算法做最大限度的减少动态元素操作,尽可能实现就地修改、复用相同类型元素。

因此,key的标记作用能够使判断更准确、更快速。

【操作集】

mixins是什么?Vue3用了什么取代

mixin是多组件之间的相同代码逻辑的抽离混入。分为局部混入全局混入,提供了分发vue组件中的可复用功能。一个mixin对象可以包含任何组件选项。

缺点

  1. 来源不明确;
  2. 多mixin可能会造成命名冲突;
  3. 迷信和组件可能出现多对的多的关系,复杂度比较高;

Vue3使用了 composition API 替代了mixin 的使用

  1. 代码提取;
  2. 代码复用;
  3. 命名冲突解决

computed 和 watch 的使用

  • computed:计算属性,存在数据缓存,能够优化一定的计算性能,内部实际是一个函数,根据需要做回调计算返回计算结果;
  • watch:用于观察和响应数据变化的特性。无缓存,支持深度监听。异步或者开销较大的操作,可以适当使用watch

页面切换保存怎么实现数据不丢失且记忆滚动条位置,常规的方案会闪一下,不如原生性能,怎么解决?

  1. 确保数据不丢失,方案:
  • Vue路由功能:keep-alive;
  • Vuex:数据缓存;
  • 本地存储数据:localStoragesessionStorage
  1. 页面切换,记录DOM元素滚动条位置:
  • 在Vue中监听滚动事件,保存滚动位置,重新加载时,拉取数据做恢复;
  • 使用vue-router中的scrollBehavior()方法:可以再该方法中判断当前路由需要记忆滚动条位置与否来设置相应的滚动行为;
  1. 解决切换页面闪烁问题:
  • 优化DOM渲染:尽量避免在页面切换时进行大量的DOM操作,也少页面的重绘和重排;
  • 使用transation组件实现过渡效果,平滑展示;
  • 调整对话框组件的渲染策略:例如使用CSS的will-change属性来提前告知浏览器哪些元素会发生变化,或使用Vue的过渡效果来实现动画切换。

自定义组件封装和注册

分别有Vue.extend() - 创建组件构造器和Vue.component() - 全局注册组件;

要求

  • 组件独立;
  • props进行父子组件通信;
  • events$on$emit )进行子父组件通信;
  • 插槽
  • 性能优化:避免复杂计算和循环;

Vue.extend()

创建“子类”(可复用的组件构造器)。

var MyComponent = Vue.extend({ 
    template: '<div>Hello, World!</div>' 
})

// 使用 new 关键字创建实例 
var myComponentInstance = new MyComponent();
myComponentInstance.$mount();

document.body.appendChild(myComponentInstance.$el)

Vue.component()

Vue.component('my-component', { 
    template: '<div>Hello, World!</div>'
}) 
// 在Vue实例的模板中使用 
new Vue({
    el: '#app', 
    template: '<myComponent />' 
})

自定义指令

Vue.directive():即注册一些自定义的DOM行为。Vue3使用app.directive()

钩子函数

  • bind:仅执行一次;
  • inserted:被绑定元素插入到父节点中时触发(仅保证父节点存在,不一定已被插入到文档中);
  • update:当组件重新渲染,导致指令绑定的值发生变化时触发;
  • componentUpdated:组件更新完毕,DOM也更新后触发;
  • unbind:只触发一次;

注意:

  • 自定义指令专注于操作DOM,而不是更改数据或者处理复杂逻辑;
  • 当与Vue的响应式系统交互时,要当心updated钩子的使用,有可能在一个生命周期多次调用;

transition标签的使用

<transition> 组件为元素和组件提供过渡效果,可自定义或者使用内置过渡效果。

使用

  • name属性:自定义过渡类名:xxx-enter-active等;
  • <transition> 包裹,使用 CSS 来定义过渡的样式,如 .v-enter-active.v-leave-active 等类名。
  • 过渡模式(mode属性)out-in表示离开-进入过渡;in-out表示离开-进入过渡;
  • duration:过渡持续时间,ms为单位,:duration="{ enter: 500, leave: 800 }"
  • JS 钩子:@before-enter
  • 列表过渡:<transition-group>,并为其指定tag属性,默认span,即渲染的根元素。

ref和reactive区别

refreactive都是用于创建响应式数据的API。不同:

  • 数据类型
    • ref:主要用于包装基本数据类型,以及单一对象;
    • reactive:仅支持数组、对象等复杂类型数据。
  • 使用方式
    • ref:在模板中,会被自动解包,可直接使用;在JS代码中,需要通过.value访问和修改;
    • reactive:在模板和JS代码中,均可直接使用;
  • 创建方式:差不多
  • 性能:处理大量数据或者复杂逻辑时,使用reactive更高效;

toRef、toRef的区别

  • toRef
    • 针对一个响应式对象(reactive封装)的 prop
    • 创建一个 ref,具有响应式
    • 两者保持引用关系
  • toRefs
    • 将响应式对象(reactive封装)转换为普通对象
    • 对象的每个prop 都是对应的 ref (不然reactive响应式直接解构会失去响应式)
    • 两者保持引用关系

Vue项目优化

基础的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析
  • Vue 项目的编译优化