Vue基础

对着 Vue 官网一行一行扒下来,知识点总结包含大部分 Vue 的基础使用内容。

数据与方法

当一个 Vue 实例被创建时,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。当这些数据改变时,视图会进行重渲染。

需要注意:只有当实例被创建时就已经存在于 data 中的 property 才是响应式的。

也就是说如果你添加一个新的 property,那么后续对于该 property 所作的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个 property,但是一开始它为空或不存在,那么你仅需要设置一些初始值。

当使用 Object.freeze() 对属性进行冻结时,Vue 系统将不再追踪 property 的变化

为什么 data 需要是函数?

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

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

模板

模板这块没有特别需要说明的,只有一处需要注意一下:

有些 HTML 元素,诸如 <ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li><tr><option>,只能出现在其它某些特定的元素内部。

这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:

<table>
  <tr is="blog-post-row"></tr>
</table>

这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 is attribute 给了我们一个变通的办法:

<table>
  <tr is="blog-post-row"></tr>
</table>

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在

  • 字符串 (例如:template: '...')

  • 单文件组件 (.vue)

  • <script type="text/x-template">

生命周期

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

Vue 的全部生命周期钩子(非 SSR):

  • beforeCreate

    在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。

  • created

    在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。

  • beforeMount

    在挂载开始之前被调用:相关的 render 函数首次被调用。

  • mounted

    实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。

    注意: mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick

  • beforeUpdate

    数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。

  • updated

    由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。

    当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性watcher 取而代之。

    注意: updated 不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在 updated 里使用 vm.$nextTick

  • activated

    被 keep-alive 缓存的组件激活时调用。

  • deactivated

    被 keep-alive 缓存的组件停用时调用。

  • beforeDestroy

    实例销毁之前调用。在这一步,实例仍然完全可用。

  • destroyed

    实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

  • errorCaptured

    当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。

需要注意:如果使用 Vue 的服务端渲染框架比如:Nestjs,那么生命周期中只有:beforeCreate、created 和 errorCaptured 有效。因为服务端渲染只负责第一屏,渲染完成后,后续的 DOM 挂载、数据变更、销毁等,均由浏览器接管。

需要注意:所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。这是因为箭头函数绑定了父上下文,因此 this 与你期待的 Vue 实例不同,this.fetchTodos 的行为未定义。

下图是一个 Vue 实例的生命周期图谱:

关于Vue生命周期的总结

beforeCreate 钩子函数调用的时候,是获取不到 props 或者 data 中的数据的,因为这些数据的初始化都在 initState 中。

然后会执行 created 钩子函数,在这一步的时候已经可以访问到之前不能访问到的数据,但是这时候组件还没被挂载,所以是看不到的。

接下来会先执行 beforeMount 钩子函数,开始创建 VDOM,最后执行 mounted 钩子,并将 VDOM 渲染为真实 DOM 并且渲染数据。mounted 不会保证所有的子组件也都一起被挂载。

接下来是数据更新时会调用的钩子函数 beforeUpdateupdated,这两个钩子函数分别在数据更新前和更新后会调用。

另外还有 keep-alive 独有的生命周期,分别为 activateddeactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

最后就是销毁组件的钩子函数 beforeDestroydestroyed。前者适合移除事件、定时器等等,否则可能会引起内存泄露的问题。然后进行一系列的销毁操作。

计算属性和侦听器

计算属性的使用场景

当绑定在模板中的数据需要依赖其他属性值进行计算,或者需要经过一些复杂计算、特殊处理才能渲染到页面上的时候,我们可以使用计算属性:computed

计算属性的特性

计算属性可以让你的绑定值依赖其他值的变化来进行动态计算,并且会根据依赖值进行缓存,只有当依赖值变化才会返回新的计算内容。

计算属性和侦听器的区别

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

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

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

计算属性和侦听器还支持对象形式的写法:

// watch
vm.$watch('obj', {
    // 深度遍历
    deep: true,
    // 立即触发
    immediate: true,
    // 执行的函数
    handler: function(val, oldVal) {}
})

// computed
var vm = new Vue({
  data: { a: 1 },
  computed: {
    aPlus: {
      // this.aPlus 时触发
      get: function () {
        return this.a + 1
      },
      // this.aPlus = 1 时触发
      set: function (v) {
        this.a = v - 1
      }
    }
  }
});

