VUE(2)——组件基础

文章目录

  • 组件基础
    • 组件是什么?
    • 组件注册
      • 全局组件
      • 局部组件
      • 组件名
      • 组件复用
      • 自闭合组件
      • 组件的data选项
      • 单个根元素
  • 组件_Prop
    • 注册自定义特性
    • Prop的大小写
    • 传递静态或动态 Prop
      • 传递一个对象的所有属性
  • 组件_Prop验证
  • 组件_单向数据流
  • 组件_非Prop特性
    • 替换/合并已有的特性
    • 禁用特性继承
  • 组件_监听组件事件
    • 使用事件抛出一个值
    • 事件名
    • 将原生事件绑定到组件
    • 在组件上使用 v-model
    • .sync 修饰符
    • v-model VS .sync
  • 组件_插槽
    • 插槽内容
    • 编译作用域
    • 后备内容
    • 具名插槽
    • 作用域插槽
      • 独占默认插槽的缩写语法
      • 解构插槽Prop
    • 动态插槽名
    • 具名插槽的缩写
    • 废弃了的语法
      • 带有slot特性的具名插槽
      • 带有slot-scope特性的作用域插槽
  • 组件_动态组件
    • 基本使用
    • keep-alive
    • activated & deactivated
  • 组件_处理边界情况
    • 访问元素 & 组件
      • 访问根实例
      • 访问父级组件实例
      • 依赖注入
      • 访问子组件实例或子元素
    • 程序化的事件侦听器
    • 循环引用
      • 递归组件
      • 组件之间的循环引用
    • 模板定义的替代品
      • 内联模板
      • X-Template
    • 控制更新
      • 强制更新
      • 通过v-once创建低开销的静态组件
  • 组件_通信
    • prop
    • $emit
      • v-model
      • .sync
    • $attrs
    • $listeners
    • $root
    • $parent
    • $children
    • ref
    • provide & inject
    • eventBus(事件总线)
    • Vuex
  • 混入
    • 基础
    • 选项合并
    • 全局混入
  • 自定义指令
    • 简介
    • 钩子函数
      • bind
      • inserted
      • update
      • componentUpdated
      • unbind
    • 钩子函数参数
    • 练习
      • 模拟 v-show
      • 模拟 v-model
      • 写一个 v-slice(截取文本框)
      • 动态指令参数
    • 函数简写
    • 对象字面量
          • ——后续还会更新。

组件基础

组件是什么?

组件是可复用的Vue实例,且带有一个名字,例如名字为my-cmp,那么我们则可以在一个通过new Vue创建的根实例中,把这个组件作为自定义元素来使用:

<div id="app">
  <my-cmp>my-cmp>
div>
const vm = new Vue({
  el: '#app'
})

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

组件注册

全局组件

Vue.component

利用Vue.component创建的组件组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。

参数:

  • {string}
  • {Function | Object} [definition]

用法:
注册或获取全局组件。注册还会自动使用给定的id设置组件的名称。

示例:

<div id="app">
  <button-counter>button-counter>
div>
Vue.component('button-counter', {
  data () {
    return {
      count: 0,
    }
  },
  template: `
    
  `
})

const vm = new Vue({
  el: '#app',
})

局部组件

components选项中定义要使用的组件。
对于 components 对象中的每一个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象。

示例:

<div id="#app">
  <button-counter>button-counter>
div>
const buttonCounter = {
  data () {
    return {
      count: 0
    }
  },
  template: `
    
  `,
}

const vm = new Vue({
  el: '#app',
  components: {
    'button-counter': buttonCounter
  }
})

组件名

在注册一个组件的时候,我们始终需要给它一个名字。你给予组件的名字可能依赖于你打算拿它来做什么,所以命名要语义化。
组件名大小写
定义组件名的方式有两种:

使用kebab-case (短横线分隔命名)

Vue.component('my-component', {/***/});

当使用kebab-case定义一个组件时,你必须在引用这个自定义元素时使用kebab-case,例如:

使用PascalCase (大驼峰命名)

Vue.component('MyComponent', {/***/});

当使用PascalCase定义一个组件时,你在引用这个自定义元素时两种命名法都可以。也就是说 都是可接受的。注意,尽管如此,直接在 DOM (即字符串模板或单文件组件) 中使用时只有 kebab-case 是有效的。

另:我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。

组件复用

可以将组件进行任意次数的复用:

<div id="#app">
  <button-counter>button-counter>
  <button-counter>button-counter>
  <button-counter>button-counter>
div>

自闭合组件

在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。

自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。

不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。

组件的data选项

当我们定义一个组件时,它的 data 并不是像这样直接提供一个对象:

data: {
  count: 0
}

取而代之的是,一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝

