写在最前面:下述vue面试题目都是来自掘金优秀的前辈的文章,我有附上文章摘自的地址。若原作者不想让我有此举,请给我留言以便我删除。仅做自己学习使用,没有其他目的,谢谢!
Vue面试题
摘自掘金 - 卞卞村长L
1.vue底层原理?
2.vue组件之间的通信?
3.vue和angular的区别?
vue的双向数据绑定是基于ES5中的getter和setter来实现的。而angular是由自己实现一套模板编译规则,需要进行所谓的脏检查。所以vue在性能上更高效,但是代价就是对于IE9以下的浏览器无法支持。vue需要提供一个el对象进行实例化,后续所有作用范围也是在el对象之下。一个页面可以有多个vue实例。而angular是整个html页面。
4.active-class是哪个组件的属性?
vue-router模块中的router-link组件。当<router-link>对应的路由匹配成功时,会自动给当前元素设置一个router-link-active的class,设置active-class可以修改默认的名称。使用场景:在进行高亮显示的导航菜单或者底部tabbar时,会使用到该类。
5.嵌套路由怎么定义?
实际项目中,我们会碰到多层嵌套的组件组合而成。如何实现嵌套路由呢?我们需要在VueRouter的参数中使用children配置,这样就可以很好的实现路由嵌套了。
<div id="app">
<!-- router-view 路由出口,路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import home from "./home.vue"
import game from "./game.vue"
const routes = [
{ path: "/", redirect: "/home" }, // 重定向,指向home组件
{
path: "/home", component: home,
children: [
{ path: "/home/game", component: game }
]
}
]
const router = new VueRouter({routes})
new Vue({
el: '#app',
router
})
<!-- home.vue点击显示就会将子路由显示出来。子路由的出口必须在父路由里,否则子路由无法显示 -->
<template>
<div>
<h3>首页</h3>
<router-link to="/home/game">
<button>显示<tton>
</router-link>
<router-view></router-view>
</div>
</template>
6.怎么定义vue-router的动态路由?怎么获取传递过来的动态参数?
- 在
router目录下的index.js文件中,对path属性加上/:id - 通过
this.$route.params.xxx获取到传递过来的动态参数
7.vue-router有哪几种导航钩子
- 全局守卫:
router.beforeEach(to, from, next),用于监听路由跳转过程,并在跳转期间进行一些操作 - 路由独享的守卫
- 组件内的守卫
8.简述sass和less的区别?
-
他们是动态的样式语言,是
css预处理器,css上的一种抽象层 -
他们采用特殊的语法编译成
css,其中变量符不一样,less是@,sass是$ -
sass支持条件语句,可以使用if{}else{}、for{}循环等,而less不支持 -
sass是基于Ruby的,是在服务端处理的,而less需要引入less.js来处理less代码输出css到浏览器
9.vue双向数据绑定原理是什么?
vue采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter。在数据变动时发布消息给订阅者,触发相应的监听回调。
具体步骤:
- 第一步:需要
observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化 - 第二步:
compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者。一旦数据有变动,收到通知,更新视图 - 第三步:
watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:- 在自身实例化时往属性订阅器
dep里面添加自己 - 自身必须有一个
update()方法 - 待属性变动
dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退
- 在自身实例化时往属性订阅器
- 第四步:
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化input-> 数据model变更的双向绑定效果
10.vue生命周期?
概念:Vue实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程。
作用:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
总共分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/后
- 创建前/后:在
beforeCreated阶段,vue实例的挂载元素$el和数据对象data都为undefined,还未初始化。在created阶段,vue实例的数据对象data有了,$el还没有 - 载入前/后:在
beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前的虚拟dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染 - 更新前/后:当
data变化时,会触发beforeUpdate和updated方法 - 销毁前/后:在执行
destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在
DOM渲染在mounted中就已经完成了。
每个周期钩子适合的场景:
beforecreate:可以在这加个loading事件,在加载实例时触发created:初始化完成时的事件写在这里,如在这结束loading事件,异步请求的调用mounted:挂载元素,获取DOM节点,官方实例是在此钩子函数中调用的updated:如果对数据统一处理,在这里写上相应函数beforeDestroy:可以做一个确认停止事件的确认框nextTick,更新数据后立即操作dom
11.vue-loader是什么?用途有哪些?
解析.vue文件的一个加载器,讲template/js/style转换成js模块。
用途:js可以写es6、style样式可以写成scss或less、template可以加jade等。
12.vue的template编译?
简而言之,就是先转化成AST树,再得到的render函数返回VNode。
首先通过compile编译器把template编译成AST语法树,compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。
然后,AST会经过generate得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点。
13.vue和react的区别?
相同点:
react采用特殊的JSX语法,Vue在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用- 中心思想相同,一切都是组件,组件实例之间可以嵌套
- 都提供合理的钩子函数,可以让开发者定制化地去处理需求
- 都不内置列数
AJAX,Route等功能到核心包,而是以插件的方式加载 - 在组件开发中都支持
mixins的特性
不同点:
react依赖Virtual DOM,而Vue使用的是DOM模板react采用的Virtual DOM会对渲染出来的结果做脏检查Vue在模板中提供了指令,过滤器等,可以非常方便,快捷地操作DOM
摘自掘金 - 寻找海蓝96
1.对MVVM的理解?
优点:
- 分离视图和模型,降低代码耦合,提高视图或者逻辑的重用性:比如视图可以独立于
Model变化和修改,一个ViewModel可以绑定不同的View上,当View变化的时候Model不可以不变,当Model变化的时候View也可以不变。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑 - 提高可测试性:
ViewModel的存在可以帮助开发者更好地编写测试代码 - 自动更新
dom:利用双向绑定,数据更新后视图自动更新,让开发者从繁琐的手动dom中解放
缺点:
-
Bug很难被调试:因为使用双向绑定的模式,当你看到界面异常了,有可能是你View的代码有Bug,也可能是Model的代码有问题。数据绑定使得一个位置的Bug被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。另外,数据绑定的声明是指令式地写在View的模版中的,这些内容是没办法去打断点debug的 -
一个大的模块中
model也会很大,虽然使用方便了也很容易保证了数据的一致性,当时长期持有,不释放内存就造成了花费更多的内存 -
对于大型的图形应用程序,视图状态较多,
ViewModel的构建和维护的成本都会比较高
2.Vue组件通信?
props/$emit+v-on:适用于父子组件通信。通过props将数据自上而下传递,而通过$emit和v-on来向上传递信息EventBus($emit/$on):适用于父子、隔代、兄弟组件通信。通过EventBus进行信息的发布与订阅。通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。vuex:是全局数据管理库,可以通过vuex管理全局的数据流$attrs/$listeners:适用于隔代组件通信。可进行跨级的组件通信provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础- 还有一些用
solt插槽或者ref实例进行通信的 ref和$parent/$children适用于父子组件通信。ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。$parent/$children:访问父 / 子实例。
3.computed和watch有什么区别?
computed:
computed是计算属性,更多用于计算值的场景computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算computed适用于计算比较消耗性能的计算场景
watch:
- 更多的是观察的作用,类似于某些数据的监听回调,用于观察
props$emit或者本组件的值,当数据变化时来执行回调进行后续操作 - 无缓存性,页面重新渲染时值不变化也会执行
总结:
-
当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为
computed -
如果需要在某个数据变化时做一些事情(异步),使用
watch来观察这个数据变化
4.vue是如何实现双向绑定的?
利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,然后根据变化进行后续响应。在vue3.0中通过Proxy代理对象进行类似的操作。
// 这是将要被劫持的对象
const data = {
name: '',
};
function say(name) {
if(name === '古天乐') {
console.log('给大家推荐一款超好玩的游戏');
}else if(name === '渣渣辉') {
console.log('戏我演过很多,可游戏我只玩贪玩懒月');
}else {
console.log('来做我的兄弟');
}
}
// 遍历对象,对其属性值进行劫持
Object.keys(data).forEach(function(key) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log('get');
},
set: function(newVal) {
// 当属性值发生变化时我们可以进行额外操作
console.log(`大家好,我系${newVal}`);
say(newVal);
},
});
});
data.name = '渣渣辉';
// 大家好,我系渣渣辉
// 戏我演过很多,可游戏我只玩贪玩懒月
5.Proxy与Object.defineProperty的优劣对比?
Proxy的优势如下:
-
可以直接监听对象而非属性
-
可以直接监听数组的变化
-
有多达13种拦截方法,是
Object.defineProperty不具备的 -
返回的是一个新对象,我们可以只操作新对象达到目的,而
Object.defineProperty只能遍历对象属性直接修改 -
作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
Object.defineProperty的优势如下:
- 兼容性好,支持
IE9
6.如何理解vue响应式系统?
-
任何一个
Vue Component都有一个与之对应的Watcher实例 -
Vue的data上的属性会被添加getter和setter属性 -
当
Vue Component render函数被执行的时候,data上会被触碰,即被读,getter方法会被调用,此时Vue会去记录此Vue component所依赖的所有data,这一过程被称为依赖收集 -
data被改动时(主要是用户操作), 即被写,setter方法会被调用,此时Vue会去通知所有依赖于此data的组件去调用他们的render函数进行更新
7.虚拟DOM的优劣如何?
优点:
- 保证性能下限:虚拟
DOM可以经过diff找出最小差异,然后批量进行patch,这种操作虽然比不上手动优化,但是比起粗暴的DOM操作性能要好很多,因此虚拟DOM可以保证性能下限 - 无需手动操作
DOM:虚拟DOM的diff和patch都是在一次更新中自动进行的,我们无需手动操作DOM,极大提高开发效率 - 跨平台:虚拟
DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等
缺点:
- 无法进行极致优化:在一些性能要求极高的应用中虚拟
DOM无法进行针对性的极致优化,比如VScode采用直接手动操作DOM的方式进行极端的性能优化
8.虚拟DOM实现原理?
- 虚拟
DOM本质上是JavaScript对象,是对真实DOM的抽象 - 状态变更时,记录新树和旧树的差异
- 最后把差异更新到真正的
dom中
9.既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异?
现代前端框架有两种方式侦测变化,一种是pull,一种是push。
pull:其代表为React,我们可以回忆一下React是如何侦测到变化的,我们通常会用setStateAPI显式更新,然后React会进行一层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从一开始就不知道到底是哪发生了变化,只是知道有变化了,然后再进行比较暴力的Diff操作查找哪发生变化了,另外一个代表就是Angular的脏检查操作。
push:Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此Vue是一开始就知道是在哪发生变化了,但是这又会产生一个问题,如果你熟悉Vue的响应式系统就知道,通常一个绑定一个数据就需要一个Watcher,一但我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统。通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff获取更加具体的差异,而Virtual Dom Diff则是pull操作,Vue是push+pull结合的方式进行变化侦测的
10.Vue中的key到底有什么用?
key是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确、更快速。
diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后超出差异。
diff过程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx。它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾。
- 准确:如果不加
key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug - 快速:
key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1)
摘自掘金 - 我是你的超级英雄
1.说说你对spa单页面的理解?
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
- 基于上面一点,SPA 相对对服务器压力小
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点:
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势
2.v-show和v-if的区别?
v-if是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。
v-show就简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS的display属性进行切换。
v-if适用于在运行时很少改变条件,不需要频繁切换条件的场景v-show则适用于需要非常频繁切换条件的场景
3.怎么理解vue的单向数据流?
所有的prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的prop都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果你这样做了,Vue会在浏览器的控制台中发出警告。子组件想修改时,只能通过$emit派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个prop的情形:
- 这个
prop用来传递一个初始值。这个子组件接下来希望将其作为一个本地的prop数据来使用。在这种情况下,最好定义一个本地的data属性并将这个prop用作其初始值:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
- 这个
prop以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个prop的值来定义一个计算属性
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
4.直接给一个数组项赋值,Vue能检测到变化吗?
由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue - 当你修改数组的长度时,例如:
vm.items.length = newLength
为了解决第一个问题,Vue 提供了以下操作方法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
为了解决第二个问题,Vue 提供了以下操作方法:
// Array.prototype.splice
vm.items.splice(newLength)
5.Vue的父组件和子组件生命周期钩子函数执行顺序?
Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:
-
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
-
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
-
父组件更新过程
父 beforeUpdate -> 父 updated
-
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
6.在哪个生命周期内调用异步请求?
可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性
7.在什么阶段才能访问操作DOM?
在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。
8.父组件可以监听到子组件的生命周期吗?
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。
9.谈谈你对 keep-alive 的了解?
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件
- 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高
- 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated
10.组件中 data 为什么是一个函数?
为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?
// data
data() {
return {
message: "子组件",
childName:this.name
}
}
// new Vue
new Vue({
el: '#app',
router,
template: '<App/>',
components: {App}
})
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。如果组件中的data是一个对象的话,那么每次根据该组件模板创造出来的实例共用的就会是同一个内存地址,造成数据联动。
11.v-model的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件
- checkbox 和 radio 使用 checked 属性和 change 事件
- select 字段将 value 作为 prop 并将 change 作为事件
<input v-model='something'>
// 相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
// 父组件
<ModelChild v-model="message"></ModelChild>
// 子组件
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},
12.Vuex
Vuex是一个专为Vue应用程序开发的状态管理模式。每一个Vuex应用的核心就是 store(仓库)。store基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
(1)Vuex的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
主要包括以下几个模块:
- State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
- Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
13.使用过 Vue SSR 吗?说说 SSR?
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 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略
14.vue-router路由模式有几种?
vue-router 有 3 种路由模式:hash、history、abstract:
- hash:使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器
- history:依赖 HTML5 History API 和服务器配置
- abstract:支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式
15.能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?
hash模式:早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':
https://www.word.com#search
特征:
-
URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送
-
hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换
-
可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
-
我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)
history模式:HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
特征:
-
pushState 和 repalceState 两个 API 来操作实现 URL 的变化
-
我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染)
-
history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)
16.Vue是如何实现数据双向绑定的?
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
- 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
- Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。
其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
17.你有对 Vue 项目进行哪些优化?
代码层面的优化:
-
v-if 和 v-show 区分使用场景
-
computed 和 watch 区分使用场景
-
v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
-
长列表性能优化
-
事件的销毁
-
图片资源懒加载
-
路由懒加载
-
第三方插件的按需引入
-
优化无限列表性能
-
服务端渲染 SSR or 预渲染
Webpack 层面的优化:
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
基础的 Web 技术的优化:
- 开启 gzip 压缩
- 浏览器缓存
- CDN 的使用
- 使用 Chrome Performance 查找性能瓶颈
18.对于即将到来的 vue3.0 特性你有什么了解的吗?
监测机制的改变:
3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
- 只能监测属性,不能监测对象
- 检测属性的添加和删除;
- 检测数组索引和长度的变更;
- 支持 Map、Set、WeakMap 和 WeakSet。
新的 observer 还提供了以下特性:
- 用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
- 默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。
- 更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。
- 不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。
- 更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。
模板:
模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
对象式的组件声明方式:
vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。
此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。