设置侦听器 deep 为 true,则能开启对于对象属性的深度监听模式,也就是说,对象下的子属性发生变动,侦听器一样能够监测到变化并且触发。

计算属性和方法的区别

有的时候,计算属性和方法(methods)都可以达到相同的效果,那么它们之间的区别是什么呢?

计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要依赖值还没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。

而 相比之下,每当触发重新渲染时,调用方法将总会再次执行函数,这是有性能损耗的。

Class 与 Style 绑定

自动添加前缀

v-bind:style 使用需要添加浏览器引擎前缀的 CSS property 时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

条件渲染

v-show 与 v-if 区别

v-show 只是在 display: nonedisplay: block 之间切换。无论初始条件是什么都会被渲染出来,后面只需要切换 CSS,DOM 还是一直保留着的。所以总的来说 v-show 在初始渲染时有更高的开销,但是切换开销很小,更适合于频繁切换的场景。

v-if 的话就得说到 Vue 底层的编译了。当属性初始为 false 时,组件就不会被渲染,直到条件为 true,并且切换条件时会触发销毁/挂载组件,所以总的来说在切换时开销更高,更适合不经常切换的场景。

并且基于 v-if 的这种惰性渲染机制,可以在必要的时候才去渲染组件,减少整个页面的初始渲染开销。

key 管理可复用的元素

Vuekey 这个东西官网上说的官方解释有些抽象,推荐大家看:Vue2.0 v-for 中 :key 到底有什么用?

简单来说:

  • 如果这个元素没有加上 key,那么在 Vue 下一次更新视图的时候,它就会尽可能地将该元素进行复用,而不是重新创造一个新的元素将其替换。典型的场景是:表单登录,用户可以选择微信号或者手机号登录,当用户输入账号后忽然想切换登录方式,代码里虽然是使用的 v-if 进行了微信号和手机号两个 input 之间的切换,但是之前填写的登录账号信息依然会保留下来,这个就是 Vue 对 Input 标签进行了复用。

  • 如果这个元素上加了唯一的 key ,就等于说给这个元素及其子元素 / 子组件加上了一个唯一的 id 标识,那么 Vue 在下一次更新视图的时候,就能够识别出哪些元素是被修改了,哪些元素是被删除掉了,从而更有针对性地选择复用还是重建元素,而不是直接一股脑地能复用就复用。

官网解释:

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种 最大限度减少动态元素 并且 尽可能的尝试就地修改/复用相同类型元素 的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

最常见的用例是结合 v-for

<ul>
  <li v-for="item in items" :key="item.id">...</li>
</ul>

它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:

  • 完整地触发组件的生命周期钩子

  • 触发过渡

例如:

<transition>
  <span :key="text">{{ text }}</span>
</transition>

text 发生改变时,<span> 总是会被替换而不是被修改,因此会触发过渡。

列表渲染

这里的 v-for 指令就涉及到上面所说的 key 的问题。

假设有一个列表,列表有三个子组件,自组件没有加唯一 key 值,每个子组件里面有一个「有状态的」孙子组件。

现在用户点击删除按钮,删除掉第二项,会出现以下结果:Vue 识别自组件 2 的名称被修改为 3 但是其他的东西都会进行复用,包括自组件,然后删除原来的 3 组件及其自组件。这就出现了问题。

不要使用 index 索引作为组件的 key!因为,这样的话,当你删除了一项比如第二项,那么第三项组件的 key 就变成了 2 ,Vue 在进行 VNodes 比对的时候,还会认为是你只是将组件 2 的名称修改成了组件 3,然后还是会复用,于是还是会出现上面的问题。

数组的更新监测

Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()

  • pop()

  • shift()

  • unshift()

  • splice()

  • sort()

  • reverse()

由于 JavaScript 的限制,Vue 不能检测 数组和对象的变化。比如,如果你使用 arr.newItem = " "; 或者 obj.newKey = " "; 这种方式为数组 / 对象新增元素,这些新增的元素 Vue 是监听不到的。

不要将 v-forv-if 一同使用

永远不要把 v-ifv-for 同时用在同一个元素上。

当 Vue 处理指令时,v-forv-if 具有更高的优先级,所以这个模板:

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

将会经过如下运算:

this.users.map(function (user) {
  if (user.isActive) {
    return user.name
  }
})

因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。

通过将其更换为在如下的一个计算属性上遍历:

computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
      return user.isActive
    })
  }
}

一般我们在两种常见的情况下会倾向于这样做:

  • 为了过滤一个列表中的项目 (比如 v-for="user in users" v-if="user.isActive")。在这种情形下,请将 users 替换为一个计算属性 (比如 activeUsers),让其返回过滤后的列表。

  • 为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers")。这种情形下,请将 v-if 移动至容器元素上 (比如 ulol)。

所以,永远不要这么写:

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
<ul>
  <li
    v-for="user in users"
    v-if="shouldShowUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

你可以这么写:

<ul>
<!-- activeUsers 是计算属性过滤后的有效用户列表 -->
  <li
    v-for="user in activeUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
<ul v-if="shouldShowUsers">
  <li
    v-for="user in users"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

事件

事件处理这块可能是 Vuer 用的最多的东西之一,大部分内容大家可能也了解,就没什么好说的了,就大概总结一下吧。

事件的写法

在 Vue 中有下面几种事件的写法:

  • 内联写法:

    <div id="example-1">
      <button v-on:click="counter += 1">Add 1</button>
      <p>The button above has been clicked {{ counter }} times.</p>
    </div>
  • 方法名写法,这种写法,默认会传入一个 DOM 的原生事件对象作为事件处理函数的第一个参数

    <div id="example-2">
      <!-- `greet` 是在下面定义的方法名 -->
      <button v-on:click="greet">Greet</button>
    </div>
      // 在 `methods` 对象中定义方法
      methods: {
        greet: function (event) {
          // `this` 在方法里指向当前 Vue 实例
          alert('Hello ' + this.name + '!')
          // `event` 是原生 DOM 事件
          if (event) {
            alert(event.target.tagName)
          }
        }
      }
  • 内联语句中直接调用:

    <div id="example-3">
      <button v-on:click="say('hi')">Say hi</button>
      <button v-on:click="say('what')">Say what</button>
    </div>

    有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event 把它传入方法:

    <button v-on:click="warn('Form cannot be submitted yet.', $event)">
      Submit
    </button>

注意:不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件:

this.$emit('myEvent');

则监听这个名字的 kebab-case 版本是不会有任何效果的:

<!-- 没有效果 -->
<my-component v-on:my-event="doSomething"></my-component>

v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。

因此,推荐 始终使用 kebab-case 的事件名

事件修饰符

这块不是面试的重点,大家大概看一下文档了解就好:事件处理——事件修饰符。只有一种需要特殊说明一下:.sync 修饰符

.sync 修饰符也是语法糖的一种,在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。这个时候,我们可以使用 .sync 语法糖来模拟双向绑定,并且 .sync 也能够在语意上让子组件更改父组件的状态代码更容易被区分。

<comp :foo.sync="bar"></comp>

等价于:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:

this.$emit('update:foo', newValue);

.sync 修饰符经常会用在弹框的显隐功能上:

<!-- 父组件中 -->
<Modal :show.sync="shoModal" />
<!-- 子组件中 -->
<div class="mask" v-show="show">
    <span class="close" @click="close">X</span>
</div>
// 子组件 vue 文件
{
    props: ['show'],
    methods: {
        close() {
            this.$emit('update:show', false);
        }
    }
}

双向绑定 v-model

v-model 的本质——语法糖

你可以用 v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定。其实,v-model 本质上只是 v-bind:value v-on:input 的语法糖而已:

使用 v-model

<input v-model="val" />

等价于:

<input :value="val" @input="evt => val = evt.target.value" />

v-model 语法糖在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value property 和 input 事件;

  • checkbox 和 radio 使用 checked property 和 change 事件;

  • select 字段将 value 作为 prop 并将 change 作为事件。

v-model 修饰符

这块内容面试不太可能问到,不过作为平时开发的小技巧可以学习一下。

.lazy

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy 修饰符,从而转为在 change 事件_之后_进行同步:

<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">

.number

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:

<input v-model.number="age" type="number">

这通常很有用,因为即使在 type="number" 时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 parseFloat() 解析,则会返回原始的值。

.trim

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:

<input v-model.trim="msg">