data () {
  return {
    count: 0
  }
}

如果 Vue 没有这条规则,点击一个按钮就可能会像下面一样影响到其它所有实例:

avatar

单个根元素

每个组件必须只有一个根元素,当模板的元素大于1时,可以将模板的内容包裹在一个父元素内。

组件_Prop

注册自定义特性

组件默认只是写好结构、样式和行为,使用的数据应由外界传递给组件。

如何传递?注册需要接收的prop,将数据作为一个自定义特性传递给组件。

如:

<div id="app">
  <video-item 
    title="羊村摇" 
    poster="https://developer.duyiedu.com/bz/video/955bac93ccb7f240d25a79b2ff6a9fdbda9537bc.jpg@320w_200h.webp" 
    play="638000" 
    rank="1207"
  >video-item>
div>
Vue.component('video-item', {
  props: ['title', 'poster', 'play', 'rank'],
})

在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data 中的值一样:

<div id="app">
  <video-item 
    title="羊村摇" 
    poster="https://developer.duyiedu.com/bz/video/955bac93ccb7f240d25a79b2ff6a9fdbda9537bc.jpg@320w_200h.webp" 
    play="638000" 
    rank="1207"
  >video-item>
div>
Vue.component('video-item', {
  props: ['title', 'poster', 'play', 'rank'],
  template: `
{{ title }}
`
})

Prop的大小写

HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。故:当传递的prop为 短横线分隔命名时,组件内的props 应为驼峰命名 。
如:

<div id="app">
  
  <video-item sub-title="hello!">video-item>
div>
Vue.component('video-item', {
  // 在 JavaScript 中是 subTitle 
  props: ['subTitle'],
  template: '

{{ subTitle }}

'
})

要注意的是:如果使用的是字符串模板,那么这个限制就不存在了。

传递静态或动态 Prop

像这样,我们已经知道了可以给 prop 传入一个静态的值:

<video-item title="羊村摇">video-item>

若想要传递一个动态的值,可以配合v-bind指令进行传递,如:

<video-item :title="title">video-item>

传递一个对象的所有属性

如果你想要将一个对象的所有属性都作为 prop 传入,你可以使用不带参数的 v-bind 。例如,对于一个给定的对象 person:

person: {
  name: 'shanshan',
  age: 18
}

传递全部属性:

<my-component v-bind="person">my-component>

上述代码等价于:

<my-component
  :name="person.name"
  :age="person.age"
>my-component>

组件_Prop验证

我们可以为组件的 prop 指定验证要求,例如你可以要求一个 prop 的类型为什么。如果说需求没有被满足的话,那么Vue会在浏览器控制台中进行警告,这在开发一个会被别人用到的组件时非常的有帮助。

为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:

Vue.component('my-component', {
  props: {
    title: String,
    likes: Number,
    isPublished: Boolean,
    commentIds: Array,
    author: Object,
    callback: Function,
    contactsPromise: Promise
  }
})

上述代码中,对prop进行了基础的类型检查,类型值可以为下列原生构造函数中的一种:StringNumberBooleanArrayObjectDateFunctionSymbol、任何自定义构造函数、或上述内容组成的数组。
需要注意的是nullundefined 会通过任何类型验证。
除基础类型检查外,我们还可以配置高级选项,对prop进行其他验证,如:类型检测、自定义验证和设置默认值。
如:

Vue.component('my-component', {
  props: {
    title: {
      type: String, // 检查 prop 是否为给定的类型
      default: '杉杉最美',   // 为该 prop 指定一个默认值,对象或数组的默认值必须从一个工厂函数返回,如:default () { return {a: 1, b: 10} },
      required: true, // 定义该 prop 是否是必填项
      validator (prop) {  // 自定义验证函数,该prop的值回作为唯一的参数代入,若函数返回一个falsy的值,那么就代表验证失败
        return prop.length < 140;
      }
    }
  }
})

为了更好的团队合作,在提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。

组件_单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

这里有两种常见的试图改变一个 prop 的情形:

  1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用,在后续操作中,会将这个值进行改变。在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
  1. 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

组件_非Prop特性

非Prop特性指的是,一个未被组件注册的特性。当组件接收了一个非Prop特性时,该特性会被添加到这个组件的根元素上。

替换/合并已有的特性

想象一下 的模板是这样的:

<input type="date" class="b">

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

<my-cmp
  class="my-cmp"
>my-cmp>

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

  • my-cmp,这是在组件的模板内设置好的
  • b,这是从组件的父级传入的

对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果在my-cmp元素上传入 type=“text” 就会替换掉 type=“date” 并把它破坏!庆幸的是,class 和 style 特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:my-cmp b。

禁用特性继承

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

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

