面试必背系列-Vue

131 阅读12分钟

一、Vue 基础

1. 双向数据绑定的原理

概念:

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

原理:

其中,View 变化更新 Data ,可以通过事件监听的方式来实现

Vue 主要通过以下 4 个步骤来实现数据双向绑定的:

实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。

实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

作用:: v-model实现表单数据双向变化,开发更高效。

2. Computed 和 Watch 的区别

总结:

  • computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
  • watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

3. 常见的事件修饰符及其作用

  • .stop:防止事件冒泡;
  • .prevent :阻止默认行为;
  • .capture :与事件冒泡的方向相反,事件捕获由外到内;
  • .self :只会触发自己范围内的事件,不包含子元素;
  • .once :只会触发一次。

4. 对keep-alive的理解?

概念: Vue.js 中的 keep-alive 是一个非常有用的组件,它可以缓存被包裹的组件的状态,避免每次切换时都重新渲染,从而提升页面性能。

原理: keep-alive 的原理主要是利用了 Vue.js 的抽象组件和生命周期钩子函数。当一个组件被包裹在 <keep-alive> 中时,它实际上会被作为一个抽象组件,不会直接渲染到页面上。

当组件第一次渲染时,createdmounted 生命周期钩子函数会被调用。而当组件被包裹在 keep-alive 中时,它将多出两个生命周期钩子函数:activateddeactivated

  • activated 钩子函数会在组件被激活时调用,即切换到包含该组件的页面时。
  • deactivated 钩子函数会在组件被停用时调用,即切换到不包含该组件的页面时。

keep-alive 通过监听路由变化来判断何时激活或停用组件。当切换到包含被缓存组件的页面时,activated 钩子函数会被调用,此时组件的状态得以保留。而当切换到不包含被缓存组件的页面时,deactivated 钩子函数会被调用,组件被停用,不再占用内存。

作用: KeepAlive组件通过缓存已经渲染的组件,可以提升应用的性能。但是,过多地使用KeepAlive组件可能会导致内存占用增加,因此需要谨慎使用。只在真正需要保持组件状态的场景下使用KeepAlive组件。

5. Vue模版编译原理

vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。

  • 解析阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。
  • 优化阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。
  • 生成阶段:将最终的AST转化为render函数字符串。

6. Vue的性能优化有哪些

(1)编码阶段

  • 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
  • v-if和v-for不能连用
  • 如果需要使用v-for给每项元素绑定事件时使用事件代理
  • SPA 页面采用keep-alive缓存组件
  • 在更多的情况下,使用v-if替代v-show
  • key保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

(2)SEO优化

  • 预渲染
  • 服务端渲染SSR

(3)打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用cdn加载第三方模块
  • 多线程打包happypack
  • splitChunks抽离公共文件
  • sourceMap优化

(4)用户体验

  • 骨架屏
  • PWA
  • 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。

二、生命周期

1. 说一下Vue的生命周期

Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。

  1. beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。
  2. created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。
  3. beforeMount(挂载前) :在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。
  4. mounted(挂载后) :在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。
  5. beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。
  6. updated(更新后) :在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
  7. beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。
  8. destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。

另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。

三、组件通信

总结

组件通信类别?

答:主要分三类父子通信隔代通信跨级通信;

父子通信

props / emitemit、parent / $children、ref

perl
复制代码
// 1 props/$emit
父组件向子组件传值: props;
子组件向父组件传值: $emit;

// 2 $parent/$children
通过$parent和$children就可以访问组件的实例。
($parent和$children的值不一样,$children 的值是数组,而`$parent`是个对象)

// 3 ref
普通的DOM元素上使用,引用指向的就是DOM元素;
如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据。
隔代通信

attrs/attrs / listeners、provide / inject

ruby
复制代码
// 1 $attrs/$listeners

// 2 provide/inject
父组件中通过`provide`来提供变量, 然后再子组件中通过`inject`来注入变量。
缺点:provide 和 inject 绑定并不是可响应的。
跨级通信

eventBus、Vuex、localStorage / sessionStorage、路由传参。

makefile
复制代码
// 1 eventBus
所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件。
缺点:当项目较大,难以维护。

// 2 Vuex
集中式存储管理应用的所有组件的状态。

// 3 localStorage / sessionStorage
localStorage: 生命周期是永久。
sessionStorage: 生命周期为当前窗口或标签页。
缺点:数据和状态比较混乱,难以维护。

// 4 路由传参
缺点: 仅跳转。

四、路由

1. 路由的hash和history模式的区别

Vue-Router有两种模式:hash模式history模式。默认的路由模式是hash模式。

1. hash模式

简介: hash模式是开发中默认的模式,它的URL带着一个#,例如:www.abc.com/#/vue,它的hash值就是#/vue