自定义组件使用 v-model

自定义组件使用 v-model 的方式,和原生 DOM 上绑定的 v-model 方式一样,只需要组件内部对外暴露 value prop ,并且组件内部向外发射 input 事件即可。

注意:一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。

<custom-input v-model="searchText"></custom-input>

等价于

<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>

组件内部

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})

自定义组件

组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 datacomputedwatchmethods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。

全局注册

直接使用 Vue.component 进行全局注册,然后再创建 Vue 的根实例,需要注意的是:全局注册必须在根 Vue 实例创建之前。

Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })

局部注册

局部注册没有什么好说的,大家基本都明白。

单个根元素

每个组件必须只有唯一一个根元素

Props

静态 Props 和动态 Props

静态 props

<my-component title="This is title" />

动态 props

<my-component v-bind:title="title" />

多个 Props 的传入方式

我们可以对一个组件传入多个 props,如果组件的 props 太多,可以将 props 组合成一个对象 prop 传入。

<my-component v-bind:title="title" v-bind:name="name" v-bind:age="age" />

<!-- 统一放入一个 prop 对象中传入 -->

<my-component v-bind:obj="{title: 'title', name: 'name', age: 'age'}" />

也可以传入一个对象的所有 property:

如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name)。例如,对于一个给定的对象 post

post: {
  id: 1,
  title: 'My Journey with Vue'
}

下面的模板:

<blog-post v-bind="post"></blog-post>

等价于:

<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>

Props 的接收方式

组件内部接收 props 时我们可以写成数组形式:

props: ['title', 'likes', 'isPublished', 'commentIds', 'author']

也可以写成对象形式,如果写成对象形式的话,就需要 Props 的类型判断:

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

props 的类型检查

type 可以是下列原生构造函数中的一个:

  • String

  • Number

  • Boolean

  • Array

  • Object

  • Date

  • Function

  • Symbol

Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

额外的,type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。例如,给定下列现成的构造函数:

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

你可以使用:

Vue.component('blog-post', {
  props: {
    author: Person
  }
})

来验证 author prop 的值是否是通过 new Person 创建的。

Props 和 html-attribute

注意:Vue 的组件中是不限制 prop 的传入数量的,如果外部传入的 prop ,组件内部没有接收,那么就会变成 kebab-case (短横线分隔命名) 命名 的方式作为自定义属性添加到组件的根元素上。 Vue 会选择性地对重复的 attribute 进行替换或者合并,比如,如果是重复的 input type ,那么则会替换,如果是 class、style 等,则会合并。对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值, classstyle attribute 会稍微智能一些,即两边的值会被合并起来,从而得到最终的值。

如果你希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false。例如:

Vue.component('my-component', {
  inheritAttrs: false,
  // ...
})

注意:inheritAttrs: false 选项 不会 影响 styleclass 的绑定。

vm.$attrs

inheritAttrs: false 的情况很适合配合 vm.$attrs 来使用。vm.$attrs 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件,这在创建高级别的组件时非常有用。有了 inheritAttrs: false$attrs,你就可以手动决定这些 attribute 会被赋予哪个元素。这个类似于 React 中的 <MyComponent {...this.props} />

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
})

组件通讯

组件通信一般分为以下几种情况:

  • 父子组件通信

  • 兄弟组件通信

  • 跨多层级组件通信

  • 任意组件

对于以上每种情况都有多种方式去实现,接下来就来学习下如何实现。

父子通信

props 和 $emit

父组件通过 props 传递数据给子组件,子组件通过 emit 发送事件传递数据给父组件,这两种方式是最常用的父子通信实现办法。

这种父子通信方式也就是典型的单向数据流,父组件通过 props 传递数据,子组件不能直接修改 props, 而是必须通过发送事件的方式告知父组件修改数据。

v-model 和 .sync

另外这两种方式还可以使用语法糖 v-model 来直接实现,因为 v-model 默认会解析成名为 valueprop 和名为 input 的事件。这种语法糖的方式是典型的双向绑定,常用于 UI 控件上,但是究其根本,还是通过事件的方法让父组件修改数据。

.sync 属性是个语法糖,可以很简单的实现子组件与父组件通信

<!--父组件中-->
<input :value.sync="value" />
<!--以上写法等同于-->
<input :value="value" @update:value="v => value = v"></comp>
<!--子组件中-->
<script>
  this.$emit('update:value', 1)
