那些年被我们忽略的 vue 语法

2018/10/09

使用 vue 这么长时间了,vue 所有的语法你都用过了吗?如果感觉自己还没有完全了解 vue,这篇文章是你不二选择,当然,你如果想要了解基础部分,出门左转不谢

说正事之前,先上一个小插曲,我们都知道 a 链接的 href 如果是 #+id,那么点击链接会直接将包含该 id 的元素滚动到页面顶部,但我们页面一般都有 header,会遮挡一部分该元素,ElementUI 巧妙的运用伪类解决了此问题

.content h2:before,
.content h3:before {
  content: '';
  display: block;
  margin-top: -91px;
  height: 91px;
  visibility: hidden;
}

想一探究竟的请移步官网 F12 查看

好,正片开始!!!

Class 与 Style 绑定 — Vue.js

多重值

从 2.3.0 起你可以为  style  绑定中的属性提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染  display: flex

条件渲染 — Vue.js

template 元素上使用  v-if  条件渲染分组

具体例子请点击上面标题链接查看

因为  v-if  是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个  <template>  元素当做不可见的包裹元素,并在上面使用  v-if。最终的渲染结果将不包含  <template>  元素。

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-for on a template

类似于 v-if,你也可以利用带有 v-for  的 <template>  渲染多个元素。比如:

<ul>
  <template v-for="item in items">
    <li></li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

用  key  管理可复用的元素

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做除了使 Vue 变得非常快之外,还有其它一些好处。例如,如果你允许用户在不同的登录方式之间切换:

<template v-if="loginType === 'username'">
  <label>Username</label> <input placeholder="Enter your username" />
</template>
<template v-else>
  <label>Email</label> <input placeholder="Enter your email address" />
</template>

那么在上面的代码中切换  loginType  将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input>  不会被替换掉——仅仅是替换了它的  placeholder

这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的  key  属性即可:

<template v-if="loginType === 'username'">
  <label>Username</label> <input placeholder="Enter your username" key="username-input" />
</template>
<template v-else>
  <label>Email</label> <input placeholder="Enter your email address" key="email-input" />
</template>

现在,每次切换时,输入框都将被重新渲染。

本人曾经遇到过一个类似的问题,不过是表格的,感兴趣的可以看看

官网对 key 的解释

v-show

另一个用于根据条件展示元素的选项是  v-show  指令。用法大致一样:

<h1 v-show="ok">Hello!</h1>

不同的是带有  v-show  的元素始终会被渲染并保留在 DOM 中。v-show  只是简单地切换元素的 CSS 属性  display

注意,v-show  不支持  <template>  元素,也不支持  v-else

v-if vs v-show

v-if  是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建

v-if  也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show  就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,v-if  有更高的切换开销,而  v-show  有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用  v-show  较好;如果在运行时条件很少改变,则使用  v-if  较好。

列表渲染 — Vue.js

用  v-for  把一个数组对应为一组元素

我们用  v-for  指令根据一组数组的选项列表进行渲染。v-for  指令需要使用  item in items  形式的特殊语法,items  是源数据数组并且  item  是数组元素迭代的别名。

你也可以用  of  替代  in  作为分隔符,因为它是最接近 JavaScript 迭代器的语法:

<div v-for="item of items"></div>

一个对象的  v-for

你也可以用  v-for  通过一个对象的属性来迭代。

<div v-for="(value, key, index) in object">. : </div>

在遍历对象时,是按  Object.keys()  的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下是一致的。

数组更改检测注意事项

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个例子:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和  vm.items[indexOfItem] = newValue  相同的效果,同时也将触发状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)

// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

你也可以使用  vm.$set  实例方法,该方法是全局方法  Vue.set  的一个别名:

vm.$set(vm.items, indexOfItem, newValue)

为了解决第二类问题,你可以使用  splice

vm.items.splice(newLength)

对象更改检测注意事项

还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

对于已经创建的实例,Vue 不能动态添加根级别的响应式属性。但是,可以使用  Vue.set(object, key, value)  方法向嵌套对象添加响应式属性。例如,对于:

var vm = new Vue({
  data: {
    userProfile: {
      name: 'Anika'
    }
  }
})

你可以添加一个新的  age  属性到嵌套的  userProfile  对象:

Vue.set(vm.userProfile, 'age', 27)

你还可以使用  vm.$set  实例方法,它只是全局  Vue.set  的别名:

vm.$set(vm.userProfile, 'age', 27)

有时你可能需要为已有对象赋予多个新属性,比如使用  Object.assign()  或  _.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

Object.assign(vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

你应该这样做:

vm.userProfile = Object.assign({}, vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

显示过滤/排序结果

有时,我们想要显示一个数组的过滤或排序副本,而不实际改变或重置原始数据。在这种情况下,可以创建返回过滤或排序数组的计算属性。

例如:

<li v-for="n in evenNumbers"></li>
data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
  evenNumbers: function () {
    return this.numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}

在计算属性不适用的情况下 (例如,在嵌套  v-for  循环中) 你可以使用一个 method 方法:

<li v-for="n in even(numbers)"></li>
data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
methods: {
  even: function (numbers) {
    return numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}

vue 修饰符

组件基础 — Vue.js

在组件上使用  v-model

自定义事件也可以用于创建支持  v-model  的自定义输入组件。记住:

<input v-model="searchText" />

等价于:

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

当用在组件上时,v-model  则会这样:

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

为了让它正常工作,这个组件内的  <input>  必须:

  • 将其  value  特性绑定到一个名叫  value  的 prop 上
  • 在其  input  事件被触发时,将新的值通过自定义的  input  事件抛出

写成代码之后是这样的:

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

现在  v-model  就应该可以在这个组件上完美地工作起来了:

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

is 特性

动态组件

<template lang="html">
  <div class="hello">
    <p :is="diff"></p>
    <button @click="click">button</button>
  </div>
</template>

<script>
  import a from '@/components/a'
  import b from '@/components/b'
  export default {
    components: {
      'com-a': a,
      'com-b': b
    },
    data() {
      return {
        diff: 'com-a'
      }
    },
    methods: {
      click() {
        this.diff = this.diff === 'com-b' ? 'com-a' : 'com-b'
      }
    }
  }
</script>

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

  • 已注册组件的名字,或
  • 一个组件的选项对象

解析 DOM 模板时的注意事项

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

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

<table>
  <blog-post-row></blog-post-row>
</table>

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

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

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

组件注册 — Vue.js

基础组件的自动化全局注册

Prop — Vue.js

传入一个对象的所有属性

如果你想要将一个对象的所有属性都作为 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>

非 Prop 的特性

一个非 prop 特性是指传向一个组件,但是该组件并没有相应 prop 定义的特性。

因为显式定义的 prop 适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的特性,而这些特性会被添加到这个组件的根元素上。

例如,想象一下你通过一个 Bootstrap 插件使用了一个第三方的  <bootstrap-date-input>  组件,这个插件需要在其  <input>  上用到一个  data-date-picker  特性。我们可以将这个特性添加到你的组件实例上:

<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>

然后这个  data-date-picker="activated"  特性就会自动添加到  <bootstrap-date-input>  的根元素上。

替换 / 合并已有的特性

想象一下  <bootstrap-date-input>  的模板是这样的:

<input type="date" class="form-control" />

为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:

<bootstrap-date-input
  data-date-picker="activated"
  class="date-picker-theme-dark"
></bootstrap-date-input>

在这种情况下,我们定义了两个不同的  class  的值:

  • form-control,这是在组件的模板内设置好的
  • date-picker-theme-dark,这是从组件的父级传入的

对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入  type="text"  就会替换掉  type="date"  并把它破坏!庆幸的是,classstyle  特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark

禁用特性继承

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

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

这尤其适合配合实例的  $attrs  属性使用,该属性包含了传递给一个组件的特性名和特性值,例如:

{
  class: 'username-input',
  placeholder: 'Enter your username'
}

有了  inheritAttrs: false  和  $attrs,你就可以手动决定这些特性会被赋予哪个元素。在撰写基础组件的时候是常会用到的:

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

这个模式允许你在使用基础组件的时候更像是使用原始的 HTML 元素,而不会担心哪个元素是真正的根元素:

<base-input
  v-model="username"
  class="username-input"
  placeholder="Enter your username"
></base-input>

自定义事件 — Vue.js

事件名

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

this.$emit('myEvent')

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

<my-component v-on:my-event="doSomething"></my-component>

不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或属性名,所以就没有理由使用 camelCase 或 PascalCase 了。并且  v-on  事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以  v-on:myEvent  将会变成  v-on:myevent——导致  myEvent  不可能被监听到。

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

自定义组件的  v-model

2.2.0+ 新增

一个组件上的  v-model  默认会利用名为  value  的 prop 和名为  input  的事件,但是像单选框、复选框等类型的输入控件可能会将  value  特性用于不同的目的model  选项可以用来避免这样的冲突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

现在在这个组件上使用  v-model  的时候:

<base-checkbox v-model="lovingVue"></base-checkbox>

这里的  lovingVue  的值将会传入这个名为  checked  的 prop。同时当  <base-checkbox>  触发一个  change  事件并附带一个新的值的时候,这个  lovingVue  的属性将会被更新。

注意你仍然需要在组件的  props  选项里声明  checked  这个 prop。

将原生事件绑定到组件

你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用  v-on  的  .native  修饰符:

<base-input v-on:focus.native="onFocus"></base-input>

在有的时候这是很有用的,不过在你尝试监听一个类似  <input>  的非常特定的元素时,这并不是个好主意。比如上述  <base-input>  组件可能做了如下重构,所以根元素实际上是一个  <label>  元素:

<label>
  
  <input v-bind="$attrs" v-bind:value="value" v-on:input="$emit('input', $event.target.value)" />
</label>

这时,父级的  .native  监听器将静默失败。它不会产生任何报错,但是  onFocus  处理函数不会如你预期地被调用。

为了解决这个问题,Vue 提供了一个  $listeners  属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

{
  focus: function (event) { /_ ... _/ }
  input: function (value) { /_ ... _/ },
}

有了这个  $listeners  属性,你就可以配合  v-on="$listeners"  将所有的事件监听器指向这个组件的某个特定的子元素。对于类似  <input>  的你希望它也可以配合  v-model  工作的组件来说,为这些监听器创建一个类似下述  inputListeners  的计算属性通常是非常有用的:

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function() {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign(
        {},
        // 我们从父级添加所有的监听器
        this.$listeners,
        // 然后我们添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function(event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

现在  <base-input>  组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的  <input>  元素一样使用了:所有跟它相同的特性和监听器的都可以工作。

插槽 — Vue.js

编译作用域

当你想在插槽内使用数据时,例如:

<navigation-link url="/profile"> Logged in as  </navigation-link>

该插槽可以访问跟这个模板的其它地方相同的实例属性 (也就是说“作用域”是相同的)。但这个插槽不能访问  <navigation-link>  的作用域。例如尝试访问  url  是不会工作的。牢记一条准则:

父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。

作用域插槽

2.1.0+ 新增

有的时候你希望提供的组件带有一个可从子组件获取数据的可复用的插槽。例如一个简单的  <todo-list>  组件的模板可能包含了如下代码:

<ul>
  <li v-for="todo in todos" v-bind:key="todo.id"></li>
</ul>

但是在我们应用的某些部分,我们希望每个独立的待办项渲染出和  todo.text  不太一样的东西。这也是作用域插槽的用武之地。

为了让这个特性成为可能,你需要做的全部事情就是将待办项内容包裹在一个  <slot>  元素上,然后将所有和其上下文相关的数据传递给这个插槽:在这个例子中,这个数据是  todo  对象:

<ul>
  <li v-for="todo in todos" v-bind:key="todo.id">
    <!-- 我们为每个 todo 准备了一个插槽, -->
    <!-- 将 `todo` 对象作为一个插槽的 prop 传入。 -->
    <slot v-bind:todo="todo">
      <!-- 回退的内容 -->
      
    </slot>
  </li>
</ul>

现在当我们使用  <todo-list>  组件的时候,我们可以选择为待办项定义一个不一样的  <template>  作为替代方案,并且可以通过  slot-scope  特性从子组件获取数据:

<todo-list v-bind:todos="todos">
  <!-- 将 `slotProps` 定义为插槽作用域的名字 -->
  <template slot-scope="slotProps">
    <!-- 为待办项自定义一个模板, -->
    <!-- 通过 `slotProps` 定制每个待办项。 -->
    <span v-if="slotProps.todo.isComplete"></span> 
  </template>
</todo-list>

在 2.5.0+,slot-scope  不再限制在  <template>  元素上使用,而可以用在插槽内的任何元素或组件上。

解构  slot-scope

如果一个 JavaScript 表达式在一个函数定义的参数位置有效,那么这个表达式实际上就可以被  slot-scope  接受。也就是说你可以在支持的环境下 (单文件组件现代浏览器),在这些表达式中使用  ES2015 解构语法。例如:

<todo-list v-bind:todos="todos">
  <template slot-scope="{ todo }">
    <span v-if="todo.isComplete"></span> 
  </template>
</todo-list>

这会使作用域插槽变得更干净一些。

动态组件 & 异步组件 — Vue.js

处理边界情况 — Vue.js

访问元素 & 组件

程序化的事件侦听器

现在,你已经知道了  $emit  的用法,它可以被  v-on  侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法。我们可以:

  • 通过  $on(eventName, eventHandler)  侦听一个事件
  • 通过  $once(eventName, eventHandler)  一次性侦听一个事件
  • 通过  $off(eventName, eventHandler)  停止侦听一个事件

你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。它们也可以用于代码组织工具。例如,你可能经常看到这种集成一个第三方库的模式:

// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
   // Pikaday 是一个第三方日期选择器的库
  this.picker = new Pikaday({
  field: this.$refs.input,
  format: 'YYYY-MM-DD'
  })
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
  this.picker.destroy()
}

这里有两个潜在的问题:

  • 它需要在这个组件实例中保存这个  picker,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
  • 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。

你应该通过一个程序化的侦听器解决这两个问题:

mounted: function () {
  var picker = new Pikaday({
  field: this.$refs.input,
  format: 'YYYY-MM-DD'
  })

  this.$once('hook:beforeDestroy', function () {
  picker.destroy()
  })
}

使用了这个策略,我甚至可以让多个输入框元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:

mounted: function () {
  this.attachDatepicker('startDateInput')
  this.attachDatepicker('endDateInput')
},
methods: {
  attachDatepicker: function (refName) {
    var picker = new Pikaday({
      field: this.$refs[refName],
      format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
      picker.destroy()
    })
  }
}

查阅这个 fiddle  可以了解到完整的代码。注意,即便如此,如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件。在这个例子中,我们推荐创建一个可复用的  <input-datepicker>  组件。

想了解更多程序化侦听器的内容,请查阅实例方法 / 事件相关的 API。

注意 Vue 的事件系统不同于浏览器的  EventTarget API。尽管它们工作起来是相似的,但是  $emit$on, 和  $off  并不是  dispatchEventaddEventListener  和  removeEventListener  的别名。

循环引用

模板定义的替代品

控制更新

混入 — Vue.js

全局混入

也可以全局注册混入对象。注意使用! 一旦使用全局混入对象,将会影响到  所有  之后创建的 Vue 实例。使用恰当时,可以为自定义对象注入处理逻辑。

// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
  created: function() {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

new Vue({
  myOption: 'hello!'
})
// => "hello!"

谨慎使用全局混入对象,因为会影响到每个单独创建的 Vue 实例 (包括第三方模板)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。也可以将其用作 Plugins  以避免产生重复应用