vue高频面试题整理

我心飞翔 分类:javascript

我写的和vue相关的文章:

  • vue实现图片懒加载组件
  • mini-vuex:实现简易响应式全局状态管理
  • 阅读vue源码的姿势
  • vue源码分析和对原理的理解
  • Vue3 的新特性

vue

生命周期钩子函数

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期

  • 参考:vue生命周期

  • Vue中组件生命周期调用顺序

生命周期 描述
beforeCreate 组件实例被创建之初,组件的属性生效之前
created 组件实例已经完全创建,属性也绑定,但真实dom还没有生成,$el还不可用
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update 组件数据更新之后
activited keep-alive专属,组件被激活时调用
deadctivated keep-alive专属,组件被销毁时调用
beforeDestory 组件销毁前调用
destoryed 组件销毁后调用

虚拟dom

  • 由于dom操作耗时十分长,且dom对象的体积很大,单个div的dom属性就有294个之多;
  • Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。
  • VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。
  • Virtual DOM到真实的dom需要经过以下过程:VNode 的 create、diff、patch

参考:虚拟dom

diff

  • vue2 diff算法思路
  • vue3 diff算法的优化

v-model双向数据绑定原理

vue 双向数据绑定是通过 数据劫持 结合 发布订阅模式 的方式来实现的,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变;
核心:Object.defineProperty() 方法。

v-model本质上是语法糖,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件

  • text 和 textarea 元素使用 value 属性和 input 事件
  • checkbox 和 radio 使用 checked 属性和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件
<input v-model="sth" /><!-- 二者等价 -->
<input :value="sth" @input="sth = $event.target.value" />
 

computed和watch区别

  • 参考:计算属性 vs 侦听属性 vs 方法

vue 插槽

设置在自组件内部的插槽像一个盒子,位置由子组件决定,放什么内容由父组件决定。

实现了内容分发,提高了组件自定义的程度,让组件变的更加灵活

  1. 默认插槽:

无需name属性,取子组件肚子里第一个元素节点作为默认插槽。

<!-- 子组件,组件名:child-component -->
<div class="child-page">
    <h1>子页面</h1>
    <slot></slot> <!-- 替换为 <p>hello,world!</p> -->
</div>

<!-- 父组件 -->
<div class="parent-page">
    <child-component>
        <p>hello,world!</p>
    </child-component>
</div>

<!-- 渲染结果 -->
<div class="parent-page">
    <div class="child-page">
        <h1>子页面</h1>
        <p>hello,world!</p>
    </div>
</div>
 
  1. 具名插槽:

在多个插槽的情况下使用,利用name标识插槽。

<!-- 子组件,组件名:child-component -->
<div class="child-page">
<h1>子页面</h1>
<slot name="header"></slot>
<slot></slot>  <!-- 等价于 <slot name="default"></slot> -->
<slot name="footer"></slot>
</div>
<!-- 父组件 -->
<div class="parent-page">
<child-component>
<template v-slot:header>
<p>头部</p>  
</template>
<template v-slot:footer>
<p>脚部</p>
</template>
<p>身体</p>
</child-component>
</div>
<!-- 渲染结果 -->
<div class="parent-page">
<div class="child-page">
<h1>子页面</h1>
<p>头部</p>
<p>身体</p>
<p>脚部</p>
</div>
</div>
  1. 作用域插槽:

子组件给父组件传递数据。

<!-- 子组件,组件名:child-component -->
<div class="child-page">
<h1>子页面</h1>
<slot name="header" data="data from child-component."></slot>
</div>
<!-- 父组件 -->
<div class="parent-page">
<child-component>
<template v-slot:header="slotProps">
<p>头部: {{ slotProps.data }}</p>
</template>
</child-component>
</div>
<!-- 渲染结果 -->
<div class="parent-page">
<div class="child-page">
<h1>子页面</h1>
<p>头部: data from child-component.</p>
</div>
</div>
  • 插槽源码分析参考:vue插槽源码简析