</script>

$parent 和 $children 和 $ref

当然我们还可以通过访问 $parent 或者 $children 对象来访问组件实例中的方法和数据,或者使用 this.$refs[compnentRefName] 的方式来拿到具名子元素实例。

另外如果你使用 Vue 2.3 及以上版本的话还可以使用 $listeners.sync 这两个属性。

注意:$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

$listeners

$listeners 属性会将父组件中的 (不含 .native 修饰器的) v-on 事件监听器传递给子组件,子组件可以通过访问 $listeners 来自定义监听器。

vm.$listener

简单点讲它是一个对象,里面包含了作用在这个组件上所有的监听器(监听事件),可以通过 v-on="$listeners" 将事件监听指向这个组件内的子元素(包括内部的子组件)。

  <div id="app">
    <child1
      :p-child1="child1"
      :p-child2="child2"
      :p-child-attrs="1231"
      v-on:test1="onTest1"
      v-on:test2="onTest2">
    </child1>
  </div>
   <script>
      Vue.component("Child1", {
        inheritAttrs: true,
        props: ["pChild1"],
        template: `
        <div class="child-1">
        <p>in child1:</p>
        <p>props: {{pChild1}}</p>
        <p>$attrs: {{this.$attrs}}</p>
        <hr>
        <child2 v-bind="$attrs" v-on="$listeners"></child2></div>`,
        mounted: function() {
          this.$emit("test1");
        }
      });
      Vue.component("Child2", {
        inheritAttrs: true,
        props: ["pChild2"],
        template: `
        <div class="child-2">
        <p>in child->child2:</p>
        <p>props: {{pChild2}}</p>
        <p>$attrs: {{this.$attrs}}</p>
          <button @click="$emit('test2','按钮点击')">触发事件</button>
        <hr> </div>`,
        mounted: function() {
          this.$emit("test2");
        }
      });
      const app = new Vue({
        el: "#app",
        data: {
          child1: "pChild1的值",
          child2: "pChild2的值"
        },
        methods: {
          onTest1() {
            console.log("test1 running...");
          },
         onTest2(value) {
            console.log("test2 running..." + value);
          }
        }
      });
    </script>

上例中,父组件在child1组件中设置两个监听事件test1test2,分别在child1组件和child1组件内部的child2组件中执行。还设置了三个属性p-child1p-child2p-child-attrs。其中p-child1p-child2被对应的组件的prop识别。所以: p-child1组件中$attrs{ "p-child2": "pChild2的值", "p-child-attrs": 1231 }; p-child2组件中$attrs{ "p-child-attrs": 1231 },效果如下图:

我们再点击child2组件中的按钮,触发事件,控制台可以打印出:

兄弟组件通信

对于这种情况可以通过查找父组件中的子组件实现,也就是 this.$parent.$children,在 $children 中可以通过组件 name 查询到需要的组件实例,然后进行通信。

跨多层次组件通信

provide / inject

对于这种情况可以使用 Vue 2.2 新增的 API provide / inject,虽然文档中不推荐直接使用在业务中,但是如果用得好的话还是很有用的。

provide 选项允许我们指定我们想要 提供 给后代组件的数据 / 方法,然后在后代组件中使用 inject 注入,就可以访问组件组件提供的 provide 出来的数据或者调用 provide 出来的方法。 假设有父组件 A,然后有一个跨多层级的子组件 B:

// 父组件 A
export default {
  provide: {
    data: 1,
    getMap: this.getMap // getMap 是个方法
  }
}
// 子组件 B
export default {
  inject: ['data', 'getMap'],
  mounted() {
    // 无论跨几层都能获得祖先组件的 data 属性
    console.log(this.data) // => 1
    // 无论跨几层都能调用祖先组件的 getMap 方法
    this.getMap() // => 1
  }
}

任意组件

这种方式可以通过 Vuex 或者 Event Bus 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况

Event Bus

Event Bus 的通信机制其实非常简单:独立与主 Vue 实例再创建一个单独的 Vue 实例,利用 vm.$on vm.$off vm.$once 等事件监听 / 解绑的 api ,专门就做事件处理的事情。因为他式独立的 Vue 实例,所以并不需要知道谁监听了事件,谁触发了事件。非常的方便。

