2023 前端 vue 面试题(边面边更)
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 对象来获取对应节点,比遍历方式更快
为什么 vue 组件中 data 必须是一个函数?
对象为引用类型,当复用组件时,由于数据对象都指向同一个 data 对象,当在一个组件中修改 data 时,其他重用的组件中的 data 会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object 的实例),引用地址不同,则不会出现这个问题。
Computed 和 Watch 的区别
对于 Computed:
它支持缓存,只有依赖的数据发生了变化,才会重新计算
不支持异步,当 Computed 中有异步操作时,无法监听数据的变化
computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 声明过,或者父组件传递过来的 props 中的数据进行计算的。
如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed
如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法。
对于 Watch:
它不支持缓存,数据变化时,它就会触发相应的操作
支持异步监听
监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
当一个属性发生变化时,就需要执行相应的操作
监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
immediate:组件加载立即触发回调函数
deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep 无法监听到数组和对象内部的变化。
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用 watch。
总结:
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
运用场景:
当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
Vue 的生命周期方法有哪些 一般在哪一步发请求
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有nextTick 来访问 Dom
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面 loading 时间;
ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
Vue.js 的 template 编译
简而言之,就是先转化成 AST 树,再得到的 render 函数返回 VNode(Vue 的虚拟 DOM 节点),详细步骤如下:
首先,通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创建编译器的。另外 compile 还负责合并 option。
然后,AST 会经过 generate(将 AST 语法树转化成 render funtion 字符串的过程)得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,里面有(标签名、子节点、文本等等)
参考 前端进阶面试题详细解答
MVC 和 MVVM 区别
MVC
MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范
Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据
View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的
Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据
MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。
MVVM
MVVM 新增了 VM 类
ViewModel 层:做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应 Vue 数据驱动的思想)
整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性
注意:Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明
那么问题来了 为什么官方要说 Vue 没有完全遵循 MVVM 思想呢?
严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了 $refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。
v-if 和 v-show 的区别
v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
写过自定义指令吗 原理是什么
指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind
原理
1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
2.通过 genDirectives 生成指令代码
3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
4.当执行指令对应钩子函数时,调用对应指令定义的方法
vue 初始化页面闪动问题
使用 vue 开发时,在 vue 初始化之前,由于 div 是不归 vue 管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。
首先:在 css 里加上以下代码:
如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"
如何从真实 DOM 到虚拟 DOM
涉及到 Vue 中的模板编译原理,主要过程:
将模板转换成
ast
树,ast
用对象来描述真实的 JS 语法(将真实 DOM 转换成虚拟 DOM)优化树
将
ast
树生成代码
Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
1)Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题
Vue 使用了 Object.defineProperty 实现双向数据绑定
在初始化实例时对属性执行 getter/setter 转化
属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的(这也就造成了 Vue 无法检测到对象属性的添加或删除)
所以 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
2)接下来我们看看框架本身是如何实现的呢?
Vue 源码位置:vue/src/core/instance/index.js
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,
最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理
defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
生命周期钩子是如何实现的
Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
相关代码如下
Vue 组件间通信有哪几种方式?
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
(1)props / $emit
适用 父子组件通信 这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。
(2)ref 与 $parent / $children
适用 父子组件通信
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent / $children
:访问父 / 子实例
(3)EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
(4)$attrs/$listeners
适用于 隔代组件通信
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何prop
时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过v-bind="$attrs"
传入内部组件。通常配合inheritAttrs
选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件
(5)provide / inject
适用于 隔代组件通信 祖先组件中通过 provider
来提供变量,然后在子孙组件中通过 inject
来注入变量。 provide / inject API
主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。 (6)Vuex
适用于 父子、隔代、兄弟组件通信 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
Vue template 到 render 的过程
vue 的模版编译过程主要如下:template -> ast -> render 函数
vue 在模版编译版本的码中会执行 compileToFunctions 将 template 转化为 render 函数:
CompileToFunctions 中的主要逻辑如下∶ (1)调用 parse 方法将 template 转化为 ast(抽象语法树)
parse 的目标:把 tamplate 转换为 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。
解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造 AST 树的目的。
AST 元素节点总共三种类型:type 为 1 表示普通元素、2 为表达式、3 为纯文本
(2)对静态节点做优化
这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化
深度遍历 AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的 DOM 永远不会改变,这对运行时模板更新起到了极大的优化作用。
(3)生成代码
generate 将 ast 抽象语法树编译成 render 字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``)
生成 render 函数。
说说 Vue 的生命周期吧
什么时候被调用?
beforeCreate :实例初始化之后,数据观测之前调用
created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、
watch/event
事件回调。无$el
.beforeMount:在挂载之前调用,相关
render
函数首次被调用mounted:了被新创建的
vm.$el
替换,并挂载到实例上去之后调用改钩子。beforeUpdate:数据更新前调用,发生在虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
beforeDestroy:实例销毁前调用,实例仍然可用。
destroyed:实例销毁之后调用,调用后,Vue 实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除
每个生命周期内部可以做什么?
created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
mounted:实例已经挂载完成,可以进行一些 DOM 操作。
beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
updated:可以执行依赖于 DOM 的操作,但是要避免更改状态,可能会导致更新无线循环。
destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。
ajax 放在哪个生命周期?:一般放在 mounted
中,保证逻辑统一性,因为生命周期是同步执行的, ajax
是异步执行的。单数服务端渲染 ssr
同一放在 created
中,因为服务端渲染不支持 mounted
方法。 什么时候使用 beforeDestroy?:当前页面使用 $on
,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove
。
理解 Vue 运行机制全局概览
全局概览
首先我们来看一下笔者画的内部流程图。
大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。相信讲完之后大家对Vue.js
内部运行机制会有一个大概的认识。
初始化及挂载
在
new Vue()
之后。 Vue 会调用_init
函数进行初始化,也就是这里的init
过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过Object.defineProperty
设置setter
与getter
函数,用来实现「 响应式 」以及「 依赖收集 」,后面会详细讲到,这里只要有一个印象即可。
初始化之后调用
$mount
会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「 编译 」步骤。
编译
compile 编译可以分成 parse
、optimize
与 generate
三个阶段,最终需要得到 render function。
1. parse
parse
会用正则等方式解析 template 模板中的指令、class、style 等数据,形成 AST。
2. optimize
optimize
的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当update
更新界面时,会有一个patch
的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了patch
的性能。
3. generate
generate
是将 AST 转化成render function
字符串的过程,得到结果是render
的字符串以及 staticRenderFns 字符串。
在经历过
parse
、optimize
与generate
这三个阶段以后,组件中就会存在渲染VNode
所需的render function
了。
响应式
接下来也就是 Vue.js 响应式核心部分。
这里的
getter
跟setter
已经在之前介绍过了,在init
的时候通过Object.defineProperty
进行了绑定,它使得当被设置的对象被读取的时候会执行getter
函数,而在当被赋值的时候会执行setter
函数。
当
render function
被渲染的时候,因为会读取所需对象的值,所以会触发getter
函数进行「 依赖收集 」,「 依赖收集 」的目的是将观察者Watcher
对象存放到当前闭包中的订阅者Dep
的subs
中。形成如下所示的这样一个关系。
在修改对象的值的时候,会触发对应的
setter
,setter
通知之前「 依赖收集 」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用update
来更新视图,当然这中间还有一个patch
的过程以及使用队列来异步更新的策略,这个我们后面再讲。
Virtual DOM
我们知道,
render function
会被转化成VNode
节点。Virtual DOM
其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
比如说下面这样一个例子:
渲染后可以得到
这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。
更新视图
前面我们说到,在修改一个对象值的时候,会通过
setter -> Watcher -> update
的流程来修改对应的视图,那么最终是如何更新视图的呢?当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的
VNode
节点,然后用innerHTML
直接全部渲染到真实DOM
中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「 浪费 」。那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「
patch
」了。我们会将新的VNode
与旧的VNode
一起传入patch
进行比较,经过 diff 算法得出它们的「 差异 」。最后我们只需要将这些「 差异 」的对应 DOM 进行修改即可。
再看全局
回过头再来看看这张图,是不是大脑中已经有一个大概的脉络了呢?
用 VNode 来描述一个 DOM 结构
虚拟节点就是用一个对象来描述一个真实的 DOM 元素。首先将 template
(真实 DOM)先转成 ast
, ast
树通过 codegen
生成 render
函数, render
函数里的 _c
方法将它转为虚拟 dom
了解 nextTick 吗?
异步方法,异步渲染最后一步,与 JS 事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
computed 的实现原理
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
评论