vue面试整理

19,864 阅读12分钟

对MVVM的理解?

MVVM是Model-View-ViewModel的缩写,Model代表数据模型负责业务逻辑和数据封装,View代表UI组件负责界面和显示,ViewModel监听模型数据的改变和控制视图行为,处理用户交互,简单来说就是通过双向数据绑定把View层和Model层连接起来。在MVVM架构下,View和Model没有直接联系,而是通过ViewModel进行交互,我们只关注业务逻辑,不需要手动操作DOM,不需要关注View和Model的同步工作。

vue等单页面应用及优缺点

vue核心是一个响应的数据绑定系统,mvvm,数据驱动,组件化,轻量,简洁,高效,快速,模块友好。

缺点:不支持低版本浏览器,最低到IE9,不利于SEO的优化,首页加载时间较长,不可以使用浏览器的导航按钮需要自行实现前进后退。

什么是虚拟DOM?

虚拟dom是相对于浏览器所渲染出来的真实DOM的,在react/vue等技术出现之前,我们要改变页面展示的内容只能通过遍历查询dom树的方式找到需要修改的dom然后修改样式行为或结构,来达到更新UI的目的。

这种方式相当消耗计算资源,因为每次查询dom几乎都需要遍历整颗dom树,如果建立一个与dom树对应的虚拟dom对象(js对象),以对象嵌套的方式来表示dom树,那么每次dom的更改就变成了js对象的属性的更改,这样一来就能查找js对象的属性变化要比查询dom树的性能开销小。

为什么虚拟DOM会提高性能?

虚拟DOM相当于在js和真实DOM中间加了一个缓存,利用dom diff算法避免了没有必要的dom操作,从而提高性能。 具体实现步骤如下:

  • 用JavaScript对象结构表示DOM树的结构,然后用这个树构建一个真正的DOM树,插到文档中
  • 当状态变更时,重新构造一颗新的对象树,然后用新的树和旧的树进行比较,记录两棵树差异
  • 把2所记录的差异应用到1所构建的真正的DOM树上,视图就更新了

什么是RESTful API,然后怎么使用?

RESTful是一个api的标准,无状态请求。请求的路由地址是固定的。 restful:给用户一个url,根据method不同在后端做不同处理:比如post 创建数据,get 获取数据,put和patch修改数据,delete删除数据

vue-cli中src目录下每个文件的用途?

1.vue-cli名字改为@vue/cli,所以全局安装了旧版的要通过npm install vue-cli -g卸载。

安装新版vue-cli

npm install -g @vue/cli

2.创建一个项目 vue create hello-world 3.assets文件夹是放静态资源;components是放组件;router是定义路由相关的配置;view视图;app.vue是一个应用主组件;main.js是入口文件

v-model的原理

语法糖

<input v-model="msg" />

相当于

<input v-bind:value="msg" v-on:input="msg=$event.target.value" />

写vue项目时,为什么要在组件中写key,其作用是什么?

key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff的速度。

v-if和v-show的区别

v-show只是在display: none和display: block之间切换,只需要切换CSS,DOM还是一直保留着,v-show在初始渲染时有更高的开销,但是切换开销很小,更适合频繁切换的场景

v-if涉及到vue底层的编译,当属性初始为false时组件不会被渲染,直到条件为true,并且切换条件时会触发销毁/挂载组件,切换时开销更高,更适合不经常切换的场景

删除数组用delete和Vue.delete有什么区别

  • delete:只是被删除数组成员变为empty/undefined,其他元素键值不变
  • Vue.delete:直接删除数组成员,并改变了数组的键值(对象是响应式的,确保删除能触发更新视图,这个方法主要用于避开vue不能检测到属性被删除的限制)

route和router的区别

route是路由信息对象,包括path,params,hash,query,fullPath,matched,name等路由信息参数。

router是路由实例对象,包括了路由的跳转方法,钩子函数。