动态组件

有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里。上述内容可以通过 Vue 的 <component> 元素加一个特殊的 is attribute 来实现,也就是动态组件:

<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>

在上述示例中,currentTabComponent 可以包括

  • 已注册组件的名字,或

  • 一个组件的选项对象

动态组件也可以使用 keep-alive 进行缓存

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块,这个就是异步组件加载。

异步组件的全局注册

<template>
<div>
    <!-- 异步组件测试
    点击按钮后
    第一个延迟300毫秒,从服务器加载
    第二个不延迟从服务器加载 -->
    <template v-if="show">
        <later></later>
        <later2></later2>
    </template>
    <button @click="toggle">加载</button>
</div>
</template>
<script>
import Vue from 'vue';
const later = Vue.component('later', function (resolve) {
    setTimeout(function () {
        require(['./later.vue'], resolve)
    }, 3000);
});
const later2 = Vue.component('later2', function (resolve) {
    require(['./later2.vue'], resolve)
});
export default{
    data: function () {
        return {
            show: false
        };
    },
    components: {
        later,
        later2,
    },
    created: function () {

    },
    mounted: function () {
    },
    computed: {},
    methods: {
        toggle:function () {
            this.show = !this.show;
        }
    },
}
</script>
<style>
</style>

异步组件的局部注册

异步组建的局部注册唯一的区别就是 components 的写法

components: {
   AddCustomerSchedule(resolve) {
     require(["../components/AddCustomer"], resolve);
   },
   AddPeopleSchedule(resolve) {
     require(["../components/AddPeople"], resolve);
   },
   CustomerInfoSchedule(resolve) {
     require(["../components/CustomerInfo"], resolve);
   },
   VisitRecordSchedule(resolve) {
     require(["../components/VisitRecord"], resolve);
   },
},

递归组件

有的时候,本来想秀下操作,然而由于 水平菜鸡 业务复杂,稍有不慎我们就很容易写出死循环的递归组件:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 falsev-if)。

组件的循环引用

组件间的循环引入的场景之一就是—— tree 组件。导致死循环引用的原因,其实是因为两个 vue 文件中的循环 import,webpack 打包这种文件的时候,就会不知所措,于是只能给你抛出一个错误。所以这种场景往往出现在局部注册的组件下,而 Vue.component() 进行的全局组件注册由于不存在循环引用,所以没有这个问题。

对于这个问题的解决方案有两种:

  1. 你可以在 beforeCreate 钩子函数里进行引入:

    beforeCreate: function () {
      this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
    }
  2. 你可以在注册局部组件的时候,使用动态组件的注册方式:

    components: {
      TreeFolderContents: () => import('./tree-folder-contents.vue')
    }

两种解决问题的核心思想,都是延迟引入,从而规避掉 webpack 打包时可能出现的懵逼情况。

keep-alive

keep-alive 多出来两个钩子函数 activateddeactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

对于这个知识点,一般也就问问它是做什么用的,也不太会问别的东西,毕竟没有什么值得深挖的。只要能答上来这个是做组件缓存的基本就没问题。想要详细了解的,这里给个传送门吧:keep-alive

分发插槽

当我们需要在自定义组件中再嵌套自定义组件,就会用到 “分发插槽” 的功能,这个就类似于 React 中的语法 <MyComponent>{...this.children}</MyComponent>

默认插槽

Vue 组件支持一个默认默认插槽,默认插槽有得话只能有一个:

<!--父组件-->
<my-comp>
    Hello world!
</my-comp>

<!--MyComp 组件-->
<div>
    <slot><slot>
</div>

<!--最终渲染结果-->
<div>
    Hello world!
</div>

你也可以为插槽提供一个后备用内容,当父组件没有任何东西插入的时候,后备内容就会默认显示,像这样:

<!--MyComp 组件-->
<div>
    <slot>Hi World!<slot>
</div>

具名插槽

具名插槽可以有很多,不过名称不能重复,具名插槽和默认插槽可以同时存在:

<!--父组件-->
<my-comp>
    <template v-slot:header>This is header</template>
    <template v-slot:body>This is body</template>
    Hello World!
</my-comp>

<!--MyComp 组件-->
<div>
    <slot name="header"><slot>
    <slot name="body"><slot>
    <slot><slot>