特点:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。

原理: hash模式的主要原理就是onhashchange()事件

javascript
复制代码
window.onhashchange = function(event){
	console.log(event.oldURL, event.newURL);
	let hash = location.hash.slice(1);
}

使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。

五、Vuex

1. Vuex 的原理

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。

b025e120ca3d0bd2ded3d038d58cacf4.jpg Vuex为Vue Components建立起了一个完整的生态圈,包括开发中的API调用一环。 (1)核心流程中的主要功能:

  • Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;
  • 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;
  • 然后 Mutations 就去改变(Mutate)State 中的数据;
  • 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。

(2)各模块在核心流程中的主要功能:

  • Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
  • dispatch∶操作行为触发方法,是唯一能执行action的方法。
  • actions∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
  • commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
  • mutations∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
  • state∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
  • getters∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。

六、Vue 3.0

1. Vue3.0有什么更新

Setup、Typescript、Proxy、Composition、性能优化。

setup: 新增生命周期setup,setup执行的时机是在beforeCreate生命函数之前执行,使用vue3时候,建议用setup代替beforeCreate、created。

Typescript: Ts强类型语言,可以清晰的了解数据结构等与编译器相结合,可以在编写阶段就报错,代码效率有了极大提升。

Proxy: 在vue2中使用Object.defineProperty的时候,遇到的问题有:

  • 一次只能对一个属性进行监听。(遍历监听所有属性)
  • 在遇到一个对象的属性还是一个对象的情况下,需要递归监听。
  • 对于对象的新增属性,需要手动监听。(vue$set方法)
  • 对于数组通过push、unshift方法增加的元素,也无法监听。(对数组无法监听的方法重写)

这些问题在Proxy中都轻松得到了解决

语法:const p = new Proxy(target, handler) 参数:

  1. target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  2. handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
javascript
复制代码
//定义一个需要代理的对象
let person = {
    age: 0,
    school: '西电'
}
//定义handler对象
let hander = {
    get(obj, key) {
        // 如果对象里有这个属性,就返回属性值,如果没有,就返回默认值66
        return key in obj ? obj[key] : 66
    },
    set(obj, key, val) {
        obj[key] = val
        return true
    }
}
//把handler对象传入Proxy
let proxyObj = new Proxy(person, hander)

// 测试get能否拦截成功
console.log(proxyObj.age)//输出0
console.log(proxyObj.school)//输出西电
console.log(proxyObj.name)//输出默认值66

// 测试set能否拦截成功
proxyObj.age = 18
console.log(proxyObj.age)//输出18 修改成功

Composition: 组合API,类似于React Hook,书写自由,简化逻辑复用。

性能优化:

  • 引入了Tree-Shaking,Tree-Shaking带来的bundle体积更小是显而易见的。
  • nextTick、set、delete等要使用es6引入,打包时对没有用到API进行剔除。
  • vdom的重写,例如编译模板的静态标记不再进行diff比较、事件缓存等。

七、虚拟DOM

1. Virtual DOM?

什么是Virtual DOM?

答:将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。

Virtual DOM的作用?

答:避免昂贵的DOM操作与大量无谓的计算量,通过diff算法,一边比较新旧节点一边给真实的DOM打补丁。

diff的原理?

在新老虚拟DOM对比时:

  • 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
  • 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
  • 匹配时,找到相同的子节点,递归比较子节点

在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

diff的比较方式?

答:前端操作dom的时候了,不会把当前元素作为上一级元素或下一级元素,很少会跨越层级地移动DOM元素,在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较,因此复杂度的问题转换成 O(n) 复杂度的问题。

diff的弊端?

答:相同⽗元素的⼦元素必须有独特的 key,否则重复的 key 会造成渲染错误。

Vue vs React
  • 相同点

    • 都是两组虚拟dom的对比(react16.8之后是fiber与虚拟dom的对比)
    • 只对同级节点进行对比,简化了算法复杂度
    • 都用key做为唯一标识,进行查找,只有key和标签类型相同时才会复用老节点
    • 遍历前都会根据老的节点构建一个map,方便根据key快速查找
  • 不同点

    • react会自顶向下全diff.
    • vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
    • react在diff遍历的时候,只对需要修改的节点进行了记录,形成effect list,最后才会根据effect list 进行真实dom的修改,修改时先删除,然后更新与移动,最后插入
    • vue 在遍历的时候就用真实dominsertBefore方法,修改了真实dom,最后做的删除操作
    • react 采用单指针从左向右进行遍历
    • vue采用双指针,从两头向中间进行遍历
    • react的虚拟diff比较简单,vue中做了一些优化处理,相对复杂,但效率更高

2. Vue中key的作用

vue 中 key 值的作用可以分为两种情况来考虑:

  • 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
  • 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速

  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快