前端面试

2,657

vue

vue生命周期(频率高,基本题)

一个完整的vue生命周期会经历以下钩子函数

4个阶段(可以理解为一个组件的生、老、病、死),8个钩子函数

beforeCreate --- 创建前

created --- 创建完成

beforeMount --- 挂载前

mounted --- 挂载完成

beforeUpdate --- 更新前

updated --- 更新完成

beforeDestroy --- 销毁前

destroyed --- 销毁完成

image.png


beforeCreate是new Vue()之后触发的第一个钩子,一个组件生命的开始,当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。

created阶段实例已经创建完成,已经完成了数据观测, 可以使用数据,更改数据,在这里更改数据不会触发updated函数。

结合代码看一下吧

  • HTML
<template>
 <div id="app">
    <h1 ref="hello" id="hello">{{message}}</h1>
    <button @click="changeMsg">change</button>
 </div>
</template>
  • DATA
  data () {
    return {
      message: '娟娟和她的三只小猫咪'
    }
  },
  • js
    beforeCreate: function() {
      console.group('------beforeCreat 创建前------');
      console.log("%c%s", "color: green","data   : " + this.$data); 
      console.log("%c%s", "color: green","message: " + this.message);  
    },
    created: function() {
      console.group('------created 创建完成------');
      console.log("%c%s", "color: green","data   : " + this.$data); 
      console.log("%c%s", "color: green","message: " + this.message);  
      let hello = document.getElementById('hello')
      console.log('hello:',hello)
    },

看看控制台的打印结果,是不是很直观

image.png


image.png


beforeMount发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。

mounted在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。

结合代码看一下吧

      beforeMount: function() {
      console.group('------beforeMount 挂载前------');
      console.log(this.$el);
      console.log("%c%s", "color:green","data   : " + this.$data); //已被初始化  
      console.log("%c%s", "color:green","message: " + this.message); //已被初始化  
      let hello = document.getElementById('hello')
      console.log('hello:',hello)
    },
      mounted: function() {
      console.group('------mounted 挂载完成------');
      console.log(this.$el);
      console.log("%c%s", "color:green","data   : " + this.$data); 
      console.log("%c%s", "color:green","message: " + this.message); 
      let hello = document.getElementById('hello')
      console.log('hello:',hello)
    },

image.png


image.png


beforeUpdate发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

updated发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

beforeDestroy发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器,可能导致内存泄漏,变量污染的一些变量,方法等

destroyed发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也被销毁。


如果使用组件的keep-alive功能时,增加两个周期:

activatedkeep-alive组件激活时调用;

deactivatedkeep-alive组件停用时调用。

<keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。<keep-alive>是一个抽象组件,它自身不会渲染一个DOM元素,也不会出现在父组件链中。 当在<keep-alive>内切换组件时,它的activateddeactivated这两个生命周期钩子函数将会执行。


既然生命周期讲了这么多顺便讲一些vue3中生命生命周期的变化

vue3.x生命周期变化

被替换

  • beforeCreate -> setup()

  • created -> setup()

重命名

  • beforeMount -> onBeforeMount

  • mounted -> onMounted

  • beforeUpdate -> onBeforeUpdate

  • updated -> onUpdated

  • beforeDestroy -> onBeforeUnmount

  • destroyed -> onUnmounted

  • errorCaptured -> onErrorCaptured

新增的

新增的以下2个方便调试 debug 的回调钩子:

  • onRenderTracked

  • onRenderTriggered

可以查看官方api

特别说明 在 Vue3.x 是兼容 Vue2.x 的语法的,因此 Vue2.x 的语法能正常在 Vue3.x 中运行,比如:虽然 beforeCreate 、 created 被 setup() 函数替代了,但是如果你要用,代码也是正常执行的。只是在 Vue3.x 中建议使用 setup(),而不是旧的API,但是,以下2个生命周期钩子函数被改名后,在 Vue3.x 中将不会再有 beforeDestroy 和 destroyed

  • beforeDestroy -> onBeforeUnmount

  • destroyed -> onUnmounted


深入理解生命周期,比如说API请求一般会放在哪个阶段,各个阶段data,method,dom的区别,各阶段的特殊所在。切忌盲目背完8个生命钩子。

vue常用指令

感觉除了红圈圈里的指令,其他都经常用的,面试官主要是想引出后面的问题

image.png

v-show 和 v-if 的区别

  • v-if是条件渲染,当条件不成立时,不会渲染DOM元素 某一块代码在运行时条件很少改变,使用 v-if 较好 (v-if 有更高的切换开销)

  • v-show操作的是样式(display),DOM元素已经渲染完成,切换当前DOM的显示和隐藏。 需要非常频繁地切换某块代码,使用 v-show渲染

v-if v-for 为什么不建议同时使用

在vue2官方文档中有做专门的说明

image.png 当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费

但是VUE3有做出改变