</div>

<!--最终渲染结果-->
<div>
    This is header
    This is body
    Hello world!
</div>

作用域插槽

当使用插槽的时候,父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。也就是说,子元素没有办法访问到父模板中的内容,那么这个时候可以使用作用域插槽来将父模板里的内容传入:

<!--父组件-->
<my-comp>
    <template v-slot:default="slotProps">
        {{ slotProps.user.firstName }}
    </template>
</my-comp>

<!--MyComp 组件-->
<div>
    <slot v-bind:user="user">
        {{ user.lastName }}
    <slot>
</div>

如果是默认插槽的 prop 可以使用简写:

<!--父组件-->
<current-user>
    <template v-slot="slotProps">
        {{ slotProps.user.firstName }}
    </template>
</current-user>

<!--MyComp 组件-->
<div>
    <slot v-bind:user="user">
        {{ user.lastName }}
    <slot>
</div>

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

<!--父组件-->
<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</current-user>

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

<!--父组件-->
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>

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

可以以为结构的方式写插槽

<!--父组件-->
<current-user>
  <template v-slot:default="{user}">
    {{ .user.firstName }}
  </template>
</current-user>

动态插槽

动态指令参数也可以用在 v-slot 上,来定义动态的插槽名

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

具名插槽缩写

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

加入作用域插槽

<current-user #default="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

混入

我们写一些相似度很高的组件时,经常会发现很多能够提取出来公共使用的东西部分,Vue 对于这部分内容,提供了一个混入的工具:mixin。mixin 对象,其实也就是普通的 vue 对象而已。

全局混入 Mixin

Vue.mixin({
    beforeCreate() {
        // ...逻辑
        // 这种方式会影响到每个组件的 beforeCreate 钩子函数
    }
})

局部混入 Mixins

const mixinObj = {
    methods: {
        mixinFunc() {}
    }
}

new Vue({
    mixins: [mixinObj],
    created() {
    this.mixinFunc();
    }
});

使用 mixin 混入时:

  • mixin 的钩子函数会再自己组件的钩子函数之前触发

  • 对于 methods、computed 这些配置项等,Vue 会进行合并

  • 对于同名属性,Vue 会用组件里的来覆盖 mixin 的

自定义混入方式

这个我从来没有用过,也没有被问到过,感觉不重要也不常用,需要的话自取文档:自定义选项合并策略

混入和 extend

Vue 里还有一个 extend 的 api,那么这个和 mixin 有什么区别呢?个人感觉这两个东西的目的都是一样的:都是作为混入和扩展的一种方法,只不过实现层面不同:mixin 是在组件层面混入,或者可以理解为对实例进行扩展;而 extend 是先扩展了构造函数也就是累类,然后根据这个扩展完成的类,在再进行实例化。看代码:

// 创建组件构造器
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')

自定义指令

这块内容不多,但是面试的时候可能会被提一句:你有没有写过自定义指令?所以我建议大家直接看官网:自定义指令,官网上介绍的很详细,关于自定义指令内容大部分是使用方式、传参细节等,没有太多的坑在里面。

这里提一下我认为有可能被深入问到的问题:

你觉得指令和组件有什么区别?它们各自的应用场景是什么?

指令和组件主要的区别大概有两点:

  1. 业务的复杂程度:

    复杂的业务不用说,肯定没法单纯用指令就能摆平,让开发者只用指令写一个弹出框出来,肯定也不现实。所以复杂的业务必须组件,可以用自定义指令来进行辅助。并且,从配置项上也能看出来,指令是包含在组件里面的。

  2. DOM 操作的频繁程度:

    Vue 是数据驱动视图,本身是不提倡大量操作 DOM 的。然而实际开发场景中,我们有很多情况需要必须操作 DOM 。这个时候,就可以使用自定义指令来进行 DOM 操作,看上去也比在业务中直接用 jquery 获取来的优雅和语义化。常见的场景之一就是 affix 图钉——当用户界面滚动下来的时候,重要信息能有个吸顶的功能。

渲染函数和 JSX

从这块内容开始,说实话,Vue 的画风逐渐变态,已经开始在我这种白嫖党最不喜欢的 “底层” 、“原理” 这种关键词之间疯狂试谈。

