# vue 基础面试题

# 第 1 题-为什么 vue 组件中的 data 是函数而不是对象

export default {
  data() {
    // data是一个函数,data: function() {}的简写
    return {
      // 页面要初始化的数据
      name: 'itclanCoder',
    };
  },
};
1
2
3
4
5
6
7
8
9

而非:如下所示

export default {
  data: {
    // data是一个对象
    name: 'itclanCoder',
  },
};
1
2
3
4
5
6

当一个组件被定义,data必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例

也就是说,在很多页面中,定义的组件可以复用在多个页面

如果data是一个纯碎的对象,则所有的实例将共享引用同一份data数据对象,无论在哪个组件实例中修改data,都会影响到所有的组件实例

如果data是函数,每次创建一个新实例后,调用data函数,从而返回初始数据的一个全新副本数据对象

这样每复用一次组件,会返回一份新的data数据,类似于给每个组件实例创建一个私有的数据空间,让各个组件的实例各自独立,互不影响,保持低耦合

可以看下面一段代码

// 声明构造器函数
function Person() {}

Person.prototype.data = {
  // 原型下挂载一对象,并有name属性
  name: 'itclanCoder',
};

var p1 = new Person();
var p2 = new Person();
p1.data.name = '川川';
console.log(p1.data.name); // 川川
console.log(p1.data.name); // 川川
1
2
3
4
5
6
7
8
9
10
11
12
13

挂载在原型下属性如果是一个对象,实例化出来的对象(p1,p2)都指向的是同一份实体

原型下的属性相当于是公有的

修改一个实例对象下的属性,也会造成另一个实例属性跟着改变,这样在组件复用的时候,肯定是不行的,那么改成函数就可以了的

function Person() {
  this.data = this.data();
}

Person.prototype.data = function() {
  return {
    name: 'itclanCoder',
  };
};

var p1 = new Person();
var p2 = new Person();

p1.data.name = '随笔川迹'; // 如果是函数的形式去定义属性,它是有自定的作用域的,在修改的时候不会影响到别人
console.log(p1.data.name); // 随笔川迹
console.log(p2.data.name); // itclanCoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 第 2 题-vue-router 路由的模式

vue-router 默认 hash 模式 —— 使用 URLhash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载

如下所示

http://localhost/#home
1

如果觉得hash很丑,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API来完成 URL跳转而无须重新加载页面

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})
1
2
3
4

如下所示

http://localhost/home
1

配置路由模式:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api的浏览器。
  • history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。
  • abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式

base为应用的基础路径,例如:整个单页面应用服务在/app/下,那么base就应该设为/app/,当你在HTML5 history模式下使用base选项之后,所有的to属性都不需要写基础路径了

# 第 3 题-写一下 vue2 实例的生命周期

生命周期函数(钩子函数):在特定的阶段,能够自动执行的函数,总共分为 8 个阶段:创建前/后,载入前/后,更新前/后,销毁前/后

  1. beforeCreate阶段: vue实例挂载元素el和数据对象data都为undefined,还未初始化

注意

在当前阶段data,methods,computed以及watch上的数据和方法都不能被访问

应用场景:

  1. 可以在此时加一些loading效果,在created时进行移除
  2. 也可以在此阶段做一些页面拦截,当进入一个路由时,可以判断是否有权限进去,是否安全,携带参数是否完整,参数是否安全,使用好这个钩子的时候就避免了让页面去判断,省掉了创建一个组件vue实例
  3. 做自定义重定向,当路由还没有进去时,判断是否正确进去,若不正确则可以重定向到指定的页面
  4. 想要在实例化数据之前做什么事情,都可以在这个钩子函数里设置
  1. created阶段: vue实例的数据对象data有了,el还没有,在实例创建完成后会被立即调用。在这一阶段,实例已完成,数据观测(data observer),property 和方法的运算,watch/event 事件回调

然而,挂载阶段还没开始,$el property目前尚不可用

在这一阶段可以做一些初始化数据的获取,在当前阶段无法与DOM进行交互,如果非要做,可以通过vm.$nextTick来访问DOM

应用场景: 需要异步请求数据的方法可以在此时执行,完成数据的初始化(Ajax请求放在这个阶段也是可以的)

挂载时

  • beforeMount: 在挂载开始之前被调用,相关的render函数首次被调用
  • mounted: 实例已经挂载完成,可以进行一些DOM操作

载入前/后

  1. beforeUpdate阶段: 在挂载开始之前被调用: 相关的 render 函数首次会被调用,监测到data发生变化,在变化的数据重新渲染视图之前会触发,这也是重新渲染之前最后修改数据的机会