Vue与Angular以及React的区别?

Vue与AngularJS的区别

  • Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
  • AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
  • AngularJS社区完善, Vue的学习成本较小

Vue与React的区别

  • vue组件分为全局注册和局部注册,在react中都是通过import相应组件,然后模版中引用;
  • props是可以动态变化的,子组件也实时更新,在react中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过props来更改视图;
  • 子组件一般要显示地调用props选项来声明它期待获得的数据。而在react中不必需,另两者都有props校验机制;
  • 每个Vue实例都实现了事件接口,方便父子组件通信,小型项目中不需要引入状态管理机制,而react必需自己实现;
  • vue使用插槽分发内容,使得可以混合父组件的内容与子组件自己的模板;
  • vue多了指令系统,让模版可以实现更丰富的功能,而React只能使用JSX语法;
  • Vue增加的语法糖computedwatch,而在React中需要自己写一套逻辑来实现;
  • react的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等;而 vue是把html,css,js组合到一起,用各自的处理方式,vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
  • react做的事情很少,很多都交给社区去做,vue很多东西都是内置的,写起来确实方便一些,比如 reduxcombineReducer就对应vuexmodules, 比如reselect就对应vuex的getter和vue组件的computed, vuex的mutation是直接改变的原始数据,而redux的reducer是返回一个全新的state,所以redux结合immutable来优化性能,vue不需要。
  • react是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合redux-form,组件的横向拆分一般是通过高阶组件。而vue是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用mixin

$route$router的区别

  • $route是路由信息对象,包括path,params,hash,query,fullPath,matched,name等路由信息参数。
  • 而$router是路由实例对象包括了路由的跳转方法,钩子函数等。

Vue的SPA 如何优化加载速度

  • 减少入口文件体积
  • 静态资源本地缓存
  • 开启Gzip压缩
  • 使用SSR,nuxt.js

vue项目中的优化

编码阶段

  • 不要在模板里面写过多表达式
  • 尽量减少data中的数据,data中的数据都会增加gettersetter,会收集对应的watcher
  • v-ifv-for不能连用
  • 如果需要使用v-for给每项元素绑定事件时使用事件代理
  • SPA 页面采用keep-alive缓存组件
  • 频繁切换的使用v-show,不频繁切换的使用v-if
  • 循环调用子组件时添加key,key保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

SEO优化

  • 预渲染
  • 服务端渲染SSRnuxt.js

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用cdn加载第三方模块
  • 多线程打包happypack
  • splitChunks抽离公共文件
  • sourceMap优化

用户体验

  • 骨架屏
  • PWA渐进式Web应用,使用多种技术来增强web app的功能,让网页应用呈现和原生应用相似的体验。

还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。

vue有了数据响应式,为何还要diff?

核心原因:粒度

  • React通过setState知道有变化了,但不知道哪里变化了,所以需要通过diff找出变化的地方并更新dom。
  • Vue已经可以通过响应式系统知道哪里发生了变化,但是所有变化都通过响应式会创建大量Watcher,极其消耗性能,因此vue采用的方式是通过响应式系统知道哪个组件发生了变化,然后在组件内部使用diff。这样的中粒度策略,即不会产生大量的Watcher,也使diff的节点减少了,一举两得。

vue模版编译

编译的核心是把 template 模板编译成 render 函数,主要分为如下三个步骤:

  • 生成AST树
  • 优化
  • codegen

详情参考:Vue template模版编译

MVVM

MVVMModel-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModelModel层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

响应式数据原理(Vue2.x & Vue3.0)

Vue2.x在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher),如果属性发生变化会通知相关依赖进行派发更细(发布订阅模式)。

vue3.0采用es6中的proxy代替Object.defineProperty做数据监听。

详情参考:深入vue响应式原理

Proxy与Object.defineProperty的优劣对比?

Proxy的优势如下:

  • Proxy可以直接监听对象而非属性
  • Proxy可以直接监听数组的变化
  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