在这种情况下,非常适合去配合实例的 $attrs 属性使用,这个属性是一个对象,键名为传递的特性名,键值为传递特性值。

{
  required: true,
  placeholder: 'Enter your username'
}

使用 inheritAttrs: false$attrs 相互配合,我们就可以手动决定这些特性会被赋予哪个元素。如:

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    
  `,
})

注意:inheritAttrs: false 选项不会影响 style 和 class 的绑定。

组件_监听组件事件

首先,我们来写一个博文组件,如:

Vue.component('blog-post', {
  props: {
    post: {
      type: Object,
    }
  },
  template: `
    

{{ post.title }}

{{ post.content }}
`
, })
<div id="app">
  <div :style="{fontSize: postFontSize + 'em'}">
    <blog-post
      v-for="post in posts"
      :key="post.id"
      :post="post"
    >
    blog-post>
  div>
div>
const vm = new Vue({
  el: '#app',
  data: {
    posts: [
      { title: '标题1', content: '正文内容', id: 0, },
      { title: '标题2', content: '正文内容', id: 1, },
      { title: '标题3', content: '正文内容', id: 2, },
    ],
    postFontSize: 1
  }
})

可以看到每一个博文组件中,都有一个按钮,可以去放大页面中字体的字号,也就是说,当点击这个按钮时,我们要告诉父组件改变postFontSize数据去放大所有博文的文本。碰见这样的情况,该如何做呢?

Vue 实例提供了一个自定义事件来解决这个问题。父组件可以像处理原生DOM元素一样,通过 v-on指令,监听子组件实例的任意事件,如:

<div id="app">
  <div :style="{fontSize: postFontSize + 'em'}">
    <blog-post
      ...
      @enlarge-text="postFontSize += 0.1"
    >
    blog-post>
  div>
div>

那么,怎么样能够去监听到一个 enlarge-text这么奇怪的事件呢?这就需要在组件内,去主动触发一个自定义事件了。

如何触发?
通过调用 $emit 方法 并传入事件名称来触发一个事件,如:

Vue.component('blog-post', {
  props: {
    ...
  },
  template: `
    
... ...
`
, })

这样,父组件就可以接收该事件,更新数据 pageFontSize 的值了。

使用事件抛出一个值

在有些情况下,我们可能想让 组件决定它的文本要放大多少。这是可以使用 $emit 的第二个参数来提供这个值,如:

Vue.component('blog-post', {
  props: {
    ...
  },
  template: `
    
... ...
`
, })

在父组件监听这个事件时,可以通过 $event 访问到被抛出的这个值:

<div id="app">
  <div :style="{fontSize: postFontSize + 'em'}">
    <blog-post
      ...
      @enlarge-text="postFontSize += $event"
    >
    blog-post>
  div>
div>

或者,将这个事件处理函数写成一个方法:

<div id="app">
  <div :style="{fontSize: postFontSize + 'em'}">
    <blog-post
      ...
      @enlarge-text="onEnlargeText"
    >
    blog-post>
  div>
div>

那么,这个值将会作为第一个参数,传入这个方法:

methods: {
  onEnlargeText: function (enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}

事件名

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

this.$emit('myEvent')

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


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

与组件和prop不同的是,事件名不会被当作一个 JS 变量名或者属性名,所以就没有理由使用camelCase 或 PascalCase 了。

并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写,所以 @myEvent 将会变成 @myevent,导致 myEvent 不可能被监听到。

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

将原生事件绑定到组件

在组件上去监听事件时,我们监听的是组件的自动触发的自定义事件,但是在一些情況下,我们可能想要在一个组件的根元素上直接监听一个原生事件。这是,可以使用 v-on 指令的 .native 修饰符,如:

<base-input @focus.native="onFocus" @blur.native="onBlur">base-input>
Vue.component('base-input', {
  template: `
    
  `
})

这样处理,在有些时候是很有用的,不过在尝试监听一个类似元素时,这并不是一个好主意,例如组件可能做了重构,如:

<label>
  姓名:
  <input type="text">
label>

可以看到,此时组件的根元素实际上是一个元素,那么父级的.native监听器将静默失败。它不会产生任何报错,但是onFocus处理函数不会如预期被调用。

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

{
  focus: function (event) { /* ... */ }
  blur: function (event) { /* ... */ },
}

有了这个 $listeners 属性,我们可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素,如:

Vue.component('base-input', {
  template: `
    
  `
})

在组件上使用 v-model

由于自定义事件的出现,在组件上也可以使用v-model指令。

在 input 元素上使用v-mode指令时,相当于绑定了value特性以及监听了input事件:

<input v-model="searchText" />

等价于:

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

当把v-model指令用在组件上时:

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

则等价于:

<base-input
  :value="searchText"
  @input="searchText = $event"
/>

同 input 元素一样,在组件上使用v-model指令,也是绑定了value特性,监听了input事件。

所以,为了让 v-model 指令正常工作,这个组件内的必须:

  • 将其value特性绑定到一个叫 value 的prop 上
  • 在其input事件被触发时,将新的值通过自定义的input事件抛出
    如:
Vue.component('base-input', {
  props: ['value'],
  template: `
    
  `
}) 

这样操作后,v-model就可以在这个组件上工作起来了。

通过上面的学习,我们知道了,一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value 特性用于不同的目的。碰到这样的情况,我们可以利用 model 选项来避免冲突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    
  `
})