image.png

v-model原理

只是一个语法糖,v-bind 数据绑定 与 v-on处理函数绑定的语法糖 ,使用 v-bind 获取 value, v-on 绑定 input 触发事件

使用 v-model 双向数据绑定事件:

<input v-model = 'doSomething'>

等价于下面的代码

 <input :value="doSomething" @input="doSomething = $event.target.value" />

vue组件通信

父子组件通信

    父->子props,子->父 $on$emit
    获取父子组件实例 $parent$children
    Ref 获取实例的方式调用组件的属性或者方法

兄弟组件通信

   Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue
   Vuex

跨级组件通信

  Vuex
  $attrs$listeners
  Provide、inject

vue MVVM

MVVM: Model-View-ViewModel


MVVM是MVC的改进版,讲MVVM之前先讲一下MVC

image.png

MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

  • Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它可以带有逻辑,会在数据变化时更新控制器。

  • View(视图) - 视图代表模型包含的数据的可视化。

  • Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。

ViewController 负责 View 和 Model 之间调度,View 发生交互事件会回调给 ViewController ,与此同时ViewController 还要把传来的数据传输给 View 用于展示。 随着业务越来越复杂,视图交互越复杂,Controller 承受就越多。


image.png

  • View层:视图展示。包含UIView以及UIViewController,View层是可以持有ViewModel的。

  • ViewModel层:视图适配器。暴露属性与View元素显示内容或者元素状态一一对应。一般情况下ViewModel暴露的属性建议是readOnly的。ViewModel层可以持有Model。

  • Model层:数据模型与持久化抽象模型。数据模型很好理解,就是从服务器拉回来的JSON数据。而持久化抽象模型暂时放在Model层,是因为MVVM诞生之初就没有对这块进行很细致的描述。按照经验,我们通常把数据库、文件操作封装成Model,并对外提供操作接口。


MVVM有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

vue的工作机制

image.png

初始化

在 new Vue() 时会调用 _init() 初始化,会初始化各种实例方法、全局方法等; 初始化之后会调用 $mount 挂载组件,主要执行编译和首次更新

编译

编译模块分为三个阶段

  • parse:使⽤用正则解析template中的vue的指令(v-xxx) 变量量等等形成抽象语法树AST
  • optimize:标记⼀一些静态节点,⽤用作后⾯面的性能优化,在diff的时候直接略略过
  • generate:把第⼀一部⽣生成的AST 转化为渲染函数 render function

虚拟dom

Virtual DOM 是react⾸首创,Vue2开始支持,就是⽤ JavaScript 对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对⽐很快,⽽真实的dom操作太慢。

Vue模版编译原理

Vue的编译过程就是将template转化为render函数的过程

• 解析模板 生成AST语法树 然后大量正则解析

• 然后将AST树转化为可执行代码

image.png

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

  • 组件的调用顺序都是先父后子, 渲染完成的顺序是先子后父。
  • 组件的销毁操作是先父后子, 销毁完成的顺序是先子后父。

keep-alive组件

作用:避免组件重新渲染。

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated

nextTick的实现原理

  • 在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM
  • nextTick主要使用了宏任务和微任务
  • 根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列

Computed和Watch

  • Computed本质是一个具备缓存的watcher,基于它的响应式依赖进行缓存。

  • 计算属性发生变化时,不会立即重新计算,不会立即重新渲染

  • 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。

  • computedgetter函数没有副作用,易于测试。


  • Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。

注意:不使用箭头函数来定义 watcher 函数,箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例。

data为什么是一个函数

vue 将会递归将 data 的 property 转换为 getter/setter,从而让 data 的 property 能够响应数据变化。当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

Vue事件绑定

原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的。

vue自定义指令

  • 全局自定义指令:vue.js对象提供了directive方法,可以用来自定义指令,directive方法接收两个参数,一个是指令名称。

  • 函数:局部自定义指令,通过组件的directives属性定义。

vue自定义组件

使用Vue.extend方法创建一个组件,使用Vue.component方法注册组件,子组件需要数据,可以在props中接收数据,而子组件修改好数据后,想要把数据传递给父组件,可以使用emit方法。

<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })

// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')

在components目录中新建组件文件,脚本一定要导出暴露的接口;导入需要用到的页面;将导入的组件注入vue.js的子组件的components属性中;在template的视图中使用自定义组件。

为什么不建议用index作为key

使用index作为key值的时候,其实相当于没有使用key,解释如下:

因为在diff算法中,会根据真实DOM生成一个虚拟DOM(virtual DOM),当数据改变,也就是virtual DOM的某个节点数据改变后会生成一个新的virtual DOM,然后新旧之间对比,如果有不一样的地方,会直接在真实DOM中修改,新的virtual DOM会替换之前的virtual DOM。

vue-cli目录(较少)

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,致力于将 Vue 生态中的工具基础标准化。