怎么定义vue-router的动态路由?怎么获取传过来的值

在router目录下的index.js文件,对path属性加上:id,

使用router对象的params.id获取

active-class是哪个组件的属性?嵌套路由怎么定义

active-class是vue-router模块中router-link组件的属性

使用children定义嵌套路由

对keep-alive的了解

keep-alive是一个内置组件,可使被包含的组件保留状态或避免重新渲染,有include(包含的组件缓存)和exclude(排除的组件不缓存)两个属性。

vue常用修饰符

.prevent: 提交时间不再重载页面

.stop:阻止单击事件冒泡

.self:当事件发生在该元素本身而不是子元素的时候触发

.capture:事件侦听,事件发生的时候会调用

组件中data什么时候可以适用对象

组件复用时所有组件实例都会共享data,如果data是对象就会造成一个组件修改data以后会影响到其他所有组件,所以需要将data写成函数,每次用到就调用一次函数获得新的数据

当我们使用new Vue()的方式的时候,无论我们将data设置为对象还是函数都是可以的,因为new Vue()的方式是生成一个根组件,该组件不会复用,也就不存在共享data的情况

vue如何监听键盘事件?

1.@keyup.方法

<template>
  <input ref="myInput" type="text" value="helloworld" @keyup.enter='handleKey'>
</template>
<script>
export default {
  name: 'HelloWorld',
  methods: {
    handleKey(e) {
      console.log(e)
    }
  }
}
</script>

2.addEventListener

<template>
  <input ref="myInput" type="text" value="helloworld" @keyup.enter='handleKey'>
</template>
<script>
export default {
  name: 'HelloWorld',
  mounted(){
    document.addEventListener('keyup', this.handleKey)
  },
  beforeDestroy() {
    document.removeEventListener('key', this.handleKey)
  },
  methods: {
    handleKey(e) {
      console.log(e)
    }
  }
}
</script>

vue-cli如何新增自定义指令?

1.创建局部指令

directives:{
   // 指令名称
   dir1: {
       inserted(el){
           // 第一个参数是当前使用指令的DOM
           el.style.width = '200px';
           el.style.height = '200px';
           el.style.background = '#000'
       }
   }
}

2.全局指令

Vue.directive('dir2', {
    inserted(el){
        console.log(el)
    }
})

3.指令的使用

<div v-dir1></div>
<div v-dir2></div>

vue如何自定义一个过滤器?

<input type="text" v-model="msg" />
{{msg | capitalize}}

data(){
    return{
        msg: ''
    }
},
filters: {
    capitalize: function(value){
        if(!value) return "";
        value = value.toString();
        return value.charAt(0).toUpperCase()+value.slice(1)
    }
}

computed和watch区别

computed是计算属性,依赖其他属性计算值,并且computed的值有缓存,只有当计算值变化才会返回内容

watch监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。

一般来说需要依赖别的属性来动态获得值的时候可以使用computed,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用watch

另外computed和watch还支持对象的写法

data: {
    firstName: 'Chen',
    lastName: 'Miao',
    fullName: 'Chen Miao'
},
watch: {
    firstName: function(val){
        this.fullName = val+ ' '+this.lastName
    },
    lastName: function(val){
        this.fullName = this.firstName+ ' '+val
    }
},
computed: {
    anoFullName: function(){
        return this.firstName+' '+this.lastName
    }
}

watch怎么深度监听对象变化

deep设置为true就可以监听到对象的变化

watch: {
    msg: {
      handler(newMsg, oldMsg) {
        console.log(newMsg);
      },
      immediate: true,
      deep: true,
    }
 }

extend能做什么?

作用是扩展组件生成一个构造器,通常与$mount一起使用。

// 创建组件构造器
let Component = Vue.extend({
    template: '<div>test</div>'
})
// 挂载到#app上
new Component().$mount('#app')

