面试总结-vue篇

247 阅读18分钟

1,slot插槽

Slot 通俗的理解就是“占坑”,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),并且可以作为承载分发内容的出口,插槽内也可以包含任何模板代码,包括HTML。

//调用test组件
<test>
     Hello Word
</test>

//test组件
<a href="#">
    <slot></slot>
</a>

具名插槽:有时候我们一个组件里需要多个插槽,<slot>元素有一个特殊的特性:name ,这个特性可以用来定义额外的插槽

// 调用test组件
<test>
   <template v-slot:header>
    <h1>这里是头部</h1>
   </template>

  <p>没起名字的</p>

  <template v-slot:footer>
    <p>这里是尾部</p>
  </template>
</div>

// test组件
<div>
  <header>
    <slot name="header"></slot>
  </header>
  
  <main>
    <slot></slot>
  </main>
  
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

作用域插槽

上面已经说了,插槽跟模板其他地方一样都可以访问相同的实例属性(也就是相同的"作用域"),而不能访问<test>的作用域

那如果想访问<test>作用域该怎么办呢?
我们把需要传递的内容绑到 <slot> 上,然后在父组件中用v-slot设置一个值来定义我们提供插槽的名字:

//test.vue
<div>
	<!-- 设置默认值:{{user.lastName}}获取 Jun -->
	<!-- 如果home.vue中给这个插槽值的话,则不显示 Jun -->
	<!-- 设置一个 usertext 然后把user绑到设置的 usertext 上 -->
	<slot v-bind:usertext="user">{{user.lastName}}</slot>
</div>

//定义内容
data(){
  return{
	user:{
	  firstName:"Fan",
	  lastName:"Jun"
	}
  }
}

然后在home.vue中接收传过来的值:

//home.vue
<div>
  <test v-slot:default="slotProps">
    {{slotProps.usertext.firstName}}
  </test>
</div>

这样就可以获得test.vue组件传过来的值了

绑定在 <slot> 元素上的特性被称为插槽 prop。在父组件中,我们可以用 v-slot 设置一个值来定义我们提供的插槽 prop 的名字,然后直接使用就好了

独占默认插槽的缩写语法

在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上

这样写法还可以更简单,因为不带参数的v-slot就被假定为默认插槽,所以上面的代码还可以简化:

<div>
  <!-- 可以把 :default 去掉,仅限于默认插槽 -->
  <test v-slot="slotProps">
    {{slotProps.usertext.firstName}}
  </test>
</div>

注: 默认插槽 的缩写语法不能和 具名插槽 混用,因为它会导致作用域不明确

<div>
  <!-- 可以把 :default 去掉,仅限于默认插槽 -->
  <test v-slot="slotProps">
    {{slotProps.usertext.firstName}}
    <!-- 无效,会警告 -->
    <template v-slot:other="otherSlotProps">
      slotProps is NOT available here
    </template>
  </test>
</div>

只要出现多个插槽,始终要为所有的插槽使用完整的基于<template>的语法:

<test>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</test>

2,vue路由守卫,路由守卫的钩子函数,实现路由拦截

路由守卫分类