通过 @vue/cli 实现的交互式的项目脚手架。

通过 @vue/cli + @vue/cli-service-global 实现的零配置原型开发。

提供一个运行时依赖 (@vue/cli-service),基于 webpack 构建,并带有合理的默认配置;可以通过项目内的配置文件进行配置;可以通过插件进行扩展。

assets文件夹存放静态资源;

components存放组件;

router定义路由相关的配置;

view是视图;

app.vue是一个应用主组件;

main.js是入口文件。

vue-cli版本区别(被问到一次)

vue.js的template编译

通过 compile 编译器把 template 编译成 ASTcompilecreateCompiler 的返回值,createCompiler 用来创建编译器,compile 还负责合并 option AST 会经过 generateAST 转换成 render function 字符串的过程得到 render 函数,render 的返回值是 VNodeVNodeVue.js 的虚拟 DOM 节点。

AST: 在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。

createCompiler: createCompiler用以创建编译器,返回值是compile以及compileToFunctions。

compile: 是一个编译器,它会将传入的template转换成对应的AST树、render函数以及staticRenderFns函数。而compileToFunctions则是带缓存的编译器,同时staticRenderFns以及render函数会被转换成Funtion对象。

详细可以参考这篇文章 template编译

vue.js中的ref属性

ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件。

因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs 也不是响应式的,因此你不应该试图用它在模板中做数据绑定。

vue项目中解决跨域问题

在config/index.js内对proxyTable项进行配置changeOrign: true

proxyTable: {
 '/api': {
   target: 'http://xxx.com',
   changeOrign: true,    在config/index.js内对proxyTable项进行配置changeOrign: true
   pathRewrite: {
    '^/api': ''
   }
  }
}

vue-router

路由懒加载是什么意思?如何实现路由懒加载?

把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件

实现:

const Foo = () => import('./Foo.vue')
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ]})

参数传值

参数过长或不希望用户看到参数应使用params传参,query会有URL长度限制;

如果希望页面刷新后,保留参数状态,应使用query传参;

参数应该是一个key/value类型的对象,注意:value不能是引用类型,否则容易导致参数丢失。如果value为Object或Array,应将参数扁平化或使用JSON.stringify(value)后再传递。

vuex

vuex在我之前的项目中用的比较少,所以很多知识没有进行深入的学习,后来面试有问到几次都吞吞吐吐说不上来,后来仔细的阅读了 vuex 官方文档,其实文档已经讲解的十分详细了。

image.png

简单介绍一下vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。是一种思想,通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,代码将会变得更结构化且易维护。 Vuex 应用的核心就是 storeVuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。

vuex核心概念State

Vuex中唯一的数据源,使用驼峰式命名规范,在对State取值时为了保证视图层健壮,使用mapState辅助函数映射到computed计算属性中。避免直接使用this.$store.state直接取值。

computed: mapState([
  // 映射 this.count 为 store.state.count
  'count'
])

vuex核心概念Getter

Getter类似于计算属性,是对State数据源计算后得到结果,在Getter使用中不要直接返回State的原始数据,对于State的值计算时建议放在组件中进行。

state: {
  todos: [
    { id: 1, text: '...', done: true },
    { id: 2, text: '...', done: false }
  ]
},
getters: {
  // 计算可也可以根据需求放到组件computed中
  doneTodos: state => {
    return state.todos.filter(todo => todo.done)
  },
  // 这样使用getter时可以直接使用mapState映射到computed,不必要增加冗余代码
  doneTodos: state => state.todos  
}

// 可以替代一些getter
computed: {
  ...mapState({
    doneTodos: state => state.todos.filter(todo => todo.done)
  })
}

vuex核心概念Mutation

Mutation必须是同步函数,在触发Mutation修改State时优先使用Action触发Mutation,使用常量替代Mutation事件类型,常量的事件类型可以注册在mutationTypes.js中。

// mutationTypes.js
export const SOME_MUTATION = 'SOME_MUTATION'

import { SOME_MUTATION } from './mutationTypes.js
mutations: {
  // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
  [SOME_MUTATION] (state) {
    // mutate state
  }
}

vuex核心概念Module

State数据源很大时,可以根据实际情况将State拆分成不同的module,当拆分时,对每个module都要开启命名空间,当拆分出model后为保证项目的逻辑的清晰尽量避免使用全局的model(全局module也能当成一个子module注册使用),同时也不要使用过深的子module的嵌套,造成不必要的困难。

vuex 和全局变量的区别

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

注意:通过提交 mutation 的方式,而非直接改变 store.state.count,是因为想要更明确地追踪到状态的变化。

mutation 和 action 的区别

action 类似于 mutation ,不同在于:

  • action 提交的是 mutation ,而不是直接变更状态。

  • action 可以包含异步操作

mutation 为什么不能异步

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

Mutation 必须是同步函数,直接上官方解释吧

image.png