里面的内容比较多,这里就不写了,我自觉不可能比官网写的好,大家还是把这块的官网内容看一下:渲染函数 & JSX。如果不太愿意看的朋友,就看我下面的总结大概:

  1. 首先呢,我们现在开发 vue 的基本操作就是 .vue 文件了,我们在里面写的很爽,但是在最终 Vue 的 compile 编译阶段,会将我们的组件编译成如下的渲染函数的形式:

    <div>
        先写一些文字
        <h1>一则头条</h1>
        <my-component v-bind:someProp="foobar" />
    </div>

    转换为:

    
    new Vue({
        render: function (createElement, context) {
            return createElement(
            'div', 
            // 一个与模板中 attribute 对应的数据对象。可选。
            {
                
            },
            // 子元素
            [
                '先写一些文字',
                createElement('h1', '一则头条'),
                createElement(MyComponent, {
                  props: {
                    someProp: 'foobar'
                  }
                })
            ]);
        }
    });

    是不是看不懂?是不是看不懂?看不懂就对了,因为这个本来就不是给人看的,是编译成这种格式给机器运行的。

  2. 拿到了编译完成的渲染函数并且执行后,vue 就能得到一个叫做 VNode虚拟 DOM,这个虚拟 DOM其实就是 用普通 js 对象的格式,对于即将渲染成的 DOM 文档格式的一种抽象描述。类似于下面这样:

    {
      'class': {
        foo: true,
        bar: false
      },
      style: {
        color: 'red',
        fontSize: '14px'
      },
      // 普通的 HTML attribute
      attrs: {
        id: 'foo'
      },
      // 组件 prop
      props: {
        myProp: 'bar'
      },
      // DOM property
      domProps: {
        innerHTML: 'baz'
      },
      on: {
        click: this.clickHandler
      },
      nativeOn: {
        click: this.nativeClickHandler
      },
      children: [...]
    }
  3. 有了这个 VNode Tree 的虚拟文档描述,Vue 就可以 将虚拟 DOM 渲染成真实的 DOM 节点,然后一次插入。

至于为什么会有这个东西呢?

  1. 首先,我们写的很爽的 template 这种模板语言格式,浏览器是不认识的,更不会执行,这个只是一种对于开发者更友好的语法糖,将真正的底层全部封装,简化最大程度简化写法,让程序员聚焦业务。而 render 函数本质上是浏览器能够识别并执行的 javascript ,浏览器拿到就很开心。

  2. 写过 React 的朋友就知道,Vue 和 React 的差别之一,就是在于: Vue 里面没办法大量地编写 javascript ,而 React 却可以。Vue 的人性化语法极大地迎合了开发者,但是这也造成了自由度没有 React 高,毕竟 React 全部 js,就连 jsx 其实也是普通的 string 。那这种情况下,在某些 SB需求 更为复杂、需要高度自由的场景下,语法糖就不适用了,比如说 table 组件。那怎么兼得鱼和熊掌?vue 的 render 函数就提供了一个解决方案,可能看上去没有那么好看,也不好理解,但是它确实能解决你的需求。

至于最后的 JSX

这也不是 Vue 官方推荐的东西,只不过考虑到这样的语法写起来确实难受,官方提供了一个可供选择的方案而已。

插件

内容不多,推荐看下官网:插件

  1. Vue.use(plugin) 注册插件

  2. 插件需要暴露一个 install 方法,接收 Vue 构造器和 options 选项作为参数:

    Plugin.install = function (Vue, options) {}
  3. 插件里面可以扩展 Vue 原型、扩展全局指令、扩展全局 mixin 等。

过滤器

内容不多,推荐看下官网:过滤器

  1. 过滤器的挂载:

    new Vue({
        filters: {
            filter1 (value) {
                return value + '';
            }
        }
    });

  2. 过滤器的使用:

    <div>{{ message | filter1 }}</div>
    <!--或者可以写成函数调用的形式-->
    <div>{{ message | filter1(arg1, arg2) }}</div>
    new Vue({
        filters: {
            filter1 (value, arg1, arg2) {
                return value;
            }
        }
    });
  3. 过滤器的链式使用:

    <!--filter1 return 去的内容,作为参数继续传入 filter2-->
    <div>{{ message | filter1 | filter2 }}</div>

参考文档

最后更新于