面试官:Vue 单页应用与多页应用的区别
- SPA : SPA 单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次 js、css 等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
- MPA : MPA 多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载 js、css 等相关资源。多页应用跳转,需要整页资源刷新。
- 区别:
| 单页面应用 | 多页面应用 | |
|---|---|---|
| 组成 | 一个主页面和多个页面片段 | 多个主页面 |
| 刷新方式 | 局部刷新 | 整页刷新 |
| url模式 | 哈希模式 | 历史模式 |
| SEO | 难实现,可使用SSR方式改善 | 容易实现 |
| 数据传递 | 容易 | 通过url、cookie、localStorage等传递 |
| 页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
| 维护成本 | 相对容易 | 相对复杂 |
面试官:那么如何实现一个SPA
原理
- 监听地址栏中hash变化驱动界面变化
- 用pushsate记录浏览器的历史,驱动界面发送变化
hash模式:核心通过监听url中的hash来进行路由跳转
javascript
// 定义 Router
class Router {
constructor () {
this.routes = {}; // 存放路由path及callback
this.currentUrl = '';
// 监听路由change调用相对应的路由回调
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
this.routes[path] && this.routes[path]()
}
}
// 使用 router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2'))
miniRouter.push('/') // page1
miniRouter.push('/page2') // page2
history模式:history 模式核心借用 HTML5 history api,api 提供了丰富的 router 相关属性先了解一个几个相关的api
- history.pushState 浏览器历史纪录添加记录
- history.replaceState修改浏览器历史纪录中当前纪录
- history.popState 当 history 发生变化时触发
javascript
// 定义 Router
class Router {
constructor () {
this.routes = {};
this.listerPopState()
}
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
listerPopState () {
window.addEventListener('popstate' , e => {
const path = e.state && e.state.path;
this.routers[path] && this.routers[path]()
})
}
}
// 使用 Router
window.miniRouter = new Router();
miniRouter.route('/', ()=> console.log('page1'))
miniRouter.route('/page2', ()=> console.log('page2'))
// 跳转
miniRouter.push('/page2') // page2
面试官:assets 和 static 的区别
- 相同点:assets 和 static 两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下
- 不同点:
- assets 中存放的静态资源文件在项目打包时,会将 assets 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。压缩后的静态资源文件最终也都会放置在 static 文件中跟着index.html一同上传至服务器。
- static 中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是static 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets 中打包后的文件提交较大点。在服务器中就会占据更大的空间。
面试官:详细说说你会在这两个里面放什么
将项目中 template 需要的样式文件js 文件等都可以放置在assets 中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如 iconfoont.css 等文件可以放置在static 中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。
面试官:delete 和 Vue.delete 删除数组的区别
delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
Vue.delete 直接删除了数组 改变了数组的键值。
面试官:Vue 模板是如何编译的
vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。
模板编译又分三个阶段,解析parse,优化optimize,生成 generate,最终生成可执行函数 render。
- 解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。
- 优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能。
- 生成阶段:将最终的 AST 转化为 render 函数字符串。
面试官:DIFF 算法的原理
在新老虚拟 DOM 对比时:
首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children 没有子节点,将旧的子节点移除)
比较如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff 核心)。匹配时,找到相同的子节点,递归比较子节点在 diff 中,只对同层的子节点进行比较,放弃跨级的节点比较,降低时间复杂度,也就是说,只有当新旧children都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。
面试官:Vue 有了数据响应式,为何还要 diff ?
数据响应式
Vue 的数据响应式系统通过 Object.defineProperty 或者 ES6 的 Proxy 来实现,主要解决了以下问题:
- 数据绑定:保证了视图与数据的同步更新,当数据发生变化时,视图会自动更新,避免了手动操作 DOM 的繁琐和易出错性。
- 依赖追踪:Vue 能够追踪每个数据的依赖关系,即哪些组件或者计算属性依赖于某个数据。当数据变化时,自动更新依赖的组件或者计算属性。
虚拟 DOM 和 Diff 算法
- 虚拟 DOM 是一种内存中的表示结构,它是对真实 DOM 的抽象。Diff 算法是一种高效更新 DOM 的策略,它通过比较新旧虚拟 DOM 树的差异,最小化了更新操作,提高了页面的渲染效率。
为什么还需要 Diff 算法?
-
性能优化:直接操作真实 DOM 是非常昂贵的,而虚拟 DOM 可以在内存中快速进行比较和计算差异。Diff 算法帮助减少了更新操作的次数和范围,从而提升了页面渲染的性能。
-
批量更新:Diff 算法能够将多次 DOM 更新操作合并为一次,避免了频繁的 DOM 操作,减少了浏览器的重排和重绘。
-
跨平台兼容:虚拟 DOM 和 Diff 算法使得 Vue 可以运行在不同的平台上,例如浏览器、Weex 等,统一了渲染逻辑和数据响应式的实现。
-
更新效率:即使是响应式系统可以自动更新视图,但是如果每次数据变化都直接操作真实 DOM,可能会带来性能问题。Diff 算法可以智能地比较新旧 DOM 树的变化,只更新必要的部分,从而提高了更新效率。
综合作用
Vue 的数据响应式系统和虚拟 DOM + Diff 算法是紧密协作的:
- 数据响应式:保证了数据和视图的同步更新,提供了便捷的开发方式。
- 虚拟 DOM + Diff 算法:提高了页面渲染的效率和性能,减少了不必要的 DOM 操作,确保了页面的流畅性和响应性。
总体来说,数据响应式和 Diff 算法是为了解决不同层面的问题,结合起来使得 Vue 能够提供高效、流畅的用户体验。
面试官:说说Vue 页面渲染流程
首先在导入引入Vue 时,会对 Vue 框架进行初始化
然后创建 Vue 实例,管理生命周期(初始化 ------ 模板编译 ------ 挂载 ------ 销毁)
在第二个阶段:
-
new Vue() 进行实例的创建(这个时候会调用_init(),相当于程序的入口),
-
挂载组件($mount),进行实例化
-
构建 VNode (_render() 方法 、createElement() 返回 VNode )通过createElement来创建虚拟节点VNode,将 VNode 渲染成 DOM。
-
patch() 对比新旧 VNode,createElm()生成(/更新)真实 DOM 节点树 (递归)。最终将整个 DOM 树插入到页面中,再移除旧的根节点(初始化渲染实际上是新的根节点代替旧的根节点)
-
节点插入生命周期钩子函数
面试官:Vue 项目中,你做过哪些性能优化?
- v-if 和 v-show 区分使用场景
v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做------直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。 - v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件 - 服务端渲染 SSR
首先服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。
-
服务端渲染的优点:
更好的 SEO: 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
更快的内容到达时间(首屏加载更快): SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
-
服务端渲染的缺点:
更多的开发条件限制: 例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。
- 在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片:
- 首先,安装 image-webpack-loader :
- 然后,在 webpack.base.conf.js 中进行配置:
javascript
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
- 提取组件的 CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 "无样式内容闪烁 (fouc) " 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。
面试官:如果使用Vue3.0实现一个 Modal,你会怎么进行设计?
组件就是将公共的部分进行提取,包含各种逻辑,相同的样式等,来实现开发模式。
如果模态框的内容基本一致那么就不需要封装成两个组件,只需要根据传入的参数不同,组件显示不同的内容就可以了,这样,下次开发相同界面程序时就可以写更少的代码,意义着更高的开发效率,更少的 Bug和更少的程序体积。
一个对话框需要的基本要素「标题,内容,确定/取消按钮」。内容需要灵活,所以可以是字符串,或一段 html 代码(也就是 slot )。
- 对话框需要"跳出",避免来自父组件的"束缚",用 Vue3 Teleport 内置组件包裹。
- 调用对话框需要在每个父组件都进行引入 import Modal from '@/Modal',比较繁琐。考虑还可以采用 API 的形式,如在 Vue2 中:this.$modal.show({ /* 选项 */ })。
- API 的形式调用,内容可以是字符串,灵活的 h 函数,或jsx语法进行渲染。
- 可全局配置对话框的样式,或者行为...,局部配置可进行覆盖。
- 与 ts 结合,使基于 API 的形式调用更友好。
面试官:Vue3.0里为什么要用 Proxy API 替代defineProperty APl ?
- Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
- Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。
- Proxy 可以劫持整个对象,并返回一个新的对象。
- Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
面试官:说说 vue3 中的响应式设计原理
当 一 个 Vue 实 例 创 建 时 , Vue 会 遍历data 中的属性,用Object.defineProperty ( vue3.0 使 用proxy )将它们转为getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
面试官:Vuex有几种属性,它们存在的意义分别是什么?
vuex一共有5大核心
- state,里面保存的是状态,也可以理解为是数组,
- getters,他们用来获取state里面的状态,并且可以对state的数据进行处理之后在返回
- mutations,他的作用主要是用来修改state里面的数据,不过在mutations里面只能进行同步的操作
- actions,这个actions也可以去改变state的状态,不过在actions里面定义的方法可以进行异步操作
- modules,如果当我们的项目比较大的时候,那么保存的状态也会增加,如果都写到index.js文件里面,文件的内容就会变得特别臃肿,后期难以维护,所以我们可是使用modules进行模块化的处理,将多个状态抽离到对应js文件里面,最后在modules进行合并,这样后期就方便维护了
面试官:为什么 Vuex 的 mutation 中不能做异步操作?
Vuex 中所有的状态更新的唯一途径都是mutation,异步操作通过Action 来提交 mutation 实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。
每个 mutation 执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现time-travel了。
如果 mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
面试官:Vue3有了解过吗?能说说跟Vue2的区别吗?
-
双向数据绑定原理不同: vue3重写了响应式系统,使用proxy代替了Object.defineProperty,实现了更高效的变化侦测
-
API类型不同:vue2:vue2使用选项类型api,选项型api在代码里分割了不同的属性:data,computed,methods等。
vue3:vue3使用组合式api,使用方法来分割,相比于v2,v3使用属性来分组,这样代码会更加简便和整洁。
-
是否支持碎片:支持多个根节点,不再强制要求单一的根节点
定义数据变量和方法不同:vue2是把数据放入data中,方法写在methods中,vue3就需要使用一个新的setup()方法,这个方法在组件初始化构造的时候触发。相当于vue2的创建前和创建后的钩子。
-
生命周期钩子函数不同:
vue2:
beforeCreate 组件创建之前
created 组件创建之后,组件初始化完毕,可以访问各种数据,获取接口数据等
beforeMount 组价挂载到页面之前执行
mounted 组件挂载到页面之后执行,dom已创建,可用于获取访问数据和dom元素;访问子组件等
beforeUpdate 组件更新之前,此时view层还未更新,可用于获取更新前各种状态
updated 组件更新之后,完成view层的更新,更新后,所有状态已是最新
vue3:
setup 开始创建组件
onBeforeMount 组价挂载到页面之前执行
onMounted 组件挂载到页面之后执行
onBeforeUpdate 组件更新之前
onUpdated 组件更新之后
-
指令和插槽不同:
vue2:可以直接使用slot;v-for与v-if在vue2中优先级高的是v-for指令,而且不建议一起使用。
vue3:vue3中必须使用v-slot的形式;vue3中v-for与v-if,只会把当前v-if当做v-for中的一个判断语句,不会相互冲突;;vue3中移除v-on.native修饰符;vue3中移除过滤器filter。
面试官:双向数据绑定的原理是什么?
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
首先对data执行响应化处理,在Observe中劫持所有属性,同时对complie中对模板进行编译,找到动态绑定的数据,从data中获取并且初始化视图,同时定义watcher和更新函数,通常data中会有多个watcher,因此定义一个Dep来管理这些watcher,当监听到Observe中有数据变化时会通知Dep,然后由Dep来通知所有的watcher去触发更新函数,最终更新视图。
面试官: slot 是什么?有什么作用?原理是什么?
slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用slot 元素作为承载分发内容的出口。插槽slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。
slot 又分三类,默认插槽,具名插槽和作用域插槽。
-
默认插槽:又名匿名插槽,当 slot 没有指定name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
-
具名插槽:带有具体名字的插槽,也就是带有name 属性的slot,一个组件可以出现多个具名插槽。
-
作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件 vm 实例化时,获取到父组件传入的slot 标签的内容,存放在 vm.$slot 中,默认插槽为vm.$slot.default,具名插槽为 vm.$slot.插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
面试官:父子组件生命周期执行的循序
父组件和子组件生命周期钩子执行顺序:
- 加载渲染过程:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted - 更新过程:
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated - 销毁过程:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
面试官:vue路由中,history和hash两种模式有什么区别?
哈希:
- url 尾巴后有的#号
- .hash模式不会包含在http请求当中,并且hash不会重新加载页面,hash模式的主要原理就是
onhashchange()事件
history: - history没有带#,外观上比hash 模式好看些.
- 使用history模式的话,如果前端的url和后端发起请求的url不一致的话,会报404错误,所以使用history模块的话我们需要和后端进行配合.
- history api可以分为两大部分,切换历史状态和修改历史状态:
修改历史状态:包括了 HTML5 中新增的 pushState() 和 replaceState() 方法,
切换历史状态: 包括forward()、back()、go()三个方法
面试官:Vue中的 v-show 和 v-if 有什么区别
v-if 是 真正 的条件渲染,是组件的创建与销毁的过程
v-show 简单地基于 CSS 的 display 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
面试官:为什么Vue中的data属性是一个函数而不是---个对象?
根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象
总结:本篇博客,详细介绍了前端中有关于vue的面试题,但是这只是其中的一部分,后续还会继续更新,我们先从vue开始整理,后续会整理JS与CSS,浏览器等其他的相关面试题,需要的小伙伴可以订阅专栏,关注后续面试题的更新。