使用组件:

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

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

.sync 修饰符

除了使用 v-model 指令实现组件与外部数据的双向绑定外,我们还可以用 v-bind 指令的修饰符 .sync 来实现。

那么,该如何实现呢?
先回忆一下,不利用 v-model 指令来实现组件的双向数据绑定:

<base-input :value="searchText" @input="searchText = $event">base-input>
Vue.component('base-input', {
  props: ['value'],
  template: `
    
  `
}) 

那么,我们也可以试着,将监听的事件名进行更改,如:

<base-input :value="searchText" @update:value="searchText = $event">base-input>
Vue.component('base-input', {
  props: ['value'],
  template: `
    
  `
}) 

这样也是可以实现双向数据绑定的,那么和 .sync 修饰符 有什么关系呢?
此时,我们对代码进行修改:

<base-input :value.sync="searchText">base-input>
Vue.component('base-input', {
  props: ['value'],
  template: `
    
  `
}) 

所以,.sync 修饰符 本质上也是一个语法糖,在组件上使用:

<base-input :value.sync="searchText">base-input>

等价于:

<base-input
  :value="searchText"
  @update:value="searchText = $event"
/>

当我们用一个对象同时设置多个prop时,也可以将.sync修饰符和 v-bind配合使用:

<base-input v-bind.sync="obj">base-input>

注意:

  • 带有.sync修饰符的v-bind指令,只能提供想要绑定的属性名,不能和表达式一起使用,如::title.sync="1+1",这样操作是无效的
  • v-bind.sync 用在 一个字面量对象上,如 v-bind.sync="{ title: 'haha' }",是无法工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

v-model VS .sync

先明确一件事情,在 vue 1.x 时,就已经支持 .sync 语法,但是此时的 .sync 可以完全在子组件中修改父组件的状态,造成整个状态的变换很难追溯,所以官方在2.0时移除了这个特性。然后在 vue2.3时,.sync又回归了,跟以往不同的是,现在的.sync完完全全就是一个语法糖的作用,跟v-model的实现原理是一样的,也不容易破环院有的数据模型,所以使用上更安全也更方便。

  • 两者都是用于实现双向数据传递的,实现方式都是语法糖,最终通过 prop + 事件 来达成目的。
  • vue 1.x 的 .sync 和 v-model 是完全两个东西,vue 2.3 之后可以理解为一类特性,使用场景略微有区别
  • 当一个组件对外只暴露一个受控的状态,切都符合统一标准的时候,我们会使用v-model来处理。.sync则更为灵活,凡是需要双向数据传递时,都可以去使用。

组件_插槽

和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:

<my-cmp>
  Something bad happened.
my-cmp>

如果有这样的需求,我们就可以通过插槽来做。

插槽内容

通过插槽,我们可以这样合成组件:

<my-cmp>
  写在组件标签结构中的内容
my-cmp>

组件模板中可以写成:

<div>
  <slot>slot>
div>

当组件渲染时,将会被替换为“写在组件标签结构中的内容”。
插槽内可以包含任何模板代码,包括HTML和其他组件。
如果没有包含元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

编译作用域

当在插槽中使用数据时:

<my-cmp>
  这是插槽中使用的数据:{{ user }}
my-cmp>

该插槽跟模板的其他地方一样可以访问相同的实例属性,也就是相同的“作用域”,而不能访问的作用域。
请记住:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

后备内容

我们可以设置默认插槽,它会在没有提供内容时被渲染,如,在组件中:

Vue.compopnent('my-cmp', {
  template: `
    
  `
})

我们希望这个 ` })

当使用组件未提供插槽时,后备内容将会被渲染。如果提供插槽,则后备内容将会被取代。

具名插槽

有时我们需要多个插槽,如组件:

Vue.compopnent('my-cmp', {
  template: `
    
`
})

此时,可以在元素上使用一个特殊的特性:name。利用这个特性定义额外的插槽:

Vue.compopnent('my-cmp', {
  template: `
    
`
})

一个不带 name 的 出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个