前端高频 vue 面试题总结
created 和 mounted 的区别
created:在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。
mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。
如何从真实 DOM 到虚拟 DOM
涉及到 Vue 中的模板编译原理,主要过程:
将模板转换成
ast树,ast用对象来描述真实的 JS 语法(将真实 DOM 转换成虚拟 DOM)优化树
将
ast树生成代码
Vue3 有了解过吗?能说说跟 vue2 的区别吗?
1. 哪些变化
从上图中,我们可以概览Vue3的新特性,如下:
速度更快
体积减少
更易维护
更接近原生
更易使用
1.1 速度更快
vue3相比vue2
重写了虚拟
Dom实现编译模板的优化
更高效的组件初始化
undate性能提高 1.3~2 倍SSR速度提高了 2~3 倍
1.2 体积更小
通过webpack的tree-shaking功能,可以将无用模块“剪辑”,仅打包需要的
能够tree-shaking,有两大好处:
对开发人员,能够对
vue实现更多其他的功能,而不必担忧整体体积过大对使用者,打包出来的包体积变小了
vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多
1.3 更易维护
compositon Api
可与现有的
Options API一起使用灵活的逻辑组合与复用
Vue3模块可以和其他框架搭配使用
更好的 Typescript 支持
VUE3是基于typescipt编写的,可以享受到自动的类型定义提示
1.4 编译器重写
1.5 更接近原生
可以自定义渲染 API
1.6 更易使用
响应式 Api 暴露出来
轻松识别组件重新渲染原因
2. Vue3 新增特性
Vue 3 中需要关注的一些新功能包括:
framentsTeleportcomposition ApicreateRenderer
2.1 framents
在 Vue3.x 中,组件现在支持有多个根节点
2.2 Teleport
Teleport 是一种能够将我们的模板移动到 DOM 中 Vue app 之外的其他位置的技术,就有点像哆啦 A 梦的“任意门”
在vue2中,像 modals,toast 等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难
通过Teleport,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它
2.3 createRenderer
通过createRenderer,我们能够构建自定义渲染器,我们能够将 vue 的开发模型扩展到其他平台
我们可以将其生成在canvas画布上
关于createRenderer,我们了解下基本使用,就不展开讲述了
2.4 composition Api
composition Api,也就是组合式api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理
关于compositon api的使用,这里以下图展开
简单使用:
3. 非兼容变更
3.1 Global API
全局
Vue API已更改为使用应用程序实例全局和内部
API已经被重构为可tree-shakable
3.2 模板指令
组件上
v-model用法已更改<template v-for>和 非v-for节点上key用法已更改在同一元素上使用的
v-if和v-for优先级已更改v-bind="object"现在排序敏感v-for中的ref不再注册ref数组
3.3 组件
只能使用普通函数创建功能组件
functional属性在单文件组件(SFC)异步组件现在需要
defineAsyncComponent方法来创建
3.4 渲染函数
渲染函数
API改变$scopedSlotsproperty 已删除,所有插槽都通过$slots作为函数暴露自定义指令 API 已更改为与组件生命周期一致
一些转换
class被重命名了:v-enter->v-enter-fromv-leave->v-leave-from组件
watch选项和实例方法$watch不再支持点分隔字符串路径,请改用计算函数作为参数在
Vue 2.x中,应用根容器的outerHTML将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x现在使用应用程序容器的innerHTML。
3.5 其他小改变
destroyed生命周期选项被重命名为unmountedbeforeDestroy生命周期选项被重命名为beforeUnmount[prop default工厂函数不再有权访问this是上下文自定义指令 API 已更改为与组件生命周期一致
data应始终声明为函数来自
mixin的data选项现在可简单地合并attribute强制策略已更改一些过渡
class被重命名组建 watch 选项和实例方法
$watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。<template>没有特殊指令的标记 (v-if/else-if/else、v-for或v-slot) 现在被视为普通元素,并将生成原生的<template>元素,而不是渲染其内部内容。在
Vue 2.x中,应用根容器的outerHTML将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x现在使用应用容器的innerHTML,这意味着容器本身不再被视为模板的一部分。
3.6 移除 API
keyCode支持作为v-on的修饰符$on,$off和$once实例方法过滤
filter内联模板
attribute$destroy实例方法。用户不应再手动管理单个Vue组件的生命周期。
为什么要使用异步组件
节省打包出的结果,异步组件分开打包,采用
jsonp的方式进行加载,有效解决文件过大的问题。核心就是包组件定义变成一个函数,依赖
import()语法,可以实现文件的分割加载。
原理
Vue.set 的实现原理
给对应和数组本身都增加了
dep属性当给对象新增不存在的属性则触发对象依赖的
watcher去更新当修改数组索引时,我们调用数组本身的
splice去更新数组(数组的响应式原理就是重新了splice等方法,调用splice就会触发视图更新)
基本使用
以下方法调用会改变原始数组:
push(),pop(),shift(),unshift(),splice(),sort(),reverse(),Vue.set( target, key, value )
调用方法:
Vue.set(target, key, value )target:要更改的数据源(可以是对象或者数组)key:要更改的具体数据value:重新赋的值
相关源码
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组 ,直接使用数组的
splice方法触发相应式;如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用
defineReactive方法进行响应式处理(defineReactive方法就是Vue在初始化对象时,给对象属性采用Object.defineProperty动态添加getter和setter的功能所调用的方法)
v-if 和 v-for 哪个优先级更高
实践中不应该把
v-for和v-if放一起在
vue2中,v-for的优先级是高于v-if,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件,哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会比较浪费;另外需要注意的是在vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常通常有两种情况下导致我们这样做:
为了过滤列表中的项目 (比如
v-for="user in users" v-if="user.isActive")。此时定义一个计算属性 (比如activeUsers),让其返回过滤后的列表即可(比如users.filter(u=>u.isActive))为了避免渲染本应该被隐藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers")。此时把v-if移动至容器元素上 (比如ul、ol)或者外面包一层template即可文档中明确指出永远不要把
v-if和v-for同时用在同一个元素上,显然这是一个重要的注意事项源码里面关于代码生成的部分,能够清晰的看到是先处理
v-if还是v-for,顺序上vue2和vue3正好相反,因此产生了一些症状的不同,但是不管怎样都是不能把它们写在一起的
vue2.x 源码分析
在 vue 模板编译的时候,会将指令系统转化成可执行的
render函数
编写一个p标签,同时使用v-if与 v-for
创建vue实例,存放isShow与items数据
模板指令的代码都会生成在render函数中,通过app.$options.render就能得到渲染函数
_l是vue的列表渲染函数,函数内部都会进行一次if判断初步得到结论:
v-for优先级是比v-if 高再将
v-for与v-if置于不同标签
再输出下render函数
这时候我们可以看到,v-for与v-if作用在不同标签时候,是先进行判断,再进行列表的渲染
我们再在查看下 vue 源码
源码位置:\vue-dev\src\compiler\codegen\index.js
在进行if判断的时候,v-for是比v-if先进行判断
最终结论:v-for优先级比v-if高
参考 前端进阶面试题详细解答
如果让你从零开始写一个 vue 路由,说说你的思路
思路分析:
首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。
借助
hash或者 history api实现url跳转页面不刷新同时监听
hashchange事件或者popstate事件处理跳转根据
hash值或者state值从routes表中匹配对应component并渲染
回答范例:
一个SPA应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:
首先我会定义一个
createRouter函数,返回路由器实例,实例内部做几件事
保存用户传入的配置项
监听
hash或者popstate事件回调里根据
path匹配对应路由
将
router定义成一个Vue插件,即实现install方法,内部做两件事
实现两个全局组件:
router-link和router-view,分别实现页面跳转和内容显示定义两个全局变量:
$route和$router,组件内可以访问当前路由和路由器实例
生命周期钩子是如何实现的
Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
相关代码如下
子组件可以直接改变父组件的数据么,说明原因
这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题
思路
讲讲单项数据流原则,表明为何不能这么做
举几个常见场景的例子说说解决方案
结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
所有的
prop都使得其父子之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的prop都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果你这样做了,Vue会在浏览器控制台中发出警告
实际开发过程中有两个场景会想要修改一个属性:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:
这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
实践中如果确实想要改变父组件属性应该
emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop,但是我们还是能够直接改内嵌的对象或属性
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 节点,里面有(标签名、子节点、文本等等)
v-if 和 v-show 区别
v-show隐藏则是为该元素添加css--display:none,dom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除编译过程:
v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换编译条件:
v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染v-show由false变为true的时候不会触发组件的生命周期v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestory、destoryed方法性能消耗:
v-if有更高的切换消耗;v-show有更高的初始渲染消耗
v-show 与 v-if 的使用场景
v-if与v-show都能控制dom元素在页面的显示v-if相比v-show开销更大的(直接操作dom节点增加与删除)如果需要非常频繁地切换,则使用 v-show 较好
如果在运行时条件很少改变,则使用
v-if较好
v-show 与 v-if 原理分析
v-show原理
不管初始条件是什么,元素总是会被渲染
我们看一下在 vue 中是如何实现的
代码很好理解,有transition就执行transition,没有就直接设置display属性
v-if原理
v-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处理 v-if 的一小部分
返回一个node节点,render函数通过表达式的值来决定是否生成DOM
Vue 组件通讯有哪几种方式
props 和emit 触发事件来做到的
children 获取当前组件的父组件和当前组件的子组件
listeners A->B->C。Vue 2.4 开始提供了listeners 来解决这个问题
父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)
$refs 获取组件实例
envetBus 兄弟组件数据传递 这种情况下可以使用事件总线的方式
vuex 状态管理
diff 算法
答案时间复杂度: 个树的完全 diff 算法是一个时间复杂度为 O(n*3) ,vue 进行优化转化成 O(n) 。
理解:
最小量更新,
key很重要。这个可以是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个 DOM 节点扩展
v-for为什么要有key,没有key会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加key只会移动减少操作 DOM。只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。
只进行同层比较,不会进行跨层比较。
diff 算法的优化策略:四种命中查找,四个指针
旧前与新前(先比开头,后插入和删除节点的这种情况)
旧后与新后(比结尾,前插入或删除的情况)
旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)
旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)
--- 问完上面这些如果都能很清楚的话,基本 O 了 ---
以下的这些简单的概念,你肯定也是没有问题的啦😉
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。这样使得我们可以方便地跟踪每一个状态的变化。
异步组件是什么?使用场景有哪些?
分析
因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。
体验
大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们
回答范例
在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。
使用异步组件最简单的方式是直接给
defineAsyncComponent指定一个loader函数,结合 ES 模块动态导入函数import可以快速实现。我们甚至可以指定loadingComponent和errorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还可以结合Suspense组件使用异步组件。异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是
vue框架,处理路由组件加载的是vue-router。但是可以在懒加载的路由组件中使用异步组件
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 是数据双向绑定的框架,双向绑定由三个重要部分构成
数据层(Model):应用的数据及业务逻辑
视图层(View):应用的展示效果,各类 UI 组件
业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解 ViewModel
它的主要职责就是:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
监听器(
Observer):对所有数据的属性进行监听解析器(
Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
为什么要用 Vuex 或者 Redux
由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致代码无法维护。
所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为。
另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,代码将会变得更结构化且易维护。
$route 和$router 的区别
$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
$router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。










评论