**【1】全局守卫:**是指路由实例上直接操作的钩子函数,特点是所有路由配置的组件都会触发,直白点就是触发路由就会触发这些钩子函数

  • beforeEach(to,from, next): 在路由跳转前触发,这个钩子作用主要是用于登录验证,也就是路由还没跳转提前告知,以免跳转了再通知就为时已晚。

    const router = new VueRouter({ ... })

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

  • beforeResolve(to,from, next): 这个钩子和beforeEach类似,也是路由跳转前触发,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,即在 beforeEach 和 组件内beforeRouteEnter 之后,afterEach之前调用。

  • afterEach(to,from): 和beforeEach相反,它是在路由跳转完成后触发,它发生在beforeEach和beforeResolve之后,beforeRouteEnter(组件内守卫)之前。这些钩子不会接受next函数也不会改变导航本身

    router.afterEach((to, from) => { // ... })

【2】路由守卫: 是指在单个路由配置的时候也可以设置的钩子函数

  • beforeEnter(to,from, next): 和beforeEach完全相同,如果两个都设置了,beforeEnter则在beforeEach之后紧随执行。在路由配置上直接定义beforeEnter守卫

    const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, beforeEnter: (to, from, next) => { // ... } } ] })

**【3】组件守卫:**是指在组件内执行的钩子函数,类似于组件内的生命周期,相当于为配置路由的组件添加的生命周期钩子函数。

  • beforeRouteEnter(to,from, next)**:**该钩子在全局守卫beforeEach和独享守卫beforeEnter之后,全局beforeResolve和全局afterEach之前调用,要注意的是该守卫内访问不到组件的实例,也就是this为undefined。因为它在组件生命周期beforeCreate阶段触发,此时的新组件还没有被创建。在这个钩子函数中,可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
  • beforeRouteUpdate(to,from, next)**:**在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例。
  • beforeRouteLeave(to,from, next)**:**导航离开该组件的对应路由时调用,可以访问组件实例this。这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过next( false )来取消。

参数的含义:to,from,next

to: Route,代表要进入的目标,它是一个路由对象。

from: Route,代表当前正要离开的路由,也是一个路由对象

next: Function,必须需要调用的方法,具体的执行效果则依赖next方法调用的参数

  • next():进入管道中的下一个钩子,如果全部的钩子执行完了,则导航的状态就是comfirmed(确认的)

  • next(false):终端当前的导航。如浏览器URL改变,那么URL会充值到from路由对应的地址。

  • next('/')||next({path:'/'}):跳转到一个不同的地址。当前导航终端,执行新的导航。

完整的导航解析流程

  1. 触发进入其它路由
  2. 调用要离开路由的组件守卫beforeRouteLeave
  3. 调用全局的前置守卫beforeEach
  4. 在重用的组件里调用 beforeRouteUpdate
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件
  7. 在将要进入的路由组件中调用beforeRouteEnter
  8. 调用全局的解析守卫beforeResolve
  9. 导航被确认
  10. 调用全局的后置钩子afterEach
  11. 触发 DOM 更新mounted
  12. 执行beforeRouteEnter守卫中传给 next的回调函数。

**路由拦截:**在router>index.js中配置beforeEach

//路由跳转之前
router.beforeEach((to, from, next) => {
  if (to.path !== '/login' && !localStorage.token) {
    return next('/login')
  }
   next()
})

3,生命周期

Vue 实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载DOM-渲染、更新-渲染、卸载等一系列的过程,我们称这是 Vue 的生命周期。

简述每个周期具体适合哪些场景?
  • beforeCreate:创建前,此阶段为实例初始化之后,this指向创建的实例,此时的数据观察事件机制都未形成,不能获得DOM节点。

    data,computed,watch,methods 上的方法和数据均不能访问。

    可以在这加个loading事件。

  • created:创建后,此阶段为实例已经创建,完成数据(data、props、computed)的初始化导入依赖项。

    可访问 data computed watch methods 上的方法和数据。

    初始化完成时的事件写在这里,异步请求也适宜在这里调用(请求不宜过多,避免白屏时间太长)。

    可以在这里结束loading事件,还做一些初始化,实现函数自执行。

    未挂载DOM,若在此阶段进行DOM操作一定要放在Vue.nextTick()的回调函数中。

  • beforeMount:挂载前,虽然得不到具体的DOM元素,但vue挂载的根节点已经创建,下面vue对DOM的操作将围绕这个根元素继续进行。

    beforeMount这个阶段是过渡性的,一般一个项目只能用到一两次。

  • mounted:挂载,完成创建vm.$el,和双向绑定

    完成挂载DOM和渲染,可在mounted钩子函数中对挂载的DOM进行操作。

    可在这发起后端请求,拿回数据,配合路由钩子做一些事情。

  • beforeUpdate:数据更新前,数据驱动DOM。

    在数据更新后虽然没有立即更新数据,但是DOM中的数据会改变,这是vue双向数据绑定的作用。

    可在更新前访问现有的DOM,如手动移出添加的事件监听器。

  • updated:数据更新后,完成虚拟DOM的重新渲染和打补丁。

    组件DOM已完成更新,可执行依赖的DOM操作。

    注意:不要在此函数中操作数据(修改属性),会陷入死循环。

  • activated:在使用vue-router时有时需要使用<keep-alive></keep-alive>来缓存组件状态,这个时候created钩子就不会被重复调用了。

    如果我们的子组件需要在每次加载的时候进行某些操作,可以使用activated钩子触发。

  • deactivated<keep-alive></keep-alive>组件被移除时使用。

  • beforeDestroy:销毁前,

    可做一些删除提示,如:您确定删除xx吗?

  • destroyed:销毁后,当前组件已被删除,销毁监听事件,组件、事件、子实例也被销毁。

    这时组件已经没有了,无法操作里面的任何东西了。

父子组件的生命周期
  • 执行顺序:
    • 父组件开始执行到beforeMount 然后开始子组件执行,最后是父组件mounted。
    • 如果有兄弟组件,父组件开始执行到beforeMount,然后兄弟组件依次执行到beforeMount,然后按照顺序执行mounted,最后执行父组件的mounted。
  • 当子组件挂载完成后,父组件才会挂载。
  • 当子组件完成挂在后,父组件会主动执行一次beforeUpdated/updated钩子函数(仅首次)
  • 父子组件在data变化中是分别监控的,但是更新props中的数据是关联的。
  • 销毁父组件时,先将子组件销毁后才会销毁父组件。
  • 兄弟组件的初始化(mounted之前)是分开进行,挂载是从上到下依次进行
  • 当没有数据关联时,兄弟组件之间的更新和销毁是互不关联的

4,计算属性与watch

计算属性computed

计算属性是自动监听依赖值的变化,从而动态返回内容,监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些事情。它有以下几个特点:

  • 数据可以进行逻辑处理,减少模板中计算逻辑。
  • 对计算属性中的数据进行监视
  • 依赖固定的数据类型(响应式数据)

计算属性由两部分组成:get和set,分别用来获取计算属性和设置计算属性。默认只有get,如果需要set,要自己添加。另外set设置属性,并不是直接修改计算属性,而是修改它的依赖。

计算属性computed vs 方法methods

两者最主要的区别:computed 是可以缓存的,methods 不能缓存;**只要相关依赖没有改变,多次访问计算属性得到的值是之前缓存的计算结果,不会多次执行。**网上有种说法就是方法可以传参,而计算属性不能,其实并不准确,计算属性可以通过闭包来实现传参:

侦听属性

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性watch。watch中可以执行任何逻辑,如函数节流,Ajax异步获取数据,甚至操作 DOM(不建议)。

使用 watch 的深度遍历和立即调用功能

使用 watch 来监听数据变化的时候除了常用到 handler 回调,其实其还有两个参数,便是:

  • deep 设置为 true 用于监听对象内部值的变化
  • immediate 设置为 true 将立即以表达式的当前值触发回调

相当于在created中调用方法

created(){
    this.fetchPostList()
},
watch: {
    searchInputValue(){
        this.fetchPostList()
    }
}

用immediate 方法优化上面的代码
watch: {
    searchInputValue:{
        handler: 'fetchPostList',
        immediate: true
    }
} 

ccomputed和watch****之间的区别:

  • watch:监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。
  • computed:监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算。

除此之外,有点很重要的区别是:计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性。watch也可以检测computed属性。

总结

计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

  • computed能做的,watch都能做,反之则不行
  • 能用computed的尽量用computed

5,vue.set

在我们使用vue进行开发的过程中,可能会遇到一种情况:当生成vue实例后,当再次给数据赋值时,有时候并不会自动更新到视图上去; 当我们去看vue文档的时候,会发现有这么一句话:如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。 如下代码,给 student对象新增 age 属性

data () {
  return {
    student: {
      name: '',
      sex: ''
    }
  }
}
mounted () { // ——钩子函数,实例挂载之后
  this.student.age = 24
}

受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。

正确写法:this.$set(this.data,”key”,value')

mounted () {
  this.$set(this.student,"age", 24)
}

6,vuex和vuex的模块化

Vuex 是一个专为 Vue.js 应用程序开发的状态管理插件。它采用集中式存储管理应用的所有组件的状态,

解决两个问题

  • 多个组件依赖于同一状态时,对于多层嵌套的组件的传参将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
  • 来自不同组件的行为需要变更同一状态。以往采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

Vuex的5个核心属性

分别是 state、getters、mutations、actions、modules 。

Vuex模块

因为使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。所以将 store 分割成模块(module)。每个模块拥有自己的 state、mutations、actions、getters,甚至是嵌套子模块,从上至下进行同样方式的分割。

在module文件新建moduleA.js和moduleB.js文件。在文件中写入

const state={
    //...
}
const getters={
    //...
}
const mutations={
    //...
}
const actions={
    //...
}
export default{
    state,
    getters,
    mutations,
    actions
}

然后再index.js引入模块

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import moduleA from './module/moduleA'
import moduleB from './module/moduleB'
const store = new Vuex.Store({
    modules:{
        moduleA,
        moduleB
    }
})
export default store

在模块中,getter和mutation和action中怎么访问全局的state和getter?

  • 在getter中可以通过第三个参数rootState访问到全局的state,可以通过第四个参数rootGetters访问到全局的getter。
  • 在mutation中不可以访问全局的satat和getter,只能访问到局部的state。
  • 在action中第一个参数context中的context.rootState访问到全局的state,context.rootGetters访问到全局的getter。

Vuex模块的命名空间。

默认情况下,模块内部的action、mutation和getter是注册在全局命名空间,如果多个模块中action、mutation的命名是一样的,那么提交mutation、action时,将会触发所有模块中命名相同的mutation、action。

这样有太多的耦合,如果要使你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true 的方式使其成为带命名空间的模块。

export default{
    namespaced: true,
    state,
    getters,
    mutations,
    actions
}

怎么在带命名空间的模块内提交全局的mutation和action?

将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。

this.$store.dispatch('actionA', null, { root: true })
this.$store.commit('mutationA', null, { root: true })

怎么在带命名空间的模块内注册全局的action?

actions: {
    actionA: {
        root: true,
        handler (context, data) { ... }
    }
  }

在v-model上怎么用Vuex中state的值?

需要通过computed计算属性来转换。

<input v-model="message">
// ...
computed: {
    message: {
        get () {
            return this.$store.state.message
        },
        set (value) {
            this.$store.commit('updateMessage', value)
        }
    }
}

Vuex的严格模式是什么,有什么作用,怎么开启?

在严格模式下,无论何时发生了状态变更且不是由 mutation函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

在Vuex.Store 构造器选项中开启,如下

const store = new Vuex.Store({
    strict:true,
})

7,vue组件通讯

prop/$emit

父组件通过prop的方式向子组件传递数据,而通过$emit子组件可以调用父组件的事件。

props:{
    list: {
      type: Array,
      default: () => {
        return [];
      }
    }
}

this.$emit("change", index);// 参数一:事件名, 参数二:事件接收的参数

.sync修饰符

有些情况下,我们希望在子组件能够“直接修改”父组件的prop值,但是双向绑定会带来维护上的问题;vue提供了一种解决方案,通过语法糖.sync修饰符。

  .sync修饰符在 vue1.x 的时候曾作为双向绑定功能存在,即子组件可以修改父组件中的值。但是它违反了单向数据流的设计理念,所以在 vue2.0 的时候被干掉了。但是在 vue2.3.0+ 以上版本又重新引入了。但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的v-on监听器。说白了就是让我们手动进行更新父组件中的值了,从而使数据改动来源更加的明显。

//Parent.vue    父组件
<template>
  <div>
    <Child :msg.sync="msg" :num.sync="num"></Child>
  </div>
</template>
<script>
import Child from "./child";
export default {
  name: "way2",
  components: {
    Child
  },
  data() {
    return {
      msg: "hello every guys",
      num: 0
    };
  }
};
</script>

//Child.vue    子组件
<template>
  <div>
    <div @click="clickRevert">点击更新字符串:{{ msg }}</div>
    <div>当前值:{{ num }}</div>
    <div @click="clickOpt('add')" class="opt">+</div>
    <div @click="clickOpt('sub')" class="opt">-</div>
  </div>
</template>
<script>
export default {
  props: {
    msg: {
      type: String,
      default: ""
    },
    num: {
      type: Number,
      default: 0
    }
  },
  methods: {
    clickRevert() {
      let { msg } = this;
      this.$emit("update:msg",msg.split("").reverse().join(""));
    },
    clickOpt(type = "") {
      let { num } = this;
      if (type == "add") {
        num++;
      } else {
        num--;
      }
      this.$emit("update:num", num);
    }
  }
};
</script>


attrs和listeners

当需要用到从A到C的跨级通信时,我们会发现prop传值非常麻烦,会有很多冗余繁琐的转发操作;如果C中的状态改变还需要传递给A,使用事件还需要一级一级的向上传递,代码可读性就更差了。

因此vue2.4+版本提供了新的方案:$attrs和$listeners,我们先来看一下官网对$attrs的描述:

//Parent.vue    父组件
<template>
  <div>
    <Child
      :notUse="'not-use'"
      :childMsg="childMsg"
      :grandChildMsg="grandChildMsg"
      @onChildMsg="onChildMsg"
      @onGrandChildMsg="onGrandChildMsg"
    ></Child>
  </div>
</template>
<script>
import Child from "./child";
export default {
  data() {
    return {
      childMsg: "hello child",
      grandChildMsg: "hello grand child"
    };
  },
  components: { Child },
  methods: {
    onChildMsg(msg) {
      this.childMsg = msg;
    },
    onGrandChildMsg(msg) {
      this.grandChildMsg = msg;
    }
  }
};
</script>

//child.vue    子组件
<template>
  <div class="box">
    <div @click="clickMsg">{{ childMsg }}</div>
    <div>$attrs: {{ $attrs }}</div>
    <GrandChild v-bind="$attrs" v-on="$listeners"></GrandChild>
  </div>
</template>
<script>
import GrandChild from "./grand-child";
export default {
  props: {
    childMsg: {
      type: String
    }
  },
  methods: {
    clickMsg() {
      let { childMsg } = this;
      this.$emit(
          "onChildMsg",
          childMsg.split("").reverse().join("")
      );
    }
  },
  components: { GrandChild }
};
</script>

//grand-child.vue    孙组件
<template>
  <div class="box1" @click="clickMsg">grand-child:{{ grandChildMsg }}</div>
</template>
<script>
export default {
  props: {
    grandChildMsg: {
      type: String
    }
  },
  methods: {
    clickMsg() {
      let { grandChildMsg } = this;
      this.$emit(
        "onGrandChildMsg",
        grandChildMsg.split("").reverse().join("")
      );
    }
  }
};
</script>

provide和inject

虽然$attrs和$listeners可以很方便的从父组件传值到孙组件,但是如果跨了三四级,并且想要的数据已经被上级组件取出来,这时$attrs就不能解决了。

  provide/inject是vue2.2+版本新增的属性,简单来说就是父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。这里inject注入的变量不像$attrs,只能向下一层;inject不论子组件嵌套有多深,都能获取到。

//Parent.vue    父组件
<template>
  <div>
    <Child></Child>
  </div>
</template>
<script>
import Child from "./child";
export default {
  components: { Child },
  data() {
    return {
      childmsg: "hello child",
      grandmsg: "hello grand child"
    };
  },
  provide() {
    return {
      childmsg: this.childmsg,
      grandmsg: this.grandmsg
    };
  },
  mounted() {
    setTimeout(() => {
      this.childmsg = "hello new child";
      this.grandmsg = "hello new grand child";
    }, 2000);
  },
};
</script>

//child.vue    子组件
<template>
  <div class="box">
    <div>child-msg:{{ childmsg }}</div>
    <div>grand-msg:{{ grandmsg }}</div>
    <GrandChild></GrandChild>
  </div>
</template>
<script>
import GrandChild from "./grand-child";
export default {
  inject: ["childmsg", "grandmsg"],
  components: { GrandChild },
};
</script>

//grand-child.vue    孙组件
<template>
  <div class="box">
    <div>child-msg:{{ childmsg }}</div>
    <div>grand-msg:{{ grandmsg }}</div>
  </div>
</template>
<script>
export default {
  name: "GrandChild",
  inject: ["childmsg", "grandmsg"],
};
</script>

EventBus

EventBus我刚开始直接翻译理解为事件车,但比较官方的翻译是事件总线。它的实质就是创建一个vue实例,通过一个空的vue实例作为桥梁实现vue组件间的通信。它是实现非父子组件通信的一种解决方案,所有的组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”。

//utils/event-bus.js
import Vue from "vue";
export default new Vue();

//main.js
import bus from "@/utils/event-bus";
Vue.prototype.$bus = bus;

// 将其挂载到全局,变成全局的事件总线,这样在组件中就能很方便的调用了。//Parent.vue    父组件
<template>
  <div class="box">
    <Child1></Child1>
    <Child2></Child2>
  </div>
</template>
<script>
import Child1 from "./child1";
import Child2 from "./child2";
export default {
  components: {
    Child1,
    Child2
  }
};
</script>// 我们先定义了两个子组件child1和child2,我们希望这两个组件能够直接给对方发送消息。//child1.vue    子组件1
<template>
  <div>
    <div class="send" @click="clickSend">发送消息</div>
    <template v-for="(item, index) in msgList">
      <div :key="index">{{ item }}</div>
    </template>
  </div>
</template>
<script>
export default {
  data() {
    return { msgList: [] };
  },
  mounted() {
    this.$bus.$on("getMsg1", res => {
      this.msgList.push(res);
    });
  },
  methods: {
    clickSend() {
      this.$bus.$emit("getMsg2", "hello from1:" + parseInt(Math.random() * 20));
    }
  }
};
</script>

//child2.vue    子组件2
<template>
  <div>
    <div class="send" @click="clickSend">发送消息</div>
    <template v-for="(item, index) in msgList">
      <div :key="index">{{ item }}</div>
    </template>
  </div>
</template>
<script>
export default {
  data() {
    return { msgList: [] };
  },
  mounted() {
    this.$bus.$on("getMsg2", res => {
      this.msgList.push(res);
    });
  },
  methods: {
    clickSend() {
      this.$bus.$emit("getMsg1", "hello from2:" + parseInt(Math.random() * 20));
    }
  }
};
</script>

vuex

在vue组件开发中,经常会遇到需要将当前组件的状态传递给其他非父子组件组件,或者一个状态需要共享给多个组件,这时采用上面的方式就会非常麻烦。vue提供了另一个库vuex来解决数据传递的问题;刚开始上手会感觉vuex非常的麻烦,很多概念也容易混淆,不过不用担心,本文不深入讲解vuex。

vuex实现了单向的数据流,在全局定义了一个State对象用来存储数据,当组件要修改State中的数据时,必须通过Mutation进行操作。

$refs

有时候我们需要在vue中直接来操作DOM元素,比如获取DIV的高度,或者直接调用子组件的一些函数;虽然原生的JS也能获取到,但是vue为我们提供了更方便的一个属性:$refs。如果在普通的DOM元素上使用,获取到的就是DOM元素;如果用在子组件上,获取的就是组件的实例对象。

//child.vue    子组件
<template>
  <div>初始化:{{ num }}</div>
</template>
<script>
export default {
  data() {
    return { num: 0 };
  },
  methods: {
    addNum() {
      this.num += 1;
    },
    subNum() {
      this.num -= 1;
    }
  }
};
</script>

// Parent.vue    父组件
<template>
  <div>
    <Child ref="child"></Child>
    <div class="opt" ref="opt_add" @click="clickAddBtn">+</div>
    <div class="opt" ref="opt_sub" @click="clickSubBtn">-</div>
    <div class="opt" ref="opt_show" @click="clickShowBtn">show</div>
  </div>
</template>
<script>
import Child from "./child";
export default {
  components: { Child },
  data() {
    return {};
  },
  methods: {
    clickAddBtn() {
      this.$refs.child.addNum();
    },
    clickSubBtn() {
      this.$refs.child.subNum();
    },
    clickShowBtn() {
      console.log(this.$refs.child);
      console.log(this.$refs.child.num);
    }
  }
};
</script>

parent和children

如果页面有多个相同的子组件需要操作的话,$refs一个一个操作起来比较繁琐,vue提供了另外的属性:$parent和$children来统一选择。

//child.vue    子组件
<template>
  <div>child</div>
</template>
<script>
export default {
  mounted() {
    console.log(this.$parent.show());
    console.log("Child", this.$children, this.$parent);
  }
};
</script>

//Parent.vue    父组件
<template>
  <div>
    parent
    <Child></Child>
    <Child></Child>
  </div>
</template>
<script>
import Child from "./child";
export default {
  components: { Child },
  mounted() {
    console.log("Parent", this.$children, this.$parent);
  },
  methods: {
    show() {
      return "to child data";
    }
  }
};
</script>

8,父子组件生命周期顺序

加载渲染过程

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

更新过程

父beforeUpdate->子beforeUpdate->子updated->父updated

销毁过程

 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

常用钩子简易版

父create->子created->子mounted->父mounted 

单一组件钩子执行顺序

activated, deactivated 是组件keep-alive时独有的钩子

  1. beforeCreate
  2. created
  3. beforeMount
  4. mounted
  5. beforeUpdate
  6. updated
  7. activated
  8. deactivated
  9. beforeDestroy
  10. destroyed
  11. errorCaptured

9,双向绑定原理

Vue 双向绑定,使用数据劫持和发布订阅模式实现的

vue2.0 采用的是Object.defineProperty进行数据劫持的

主要实现原理是使用描述对象中的set方法进行拦截,并发送订阅器信号

// ... 
let dep = new Dep()
return Object.defineProperty(obj, prop, {
    // ...
    get: function(key) {
        dep.target = this
        dep.addSub()
        // ...
    }
    set: function(newVal) {
        val = newVue;
        // 发送一个dep信号
        dep.notify()
        // ...
    }
})

而vue3.0中采用Proxy来实现数据劫持

let target = {}

let p = new Proxy(target, {
    set: function() {
        //...
    },
    get: function() {
        //...
    }
})

为什么用proxy呢?

我们知道 Object.defineProperty 是有局限性的,他的拦截的 target 就是单纯的对象的key的值

所以呢,对象属性的删减,数组,数组长度的改变,它就没法进行劫持了

而 ES6 的新特性,Proxy,它可以拦截对象,数组几乎一切对象包装类型

注意:  Proxy 没法兼容 IE,

从上图我们可以看到,Observer 观察了 object 值的变化,这是一种观察者模式

而 Observer 将观察的信号发布给订阅器这是一种 发布订阅模式

那么观察者模式与发布订阅模式有什么区别呢?

我们先谈观察者模式

什么是观察者模式,首先有一个观察者,一个被观察者,被观察者这里是数据,而观察者是Observer,被观察者发生变化时,主动发生信号给被观察者

按照这个思路来说,我们也能想象尤大,当时设计双向绑定时候,思考怎样去监听这个数据的变化,也就是如何使用观察者模式来实现,而恰好对一个对象的处理中有个对象方法我们可以使用,就是 Object.defineProperty

假如没有这个方法我们怎么实现呢?

这就是 angular 的另外一种实现方式脏检测,也就是不停的轮询数据的变化情况,显然脏检测对性能消耗比较大

再谈谈发布订阅模式

软件架构中,发布订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

这里很明显了,区别就在于,不同于观察者和被观察者,发布者和订阅者是互相不知道对方的存在的,发布者只需要把消息发送到订阅器里面,订阅者只管接受自己需要订阅的内容

由此发布订阅模式是一种松耦合的关系,watcher 和 Observer 之间是互相不受影响

10,懒加载

如何实现懒加载

我们知道vue的v-if可以决定是否要渲染该组件,所以我们利用v-if来控制懒加载的组件,如果这个组件不在可视区域,则将其设为false,或者让它渲染骨架,当组件出现在可视区域的时候再去渲染真实的dom。

如何判断组件出现在可视区域

判断组件是否出现在可视区域有两种手段:
一、是利用元素的getBoundingClientRect方法获取元素所处的位置,然后判断top属性是否大于0到window.innerHeight之间,并且需要监听页面的scroll事件,不断进行上述判断,想要了解更多的,请阅读我开头说的图片懒加载实现方式。
二、利用新特性IntersectionObserver,这个特性不需要我们去监听滚动事件,只要元素出现在视线,就会触发可见的事件

11,父组件如何调用子组件方法

ref, vuex, children等

12,混合mixins和继承extends

mixins:

值可以是一个混合对象数组,混合实例可以包含选项,将在extend将相同的选项合并 mixins代码:

  var mixin={
    data:{mixinData:'我是mixin的data'},
    created:function(){
      console.log('这是mixin的created');
    },
    methods:{
      getSum:function(){
        console.log('这是mixin的getSum里面的方法');
      }
    }
  }

  var mixinTwo={
    data:{mixinData:'我是mixinTwo的data'},
    created:function(){
      console.log('这是mixinTwo的created');
    },
    methods:{
      getSum:function(){
        console.log('这是mixinTwo的getSum里面的方法');
      }
    }
  } 

  var vm=new Vue({
    el:'#app',
    data:{mixinData:'我是vue实例的data'},
    created:function(){
      console.log('这是vue实例的created');
    },
    methods:{
      getSum:function(){
        console.log('这是vue实例里面getSum的方法');
      }
    },
    mixins:[mixin,mixinTwo]
  })
  
  //打印结果为:
  这是mixin的created
  这是mixinTwo的created
  这是vue实例的created
  这是vue实例里面getSum的方法

结论: 1.mixins执行的顺序为mixins>mixinTwo>created(vue实例的生命周期钩子); 2.选项中数据属性如data,methods,后面执行的回覆盖前面的,而生命周期钩子都会执行

extends:

extends用法和mixins很相似,只不过接收的参数是简单的选项对象或构造函数,所以extends只能单次扩展一个组件

13,vuex的辅助方法

mapState:

简单实现在store中存入一部分属性,然后部分组件去调用它。(单纯的取)
 //store/index.js
 state:{
        userName: 'phoebe',
        age: '23',
        habit: 'dance'
 }
        
  // app.vue
  
  // 1.简单获取
  this.$store.state.age
  this.$store.state.habit
  this.$store.state.userName
  
  //2.使用辅助函数 mapState
  import { mapState } from 'vuex'
  computed: {
      //this.habit :dance  即可调用
    ...mapState(['habit','age'.'userName'])
  },

mapMutations:

  // store/index.js
    mutations: {
        getUserName (state,value) {
            state.userName = value
        }
    },
    
    // app.vue
    
    // 1.简单commit
    this.$store.commit('getUserName','phoebe')
    
    // 2.使用辅助函数 mapMutations将组件中的 methods 映射为 store.commit 调用
    import {mapMutations} from 'vuex'
    methods: {
      ...mapMutations(['getUserName']), 
      init () {
        //this.$store.state.userName:'change phoebe to MM' 即可改变
        this.getUserName('change phoebe to MM')  
      }
    }

mapActions: 

 // store/index.js
   mutations: {
        getUserName (state,value) {
            state.userName = value
        }
    },
    // action就是为了提交mutation
    actions: {
        getUserName (context,value) {
            context.commit('getUserName',value)
        }
    }
    
    // app.vue
    
    //1. 简单分发
    this.$store.dispatch('getUserName', 'change name by action')
    
    //2.使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用
    import { mapActions} from 'vuex'
    methods: {
       ...mapActions(['getUserName']), 
    }
    init (){
      //this.$store.state.userName:'change phoebe to MA' 即可改变
      this.getUserName('change phoebe by MA') 
    }

mapGetters :

mapGetters 辅助函数仅仅是将 store 中的 getters 映射到局部计算属性,与state类似

import { mapGetters } from 'vuex'
 
export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getters 混入 computed 对象中
    ...mapGetters([
      'countDouble',
      'CountDoubleAndDouble',
      //..
    ])
  }
}


如果你想将一个 getter 属性另取一个名字,使用对象形式:
mapGetters({  // 映射 this.double 为 store.getters.countDouble    double: 'countDouble'})

14,nextTick 

nextTick的主要应用的场景及原因。

  • 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中

created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted()钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。

  • 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。

    <>script> new Vue({ el: '.app', data: { msg: 'Hello Vue.', msg1: '', msg2: '', msg3: '' }, methods: { changeMsg() { this.msg = "Hello world." this.msg1 = this.$refs.msgDiv.innerHTML this.$nextTick(() => { this.msg2 = this.$refs.msgDiv.innerHTML }) this.msg3 = this.$refs.msgDiv.innerHTML } } })

    // 结果就是只有 msg2 起作用了

具体原因在Vue的官方文档中详细解释:

Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.thenMessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0)代替。

例如,当你设置vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

15,组件中的data为什么是一个函数?

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

16.Vue2.x和Vue3.x渲染器的diff算法分别说一下

简单来说,diff算法有以下过程

  • 同级比较,再比较子节点
  • 先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较都有子节点的情况(核心diff)
  • 递归比较子节点

正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue3.x借鉴了 ivi算法和 inferno算法

在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。)

该算法中还运用了动态规划的思想求解最长递归子序列。

17.虚拟Dom以及key属性的作用

由于在浏览器中操作DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。这就是虚拟Dom的产生原因

Vue2的Virtual DOM借鉴了开源库snabbdom的实现。

Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点。是对真实DOM的一层抽象。(也就是源码中的VNode类,它定义在src/core/vdom/vnode.js中。)

VirtualDOM映射到真实DOM要经历VNode的create、diff、patch等阶段。

「key的作用是尽可能的复用 DOM 元素。」

新旧 children 中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的。

需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key也就是children中节点的唯一标识。

18.keep-alive

keep-alive可以实现组件缓存,当组件切换时不会对当前组件进行卸载。

常用的两个属性include/exclude,允许组件有条件的进行缓存。

两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。

keep-alive的中还运用了LRU(Least Recently Used)算法。

19.SSR

SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端

SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreatecreated两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求。