可以在当前阶段进行更改数据,不会造成重渲染

  1. updated: 监测到data发生变化,并完成渲染更新视图之后触发,虚拟 DOM 重新渲染和打补丁之后调用,组合新的DOM已经更新,避免在这个钩子函数中操作数据,防止死循环

销毁前/后

  1. beforeDestory阶段: 实例销毁前调用,实例还可以用,this能获取到实例,常用于销毁定时器,解绑事件

在当前阶段实例完全可以被使用,我们可以在时进行善后收尾工作,比如:清除计时器

  1. destoryed阶段: 实例销毁后调用,调用后所有事件监听器会被移除,所有的子实例都会被销毁,当前阶段组件被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁

# 第 4 题-vue 的父组件和子组件声明周期钩子执行顺序

第一次页面加载时会触发beforeCreate,created,beforeMount,mounted

渲染过程:

父组件挂载完成一定是等子组件都挂载完后,才算是父组件挂载完,所以父组件的mounted在子组件mounted之后

父组件beforeCreate-->父created-->父beforeMount--> 子beforeCreate-->子created-->子beforeMount--->子mounted --> 父mounted

父组件更新过程

影响到子组件: 父beforeUpdate-->子beforeUpdate-->子updated--->父updated

不影响子组件: 父beforeUpdate-->父updated 销毁过程

beforeDestory-->子beforeDestory-->子destoryed-->父destroyed

注意: 父组件等待子组件完成后,才会执行自己对应完成的钩子

# 第 5 题-vue.js DOM 渲染性能为什么比 jQuery 快?

vue通过建立一个虚拟 DOM来追踪自己要如何改变真实的DOM 通过数据改变去diff差异,而jq创建之后,比如:更新,会去拼接字符串,再渲染,但是dom节点上包含很多属性,每次都要重复变,是很浪费性能的,所以就有了虚拟DOM,与数据进行绑定,只有数据变了,dom也会跟着变

# 第 6 题-vue 脚手架编译后,如果存在过大的 js 文件怎么处理?

  • 把一些不常改变的库放到index.html中,通过cdn的方式引入
<script src="https://unpkg.com/vue@2.5.2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3.0.1/dist/vue-router.js"></script>
<script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
1
2
3

然后找到build/webpack.base.config.js文件,在module.exports={}中添加以下代码

externals: {
  'vue': 'vue',
  'element-ui': 'element',
  'axios': 'axios'
}
1
2
3
4
5
  • 通过路由的懒加载
export default new VueRouter({
  mode: `history`,
  routes: [
    {
      path: '/',
      name: 'Account',
      compontent: (resolve) => require(['@/components/Account'], resolve),
    },
  ],
});
1
2
3
4
5
6
7
8
9
10
  • 剥离css文件,单独打包

安装webpack插件extract-text-webpack-plugin,npm install extract-text-webpack-plguin --save-dev

plugins: [new ExtractTextPlugin('static/css/styles.[contenthash].css')];
1
  • 开启gzip压缩

使用compression-webpack-plugin插件进行压缩 安装npm install compression-webpack --save-dev

const CompressionPlugin = require('compression-webpack-plugin');
plugins: [
  new CompressionPlugin({
    asset: '[path].gz[query]', //目标资源名称。[file] 会被替换成原资源。[path] 会被替换成原资源路径,[query] 替换成原查询字符串
    algorithm: 'gzip', //算法
    test: new RegExp(
      '\\.(js|css)$' //压缩 js 与 css
    ),
    threshold: 10240, //只处理比这个值大的资源。按字节计算
    minRatio: 0.8, //只有压缩率比这个值小的资源才会被处理
  }),
];
1
2
3
4
5
6
7
8
9
10
11
12

# 第 7 题-谈谈对 SPA 单⻚⾯的理解,优缺点是什么?

仅仅在 web 页面初始化时加载相应的html,javaScriptcss一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转,取而代之的是利用路由机制实现html内容的变换,UI与用户的交互,避免页面的重新加载

优点:

  1. 用户体验好,快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
  2. SPA 相对服务器压力小
  3. 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理

缺点:

  1. 首屏初次加载慢:为实现单页面 web 应用功能及显示效果,需要在加载页面的时候将javaScript,css统一加载,部分页面按需加载
  2. 不利于seo,由于所有的内容都在一个页面中动态替换显示,所以在seo上有着天然的弱势

# 第 8 题-怎么提高首屏渲染

安装webpack-bundle-analyzer这个插件,然后使用npm run build --report输出项目打包情况,直观的比较哪个bundle文件的大小,有针对性的模块化拆分

  • 路由懒加载 在 router.js文件中,原来的静态引用方式,如
import ShowBlogs from '@/components/ShowBlogs';

routes: [(path: 'Blogs'), (name: 'ShowBlogs'), (component: ShowBlogs)];
1
2
3

改为