// 扩展已有组件
let SuperComponent = Vue.extend(Component)
new SuperComponent({
    created(){
        console.log(1)
    }
})
new SuperComponent().$mount('#app')

mixin和mixins区别

mixin用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。

Vue.mixin({
    beforeCreate(){
        // 会影响到每个组件的beforeCreate钩子函数
    }
})

mixins最常用的扩展组件的方式。如果多个组件有相同的业务逻辑,就可将这些逻辑剥离出来,通过mixins混入代码。需要注意:mixins混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

如何使用vue.nextTick()?

nextTick可以使我们在下次DOM更新循环结束之后执行延迟回调,用于获得更新后的DOM

data:function(){
    return {
        message: '没有更新'
    }
},
methods: {
    updateMessage: function(){
        this.message='更新完成'
        console.log(this.$el.textContent) // '没有更新'
        this.$nextTick(function(){
          console.log(this.$el.textContent)// '更新完成'  
        })
    }
}

transition 过渡的实现原理

<transition name="fade1">
    <router-view></router-view>
</transition>

类名介绍:

  • v-enter:定义进入过渡的开始状态
  • v-enter-active:定义进入过渡生效时的状态
  • v-enter:定义进入过渡的结束状态
  • v-leave:定义离开过渡的开始状态
  • v-leave-active:定义离开过渡生效时的状态
  • v-leave-to:定义离开过渡的结束状态

在vue项目中如何引入第三方库(比如jQuery)?有哪些方法可以做到?

1.绝对路径直接引入

// 在index.html中用script引入
<script src="./static/jquery-1.12.4.js"></script>
// 在webpack中配置external
externals: {'jquery':'jQuery'}
// 在组件中使用时import
import $ from 'jquery'

2.在webpack中配置alias

resolve: {
    extensions: ['.js','.vue','.json'],
    alias {
        '@':resolve('src'),
        'jquery':resolve('static/jquery-1.12.4.js')
    }
}
// 在组件中使用时import
import $ from 'jquery'

3.在webpack中配置plugins

plugins: [
    new webpack.ProvidePlugin({ $: 'jquery' })
}

全局使用,但在使用eslint情况下会报错,需要在使用了$的代码前添加/*eslint-disable*/来去掉ESLint的检查。

简单说一下组件通信

父子通信

1.props和emit

父组件通过props传递数据给子组件,子组件通过emit发送事件传递给父组件。

// 父组件
<div>
    <child :data="child" @send="getFromChild"></child>
</div>

data(){
    return{
        toChild: '大儿子',
        fromChild: ''
    }
},
methods: {
    getFromChild(val){
        this.fromChild=val
    }
}
// 子组件
<div @click="toParent">{{data}}</div>

props:[data],
methods: {
    toParent(){
        this.$emit('send', '给父亲')
    }
}

2.v-model

v-model其实是props,emit的语法糖,v-model默认会解析成名为value的prop和名为input的事件。

// 父组件
<children v-model="msg"></children>
<p>{{msg}}</p>

data(){
    return{
        msg:'model'
    }
}
// 子组件
<input :value="value" @input="toInput" />

props: ['value'],
methods: {
    toInput(e){
        this.$emit('input', e.target.value)
    }
}

3.在父组件使用$children访问子组件,在子组件中使用$parent访问父组件

// 父组件
<child />

data(){
    return {
        msg: '父组件数据'
    }
},
methods: {
    test(){
        console.log('我是父组件的方法,被执行')
    }
},
mounted(){
    console.log(this.$children[0].child_msg); // 执行子组件方法
}
// 子组件
<div>{{$parent.msg}}</div>

data(){
    return{
        child_msg: '子组件数据'
    }
},
mounted(){
    // 子组件执行父组件方法
    this.$parent.test(); 
}

$listeners$attrs

$attrs--继承所有父组件属性(除了prop传递的属性)

inheritAttrs--默认值true,继承所有父组件属性(除props),为true会将attrs中的属性当做html的data属性渲染到dom根节点上