Object.defineProperty的优势如下:

  • 兼容性好,支持IE9

vue的$nextTick

在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用

  • Promise
  • MutationObserver
  • setImmediate
  • 如果以上都不行则采用setTimeout

定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

vue组件中data为什么必须是一个函数

复用组件的时候,都会返回一份新的data,相当于每个组件实例都有自己私有的数据空间,不会共享同一个data对象。

vue中组件通信的方式

  1. 父传子:props
  2. 子传父:$emitref
  3. 兄弟:EventBus

vue-router

vue-router是vue的官方插件,主要用来管理前端路由。
对于 Vue 这类渐进式前端开发框架,为了构建 SPA(单页面应用),需要引入前端路由系统,这也就是 Vue-Router 存在的意义。前端路由的核心,就在于:改变视图的同时不会向后端发出请求

功能有:

  • 改变URL且不让浏览器向服务器发出请求
  • 检测URL的改变
  • 记录当前页面的状态
  • 可以使用浏览器的前进后退功能
  • URL路径决定页面如何显示

history和hash模式的区别

1. 实现原理

hash 模式和 history 模式都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由。

2. 对比表格

区别 \ mode hash history
监听事件 hashChange popstate
缺点 # 号不好看 子路由刷新404、ie9及以下不兼容
push操作 window.location.assign window.history.pushState
replace操作 window.location.replace window.history.replaceState
访问操作 window.history.go window.history.go
后退操作 window.history.go(-1) window.history.go(-1)
向前操作 window.history.go(1) window.history.go(1)

3. 关于 popstate 事件监听路由的局限
history对象的 back(), forward() 和 go() 三个等操作会主动触发 popstate 事件,但是 pushState 和 replaceState 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

4. 关于子路由刷新的解决方式

history模式子路由刷新会404,因此需要后端配合,将未匹配到的路由默认指向html文件
具体参考:vue-router history模式详解

5. 浏览器(环境)兼容处理

history 模式中pushStatereplaceStateHTML5的新特性,在 IE9 下会强行降级使用 hash 模式,非浏览器环境转换成abstract 模式。

6. router-link
router-link点击相当于调用$router.push方法去修改url

<router-link> 比起写死的 <a href="..."> 会好一些,理由如下:

  1. 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。
  2. 在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面。
  3. 当你在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写(基路径)了。

vue-router路由懒加载

像 vue 这种单页面应用,如果没有路由懒加载,运用 webpack 打包后的文件将会很大,造成进入首页时,需要加载的内容过多,出现较长时间的白屏,运用路由懒加载则可以将页面进行划分,需要的时候才加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时。

  • 参考: vue路由懒加载

vue 路由懒加载有以下三种方式:

  1. vue 异步组件
  2. ES6 的 import()
  3. webpack 的 require.ensure()

1. vue 异步组件
这种方法主要是使用了 resolve 的异步机制,用 require 代替了 import 实现按需加载

export default new Router({
routes: [
{
path: '/home',',
component: (resolve) => require(['@/components/home'], resolve),
},
{
path: '/about',',
component: (resolve) => require(['@/components/about'], resolve),
},
],
})

2. ES6 的 import()
vue-router 在官网提供了一种方法,可以理解也是为通过 Promise 的 resolve 机制。因为 Promise 函数返回的 Promise 为 resolve 组件本身,而我们又可以使用 import 来导入组件。

export default new Router({
routes: [
{
path: '/home',
component: () => import('@/components/home'),
},
{
path: '/about',
component: () => import('@/components/home'),
},
],
})

1. webpack 的 require.ensure()
这种模式可以通过参数中的 webpackChunkName 将 js 分开打包。

export default new Router({
routes: [
{
path: '/home',
component: (resolve) => require.ensure([], () => resolve(require('@/components/home')), 'home'),
},
{
path: '/about',
component: (resolve) => require.ensure([], () => resolve(require('@/components/about')), 'about'),
},
],
})

vuex

nuxt.js

回复

我来回复
  • 暂无回复内容