routes:[
 		path: 'Blogs',
 		name: 'ShowBlogs',
 		component: () => import('./components/ShowBlogs.vue')
 	]
1
2
3
4
5

如果是在 vuecli 3中,我们还需要多做一步工作 因为 vuecli 3默认开启 prefetch(预先加载模块),提前获取用户未来可能会访问的内容 在首屏会把这十几个路由文件,都一口气下载了 所以我们要关闭这个功能,在 vue.config.js中设置

// vue.config.js
module.exports = {
  chainWebpack: (config) => {
    // 移除prefetch插件
    config.plguins.delete('prefetch');

    // 或者
    // 修改它的选项
    config.plugin('prefetch').tap((options) => {
      options[0].fileBlacklist = options[0].fileBlacklist || [];
      options[0].fileBlacklist.push(/myasyncRoute(.)+?\.js$/);
      return options;
    });
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 一些 UI 框架按需加载,不要整个的引入 这里以饿了么 ui 为例: 原本的引进方式引进了整个包
import ElementUI from 'element-ui';
Vue.use(ElementUI);
1
2

如果只用了按钮,表单,分页,表格,提示等更改为

import {
  Button,
  Input,
  Pagination,
  Table,
  TableColumn,
  MessageBox,
} from 'element-ui';
Vue.use(Button);
Vue.use(Input);
Vue.use(Pagination);
Vue.prototype.$alert = MessageBox.alert;
1
2
3
4
5
6
7
8
9
10
11
12

注意 MessageBox注册方法的区别,虽然用到了alert,但并不需要引入 Alert组件

.babelrc / babel.config.js文件中添加(vue-cli 3要先安装 babel-plugin-component)

plugins: [
  [
    'component',
    {
      libraryName: 'element-ui',
      styleLibraryName: 'theme-chalk',
    },
  ],
];
1
2
3
4
5
6
7
8
9
  • gzip 压缩 安装 compression-webpack-plugin
cnpm i compression-webpack-plugin -D
1

vue.congig.js中引入并修改 webpack配置

const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') {
            // 为生产环境修改配置...
            config.mode = 'production'
            return {
                plugins: [new CompressionPlugin({
                    test: /\.js$|\.html$|\.css/, //匹配文件名
                    threshold: 10240, //对超过10k的数据进行压缩
                    deleteOriginalAssets: false //是否删除原文件
                })]
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

注意:在服务器也要做相应的配置,如果发送请求的浏览器支持 gzip,就发送给它gzip格式的文件 如果服务器是用express框架搭建的 只要安装一下 compression就能使用,其他也类似

const compression = require('compression');
app.use(compression()); // 要放在所有其他中间件注册之前
1
2
  • 首屏内容可以做静态缓存(hash+强缓存的一个方案。比如hash+ cache control: max-age=1年)
  • 首屏内联 css 渲染
  • 图片懒加载(可以通过给img标签上添加loading=lazy)来开启懒加载模式
  • 使用字体图标代替小图片
  • 图片尺寸大小控制适当
  • 利用好script标签的asyncdefer这两个属性,功能独立且不要求马上执行的js文件,可以加入async属性,如果是优先级低且没有依赖的js,可以加入defer属性
  • 前端做一些接口的缓存:缓存的位置有两个: 一个是内存,即赋值给运行时的变量,另一个是localStorage,比如签到日历(展示用户是否签到),可以缓存这样的接口到localStorage,有效期是当天,或者有个列表页,我们总是缓存上次的列表内容到本地,下次加载时,我们先从本地读取缓存,并同时发起请求到服务器获取最新列表
  • 页面使用骨架屏(元素进行占位)
  • 使用ssr渲染:服务器性能一般都很好,那么可以在服务器先把vdom计算完后,在输出给前端
  • 引入http2.0,http2.0对比http1.1最主要的是提升是传输性能,在接口小而多的时候更加明显
  • 选择先进的图片格式:使用webP的图片格式来代替现有的jpegpng,当页面图片较多时,这点作用非常明显
  • 利用好http压缩:即使是最普通的gzip,也能把文件大小压缩不小

# 第 9 题-new Vue()发生了什么?

new Vue()是创建了vue实例,它内部执行了根实例的初始化过程

具体包括以下操作

  • 选项合并
  • $children,$refs,$slots,$createElement
  • 自定义事件处理
  • 数据响应式处理
  • 生命周期钩子调用(beforecreate created)
  • 挂载

new Vue()创建了根实例并准备好数据和方法,未来执行挂载时,此过程还会递归的应用于它的子组件上,最终形成了一个有紧密关系的组件实例树

# 第 10 题-Vue.use 是干什么的,原理是什么?

Vue.use是用来使用插件的,我们可以在插件中拓展全局组件,指令,原型方法

  1. 检查插件是否注册,若已注册,则直接跳出
  2. 处理入参,将第一个参数之后的参数归集,并在首部塞入this上下文
  3. 执行注册方法,调用定义好的install方法,传入处理的参数,若没有install方法并且插件本身为function则直接进行注册

注意

  1. 插件不能重复的加载
  2. install 方法的第一个参数是vue的构造函数,其他参数是vue.set中除了第一个参数的其他参数,代码:args.unshift(this)
  3. 调用插件的install方法,代码: typeof plugin.install === 'function'
  4. 插件本身是一个函数,直接让函数执行,代码:plugin.apply(null, args)
  5. 缓存插件:代码:installedPlguins.push(plugin)

# 第 11 题-说一下响应式数据的理解

根据数据类型来做不同的处理,数组和对象类型当值变化时如何劫持

  1. 对象内部通过defineReactive方法,使用Object.defineProperty()监听数据属性的get来进行数据依赖收集,在通过set来完成数据更新的派发
  2. 数组则通过重写数组方法来实现的,拓展它的 7 个变更方法,通过监听这些方法可以做到依赖收集和派发更新(push/pop/shift/unshift/splice/reverse/sort)
  3. vue3中是使用proxy来实现响应式数据

内部依赖收集是怎么做到的,每个属性都拥有自己的dep属性,存放它所依赖的watcher,当属性变化后会通知自己对应的watcher去更新

响应式流程:

  1. defineReactive把数据定义成响应式的
  2. 给属性增加一个dep,用来收集对应的那些watcher
  3. 等数据变化进行更新
  4. dep.depend() // get 取值,进行依赖收集
  5. dep.notify() // set 设置时,通知视图更新

这里可以引出性能优化相关的内容:

  1. 对象层级过深,性能就会差
  2. 不需要响应式数据的内容不要放在data
  3. object.freeze()可以冻结数据

# 第 12 题-Vue 如何检测数组变化?

没有考虑数组原因是有用defineProperty对数组的每一项进行拦截,而是选择重写数组方法以进行重写,当数组调用到这 7 个方法的时候,执行obj.dep.notify()进行派发通知watcher更新,重写数组方法:push/pop/shift/unshift/splice/reverse/sort

Vue中修改数组的索引和长度是无法监控到的。需要通过以下 7 种变异方法修改数组才会触发数组对应的wacther进行更新。

数组中如果是对象数据类型也会进行递归劫持。

那如果想要改索引更新数据怎么办?

可以通过Vue.set()来进行处理 =》 核心内部用的是 splice 方法

// 取出原型方法;
const arrayProto = Array.prototype
// 拷贝原型方法;
export const arrayMethods = Object.create(arrayProto)
// 重写数组方法;
def(arrayMethods, method, function mutator (...args) { }
ob.dep.notify()  // 调用方法时更新视图;
1
2
3
4
5
6
7

# 第 13 题-Vue.set 方法是如何实现的?

为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性,当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组

官方定义: Vue.set(object, key, value)

如果是数组,调用重写splice方法 代码: target.splice(key, 1, val)

  1. 如果不是响应式的也不需要将其定义成响应式属性
  2. 如果是对象,将属性定义成响应式的defineReactive(ob.value,key,val),通知视图更新ob.dep.notify()

# 第 14 题-vue 中是如何监听路由 hash 变化的

# 第15题-说说Vue组件间的通信方式

  1. 父组件向子组件通信

  2. 子组件向父组件通信

  3. 隔代组件间通信

  4. 兄弟组件间通信

props,vue自定义事件,消息订阅与发布,vuex,slot

方式1: props

通过在组件上自定义属性方式实现父向子组件通信

通过函数属性实现向子向父通信

缺点: 隔代组件和兄弟组件间通信比较麻烦

方式2: vue自定义事件

Vue内置实现,可以代替函数类型的props

a:绑定监听:<MyComp @eventName='callback'>

b: 触发(分发):事件: this.$emit("eventName",data)

缺点: 只适合子向父通信

方式3:消息订阅与发布

需要引入消息与发布的实现库,如:pubsub-js

a. 订阅消息:PubSub.suscrbe('msg',(msg,data)=>{}) b. 发布消息:PubSub.publish('msg',data);

优点:此方式可实现任意的关系组件的通信

方式4-vuex

vuex是vue官方提供的集中式管理vue多组件共享状态数据的vue插件

优点:对组件间关系没有限制,相比于pubsub库管理更集中,更方便

方式5-slot

专门用来实现父向子传递带数据的标签

a:子组件 b: 父组件

通信的标签模板是在父组件中解析好后在传递给子组件的

白色

关注公众号

一个走心,有温度的号,同千万同行一起交流学习

加作者微信

扫二维码 备注 【加群】

扫码易购

福利推荐