$listeners--属性,包含了作用在这个组件上所有监听器,v-on="$listeners"将所有事件监听器指向这个组件的某个特定子元素

// 父组件
<children :child1="child1" :child2="child2" @test1="onTest1"
@test2="onTest2"></children>

data(){
    return {
        child1: 'childOne',
        child2: 'childTwo'
    }
},
methods: {
    onTest1(){
        console.log('test1 running')
    },
    onTest2(){
        console.log('test2 running')
    }
}

// 子组件
<p>{{child1}}</p>
<child v-bind="$attrs" v-on="$listeners"></child>

props: ['child1'],
mounted(){
    this.$emit('test1')
}

// 孙组件
<p>{{child2}</p>
<p>{{$attrs}}</p>

props: ['child2'],
mounted(){
    this.$emit('test2')
}

.sync方式

在vue1.x中是对prop进行双向绑定,在vue2只允许单向数据流,也是一个语法糖

// 父组件
<child :count.sync="num" />

data(){
    return {
        num: 0
    }
}
// 子组件
<div @click="handleAdd">add</div>

data(){
    return {
        counter: this.count
    }
},
props: ["count"],
methods: {
    handleAdd(){
        this.$emit('update:count', ++this.counter)
    }
}

兄弟组件通信

可以通过查找父组件中的子组件实现, this.$parent.$children$children中可以通过组件name查询到需要的组件实例,然后进行通信

跨多层次组件通信

可以使用provide/inject,虽然文档中不推荐直接使用在业务中。

假设有父组件A,然后有一个跨多层次的子组件B

// 父组件A
export default{
    provide: {
        data: 1
    }
}
// 子组件B
export default{
    inject: ['data'],
    mounted(){
        // 无论跨几层都能获取父组件的data属性
        console.log(this.data); // 1
    }
}

任意组件

可以用Vuex或Event Bus解决

eventBus的使用

1.新建一个bus.js文件

import Vue from 'vue';
export default new Vue();

2.使用它

<div @click="addCart">添加</div>
import Bus from 'bus.js';
export default{
    methods: {
        addCart(event){
            Bus.$emit('getTarget', event.target)
        }
    }
}
// 另一组件
export default{
    created(){
        Bus.$on('getTarget', target =>{
            console.log(target)
        })
    }
}

vue-router路由

单页面应用SPA的核心之一是:更新视图而不重新请求页面

普通路由

router.push('home')

router.push({path: 'home')

命名路由

const router=new VueRouter({
    routes: [{
        path: '/user',
        name: 'user',
        component: User
    }]
})
<router-link :to="{name: 'user'}"></router-link>
router.push({
    name: 'user'
})

动态路由匹配

<div>{{$route.params.id}}</div>
const router = new VueRouter({
    routes: [{
        path: '/user/:id',
        component: User
    }]
})

router.push({name:'user',params: {id: 123})

嵌套路由

<h2>{{$route.params.id}}</h2>
<router-view></router-view>
const router = new VueRouter({
    routes: [{
        path: '/user/:id',
        children: [{
            // 当/user/:id
            path: '',
            component: UserHome
        },{
            // 当/user/:id/profile
            path: 'profile',
            component: UserProfile
        }]
    }]
})

参数的路由

router.push({path:'register',query:{plan:'private'})

编程式导航

router.push()

参数:

1.字符串

router.push('home')

2.对象

router.push({path: 'home'})

3.命名的路由

router.push({ name: 'user', params: { userId: 123 } })

4.带查询参数,变成/register?plan=private

router.push({ path: 'register', query: { plan: 'private' } })

router.replace()

不会向history添加新纪录,替换当前的history记录

点击<router-link :to="..." replace>等同于调用router.replace(...)

router.go(n)

在历史记录中向前或向后退多少步

// 前进一步,等同history.forward()
router.go(1)
// 后退一步
router.go(-1)
// 前进3步记录
router.go(3)

命名视图

<router-view></router-view>
<router-view name="a"></router-view>
<router-view name="b"></router-view>
const router = new VueRouter({
    routes: [{
        path: '/',
        components: {
            default: Foo,
            a: Bar,
            b: Baz
        }
    }]
})

vue路由的钩子函数

导航钩子主要用来拦截导航,让它完成跳转或取消。

router.beforEach((to,from,next) => {})

钩子是异步执行解析的,每个钩子方法接收三个参数:

to: Route即将进入的目标路由对象

from: Route当前导航正要离开的路由

next: Function,调用该方法来resolve这个钩子,执行效果看参数

  • next():进行下一个钩子

  • next(false):中断当前的导航

  • next('/')或next({path: '/'}):跳转到另一地址

请详细说下你对vue生命周期的理解

生命周期共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后

创建前/后:在beforeCreated阶段,vue实例的挂载元素el和数据对象data都为undefined,还未初始化。created阶段,vue实例的数据对象data有了,el还没有。

载入前后:在beforeMount阶段,vue实例的el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染

更新前/后:当data变化时,会触发beforeUpdated和updated方法

销毁前/后:beforeDestroy在实例销毁前调用,实例仍然完全可用。destroy在实例销毁之后调用,调用后所有事件监听器会被移除,所有子实例也会被销毁。

生命周期的作用?

生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程中更容易形成好的逻辑。

vue在created和mounted这两个生命周期中请求数据有什么区别呢?

一般在created里就可以,如果涉及到需要页面加载完成之后的话就用mounted。在created的时候,视图中的HTML并没有渲染出来,所以此时如果直接去操作HTML的dom节点,一定找不到相关的元素。而在mounted中,由于此时HTML已经渲染出来了,所以可以直接操作dom节点

vuex是什么?怎么使用?哪种功能场景使用它?

1.vuex是vue生态系统中的状态管理,用来管理vue中的所有组件状态。

2.使用

import Vue from vue;
import Vuex from vuex;
Vue.use(Vuex);
const store = new Vuex.store({
    state: {
        count: 0
    },
    getters: {
        addTen: state => {
            return state.count+10;
        }
    },
    mutations: {
        increment(state){
            state.count++
        }
    }
})
store.commit('increment');
console.log(this.$store.state.count);

vuex中有

①state状态,还有mapState映射状态

computed: mapState({
    count: state => state.count
})

getter相当于store的计算属性,主要用来过滤一些数据

getters: {
    addTen: state => {
        return state.count+10;
    }
}
store.getters.addTen

mapGetters是将store中的getter映射到局部计算属性中

computed: {
    ...mapGetters([        'addTen'    ])
}

③Mutation 改变vuex中store状态唯一方法就是提交mutation,可传入额外参数,是一个同步函数。 在组件中提交Mutation

import {mapMutations} from 'vuex'
export default{
    methods: {
        ...mapMutations([
            'increment'
        ]),
        ...mapMutaions({
            add: 'increment'
        })
    }
}

④Action 类似mutation,但是是异步的,view层通过store.dispath分发action

actions:{
    increment(context){
        context.commit('increment')
    }
}

⑤module 当应用比较复杂,可以将store分割成模块

3.常用的场景有:单页应用中,组件之间的状态,音乐播放、登录状态、加入购物车等等

谈谈你对vue的双向数据绑定原理的理解

vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.definePorperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指定(解析{{}}),最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化->视图更新;视图交互变化input->数据model变更的双向绑定效果

实现简单的双向绑定

<input type="text" id="inp" />
<div id="show"></div>
<script type="text/javascript">
var inp = document.getElementById('inp');
var show = document.getElementById('show');
var obj = {};
function watch(obj, key, callback){
    var val = obj[key];
    Object.defineProperty(obj, key, {
        get: function(){
            return val;
        },
        set: function(newVal){
            callback(newVal, this)
        }
    })
}
watch(obj, "input", function(val){
    show.innerHTML = val
})
inp.addEventListener('keyup', function(e){
    obj.input = e.target.value
})
</script>

另一种方式实现vue的响应式原理

Proxy在目标对象之前架设一层“拦截”,外界对该对象的访问都必须先通过这层拦截,因此提供一种机制,可以对外界的访问进行过滤和改写。

<input type="text" id="txt" />
<div id="show"></div>
<script type="text/javascript">
  var inp = document.getElementById('txt');
  var show = document.getElementById('show')
  var obj = {}
  var objKey = 'text'; // 将键保存起来
  // Object.defineProperty
  Object.defineProperty(obj, objKey, {
    get: function(){
      return obj[objKey];
    },
    set: function(newVal){
      show.innerHTML = newVal
    }
  })
  inp.addEventListener('keyup', function(e){
    obj[objKey] = e.target.value
  })
  
  // proxy的实现
  const newObj = new Proxy(obj, {
    get: function(target, key, receiver){
      return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value,receiver){
      if(key === objKey){
        show.innerHTML = value
      }
    }
  })
  inp.addEventListener('keyup',function(e){
    newObj[objKey] = e.target.value;
  })

Object.defineProperty的缺点:

1.不能检测到增加或删除的属性

2.数组方面的变动,如根据索引改变元素,以及直接改变数组长度时的变化,不能被检测到。

vue3.0里为什么要用Proxy API替代defineProperty API?

  • defineProperty API的弊端 无法直接监听属性的新增和删除;无法直接监听数组
  • Proxy的优势 Proxy相对于给一个对象外层加了一层拦截,这层拦截可以做很多操作,比如对数据信息的过滤、修改或收集数据信息,可以操作对象属性同时监听属性的新增和删除;可以监听数组的变化

vue3.0编译做了哪些优化?

类型检查

  • vue2.x选用Flow做类型检查,对于一些复杂场景类型的检查,支持得并不好。
  • vue3.x源码自身采用了TypeScript开发,非常有利于代码的维护,做类型检查 diff方法优化
  • vue2.x的虚拟dom是进行全量的对比
  • vue3.0新增了静态标记(PatchFlag):在于上次虚拟结点进行对比的时候,值对比带有patch flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内容化。 hoistStatic静态提升
  • vue2.x无论元素是否参与更新,每次都会重新创建
  • vue3.0对不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停地复用 数据劫持
  • vue2.x通过Object.defineProperty去劫持数据的getter和setter,并不能检测对象属性的添加和删除。
  • vue3.x使用Proxy API做数据劫持,对对象属性的增加和删除都能检测到 生成block tree
  • vue2.x的数据更新并触发重新渲染的粒度是组件级的,单个组件内部需要遍历该组件的整个vnode树
  • vue3.x做到了通过编译阶段对静态模板的分析,编译生成了Block tree。Block tree是一个将模板基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是规定的。每个区块只需要追踪自身包含的动态节点。 slot编译优化
  • vue2.x如果有一个组件传入了slot,那么每次父组件更新的时候,会强制子组件update,造成性能的浪费
  • vue3.0优化了slot的生成,使得非动态slot中属性的更新只会触发子组件的更新 cacheHandlers事件侦听器缓存 默认情况下onClick会被视为动态绑定,所以每次都会去追踪它的变化,但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用

聊聊你对Vue.js的template编译的理解

先转化成AST树,再得到render函数返回VNode(Vue的虚拟DOM节点)

详细步骤:首先通过compile编译器把template编译出AST语法树,然后AST会经过generate(将AST语法树转化成render function字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有标签名,子节点,文本等

说说vue react angularjs jquery的区别

  • jQuery和其他最大的区别:jQuery是事件驱动,其他是数据驱动
  • Angular,vue是双向绑定,而react不是