VUE学习笔记——组件篇

文章目录

组件基础

组件是什么?

组件是可复用的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: `
    <button @click="count ++">你按了我{{ count }}次</button>
  `
})

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

局部组件

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

示例:

<div id="#app">
  <button-counter></button-counter>
</div>
const buttonCounter = {
  data () {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count ++">你按了我{{ count }}次</button>
  `,
}

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

组件名

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

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

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

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

使用PascalCase (大驼峰命名)

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

当使用PascalCase定义一个组件时,你在引用这个自定义元素时两种命名法都可以。也就是说<my-component-name><MyComponentName> 都是可接受的。注意,尽管如此,直接在 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: `<div>{{ title }}</div>`
})

Prop的大小写

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

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

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

传递静态或动态 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特性时,该特性会被添加到这个组件的根元素上。

替换/合并已有的特性

想象一下 <my-cmp> 的模板是这样的:

<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: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `,
})

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

组件_监听组件事件

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

Vue.component('blog-post', {
  props: {
    post: {
      type: Object,
    }
  },
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <button>放大字号</button>
      <div>{{ post.content }}</div>
    </div>
  `,
})
<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: `
    <div class="blog-post">
      ...
      <button @click="$emit('enlarge-text')">放大字号</button>
      ...
    </div>
  `,
})

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

使用事件抛出一个值

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

Vue.component('blog-post', {
  props: {
    ...
  },
  template: `
    <div class="blog-post">
      ...
      <button @click="$emit('enlarge-text', 0.2)">放大字号</button>
      ...
    </div>
  `,
})

在父组件监听这个事件时,可以通过 $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: `
    <input type="text" />
  `
})

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

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

可以看到,此时组件的根元素实际上是一个

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

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

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

Vue.component('base-input', {
  template: `
    <label>
      名字:
      <input v-on="$listeners" />
    </label>
  `
})

在组件上使用 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 指令正常工作,这个组件内的<input>必须:

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

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

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

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

使用组件:

<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: `
    <input 
      :value="value"
      @input="$emit('input', $event.target.value)"
    />
  `
}) 

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

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

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

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

所以,.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>

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

编译作用域

当在插槽中使用数据时:

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

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

后备内容

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

Vue.compopnent('my-cmp', {
  template: `
    <button type="submit">
      <slot></slot>
    </button>
  `
})

我们希望这个<button>内绝大多数情况下都渲染文本“Submit”,此时就可以将“Submit”作为后备内容,如:

Vue.compopnent('my-cmp', {
  template: `
    <button type="submit">
      <slot>Submit</slot>
    </button>
  `
})

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

具名插槽

有时我们需要多个插槽,如<my-cmp>组件:

Vue.compopnent('my-cmp', {
  template: `
    <div class="container">
      <header>
        <!-- 页头 -->
      </header>
      <main>
        <!-- 主要内容 -->
      </main>
      <footer>
        <!-- 页脚 -->
      </footer>
    </div>
  `
})

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

Vue.compopnent('my-cmp', {
  template: `
    <div class="container">
      <header>
        <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>
  `
})

一个不带 name 的 <slot> 出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

<my-cmp>
  <template v-slot:header>
    <h1>头部</h1>
  </template>

  <p>内容</p>
  <p>内容</p>

  <template v-slot:footer>
    <p>底部</p>
  </template>
</my-cmp>

现在<template>元素中的所有内容都会被传入相应的插槽。任何没有被包裹在带有v-slot<template>中的内容都会被视为默认插槽的内容。
为了模板更清晰,也可以写成以下这样:

<my-cmp>
  <template v-slot:header>
    <h1>头部</h1>
  </template>

  <template v-slot:default>
    <p>内容</p>
    <p>内容</p>
  </template>

  <template v-slot:footer>
    <p>底部</p>
  </template>
</my-cmp>

注意:v-slot只能添加在<template>上,只有一种例外情况。

作用域插槽

为了能够让插槽内容访问子组件的数据,我们可以将子组件的数据作为<slot>元素的一个特性绑定上去:

Vue.component('my-cmp', {
  data () {
    return {
      user: {
        name: '杉杉',
        age: 18,
      }
    }
  },
  template: `
    <span>
      <slot v-bind:user="user"></slot>
    </span>
  `,
})

绑定在 <slot> 元素上的特性被称为插槽 prop
那么在父级作用域中,我们可以给v-slot带一个值来定义我们提供的插槽prop的名字:

<div id="app">
  <my-cmp>
    <template v-slot:default="slotProps">
      {{ slotProps.user.name }}
    </template>
  </my-cmp>
</div>

独占默认插槽的缩写语法

当被提供的内容只有默认插槽时,组件的标签可以被当作插槽的模板来使用,此时,可以将v-slot直接用在组件上:

<my-cmp v-slot:default="slotProps">
  {{ slotProps.user.name }}
</my-cmp>

也可以更简单:

<my-cmp v-slot="slotProps">
  {{ slotProps.user.name }}
</my-cmp>

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

<!-- 无效,会导致警告 -->
<my-cmp v-slot="slotProps">
  {{ slotProps.user.name }}
  <template v-slot:other="otherSlotProps">
    slotProps 在这里是不合法的
  </template>
</my-cmp>

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

解构插槽Prop

我们可以使用解构传入具体的插槽prop,如:

<my-cmp v-slot="{ user }">
  {{ user.name }}
</my-cmp>

这样模板会更简洁,尤其是在为插槽提供了多个prop时。
此外还可以有其他可能,如prop重命名:

<my-cmp v-slot="{ user: person }">
  {{ person.name }}
</my-cmp>

以及自定义后备内容,当插槽prop是undefined时生效:

<my-cmp v-slot="{ user = { name: 'Guest' } }">
  {{ user.name }}
</my-cmp>

动态插槽名

Vue 2.6.0新增

<my-cmp>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</my-cmp>

具名插槽的缩写

Vue 2.6.0新增

v-onv-bind一样,v-slot也有缩写,将v-slot:替换为#

<my-cmp>
  <template #header>
    <h1>头部</h1>
  </template>

  <template #default>
    <p>内容</p>
    <p>内容</p>
  </template>

  <template #footer>
    <p>底部</p>
  </template>
</my-cmp>

当然,和其它指令一样,该缩写只在其有参数的时候才可用。

废弃了的语法

带有slot特性的具名插槽

自 2.6.0 起被废弃

<my-cmp>
  <template slot="header">
    <h1>头部</h1>
  </template>

  <template>
    <p>内容</p>
    <p>内容</p>
  </template>

  <template slot="footer">
    <p>底部</p>
  </template>
</my-cmp>

带有slot-scope特性的作用域插槽

自 2.6.0 起被废弃

<my-cmp>
  <template slot="default" slot-scope="slotProps">
    {{ slotProps.user.name }}
  </template>
</my-cmp>

组件_动态组件

基本使用

当我们在一个多标签的界面中,在不同组件之间进行动态切换是非常有用的。

<div id="app">
  <button 
    v-for="page in pages"
    @click="pageCmp = page.cmp"
    :key="page.id"
  >{{ page.name }}</button>
  <component :is="pageCmp"></component>
</div>
Vue.component('base-post', {
  data () {
    return {
      postCmp: '',
      posts: [
        { title: "标题1", content: { template: `<div>内容1</div>`}, id: 11}, 
        { title: "标题2", content: { template: `<div>内容2</div>`}, id: 12}, 
        { title: "标题3", content: { template: `<div>内容3</div>`}, id: 13}, 
      ],
    }
  },
  mounted () {
    this.postCmp = this.posts[0].content;
  },
  template: `
    <div>
      <button
        v-for="post in posts"
        @click="postCmp = post.content"
        :key="post.id"
      >{{ post.title }}</button>
      <component :is="postCmp"></component>
    </div>
  `
})
Vue.component('base-more', {
  template: `<div>更多内容</div>`
})

const vm = new Vue({
  el: '#app',
  data: {
    pages: [
      { name: '博客', cmp: 'base-post', id: 0},
      { name: '更多', cmp: 'base-more', id: 1}
    ],
    pageCmp: 'base-post'
  },
})

通过上面方法,我们可以实现组件间的切换,能够注意到的是:每一次切换标签时,都会创建一个新的组件实例,重新创建动态组件在更多情况下是非常有用的,但是在这个案例中,我们会更希望哪些标签的组件实例能够在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个<keep-alive>元素将动态组件包裹起来。如:

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="pageCmp"></component>
</keep-alive>

注意:<keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。

keep-alive

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

activated & deactivated

activated:keep-alive 组件激活时调用。
deactivated: keep-alive 组件停用时调用。

组件_处理边界情况

接下来我们要学习的都是和处理边界情况有关的功能,即一些需要对 Vue 的规则做一些小调整的特殊情况。需要注意的是,这些功能都是有劣势或危险场景的。

访问元素 & 组件

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。

访问根实例

在每个子组件中,可以通过 $root 访问根实例。

// Vue 根实例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar () { /* ... */ }
  },
  methods: {
    baz () { /* ... */ }
  }
})

所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。

// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()

在demo或在有少量组件的小型应用中使用是非常方便的。但是在大型应用里使用就会很复杂了。所以,我们还是要用Vuex(后面会学)来管理应用的状态。

访问父级组件实例

在子组件中,可以通过 $parent 访问 父组件实例。这可以替代将数据以prop的方式传入子组件的方式。

如:

<cmp-parent>
  <cmp-a></cmp-a>
</cmp-parent>

若 cmp-parent 需要共享一个属性 share,它的所有子元素都需要访问 share 属性,在这种情况下 cmp-a 可以通过 this.$parent.share的方式访问share。

但是,通过这种模式构建出来的组件内部仍然容易出现问题。比如,我们在cmp-a 中嵌套一个一个子组件 cmp-b,如:

<cmp-parent>
  <cmp-a>
    <cmp-b></cmp-b>
  </cmp-a>
</cmp-parent>

那么,在cmp-b组件中去访问share时,需要先查看一下,其父组件中是否存在share,如果不存在,则在向上一级查找,落实到代码上为:

var share = this.$parent.share || this.$parent.$parent.share;

这样做,很快组件就会失控:触达父级组件会使应用更难调试和理解,尤其是当变更父组件数据时,过一段时间后,很难找出变更是从哪里发起的。

碰到上述情况,可以使用依赖注入解决。

依赖注入

在上面的例子中,利用 $parent 属性,没有办法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:provide 和 inject。

provide 选项允许我们指定想要提供给后代组件的数据/方法,例如:

Vue.component('cmp-parent', {
  provide () {
    return {
      share: this.share,
    }
  },
  data () {
    return {
      share: 'share',
    }
  },
  template: `<div>cmp-parent</div>`
})

然后再任何后代组件中,我们都可以使用 inject 选项来接受指定想要添加在实例上的属性。

Vue.component('cmp-a', {
  inject: ['share'],
  template: `<div>cmp-a</div>`
})

相比 $parent 来说,这个用法可以让我们在任意后代组件中访问share,而不需要暴露整个 cmp-parent 实例。这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和 props 一样。

实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  • 祖先组件不需要知道哪些后代组件使用它提供的属性
  • 后代组件不需要知道被注入的属性来自哪里

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。

访问子组件实例或子元素

尽管存在prop和事件,但是有时候我们仍可能需要在JS里直接访问一个子组件,那么此时,我们可以通过 ref 特性为子组件赋予一个ID引用:

<my-cmp ref="cmp"></my-cmp>

这样就可以通过this.$refs.cmp 来访问<my-cmp>实例。
ref 也可以 对指定DOM元素进行访问,如:

<input ref="input" />

那么,我们可以通过 this.$refs.input 来访问到该DOM元素。

当ref 和 v-for 一起使用时,得到的引用将会是一个包含了对应数据源的这些子组件的数组。

注意:$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。应该避免在模板或计算属性中访问 $refs。

程序化的事件侦听器

除了 v-on 和 $emit 外, Vue 实例在其事件接口中还提供了其它的方法。我们可以:

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

这几个方法一般不会被用到,但是,当需要在一个组件实例上手动侦听事件时,他们是可以派的上用场的。

例如,有时我们会在组件中集成第三方库:

Vue.component('my-cmp', {
  // 一次性将这个日期选择器附加到一个输入框上
  // 它会被挂载到 DOM 上。
  mounted () {
    // Pikaday 是一个第三方日期选择器的库
    this.picker = new Pikaday({
      field: this.$refs.input,
      format: 'YYYY-MM-DD',
    })
  },
  // 在组件被销毁之前,
  // 也销毁这个日期选择器。
  beforeDestroy () {
    this.picked.destroy();
  },
  template: `
    <div>
      <input type="text" ref="input" />
      <button @click="$destroy()">销毁组件</button>
    </div>
  `,
})

使用上面的方法,有两个潜在的问题:

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

所有,我们可以通过程序化的侦听器解决这两个问题:

Vue.component('my-cmp', {
  mounted () {
    var picker = new Pikaday({
      field: this.$refs.input,
      format: 'YYYY-MM-DD',
    })
    this.$once('hook:beforeDestroy', () => {
      picker.destroy();
    })
  },
  template: `
    <div>
      <input type="text" ref="input" />
      <button @click="$destroy()">销毁组件</button>
    </div>
  `
})

使用了这个策略,我们还可以让多个输入框元素使用不同的pikaday:

Vue.component('my-cmp', {
  mounted () {
    this.datePicker('inputA');
    this.datePicker('inputB');
  },
  methods: {
    datePicker (refName) {
      var picker = new Pikaday({
        field: this.$refs[refName],
        format: 'YYYY-MM-DD',
      })
      this.$once('hook:beforeDestroy', () => {
        picker.destroy();
      })
    },
  },
  template: `
    <div>
      <input type="text" ref="inputA" />
      <input type="text" ref="inputB" />
      <button @click="$destroy()">销毁组件</button>
    </div>
  `
})

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

循环引用

递归组件

组件是可以在它们自己的模板中调用自身的,不过它们只能通过name选项来做这件事:

name: 'my-cmp'

不过当使用 Vue.component 全局注册一个组件时,全局的ID会自动设置为该组件的 name 选项。

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

稍有不慎,递归组件就可能导致无限循环:

name: 'my-cmp',
template: `<div><my-cmp /></div>`

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

组件之间的循环引用

有时,在去构建一些组件时,会出现组件互为对方的后代/祖先:

Vue.component('cmp-a', {
  template: `
    <div>
      <cmp-b></cmp-b>
    </div>
  `
})
Vue.component('cmp-b', {
  template: `
    <div>
      <cmp-a></cmp-a>
    </div>
  `
})

此时,我们使用的是全局注册组件,并不会出现悖论,但是如果使用的为局部组件就会出现悖论。

但是即使用了全局注册组件,在使用webpack去导入组件时,也会出现一个错误:Failed to mount component: template or render function not defined。

模块系统发现它需要 A,但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点:“A 反正是需要 B 的,但是我们不需要先解析 B。”

beforeCreate () {
  this.$options.components.CmpB = require('./tree-folder-contents.vue').default;
}

或者,在本地注册组件的时候,你可以使用 webpack 的异步 import:

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

模板定义的替代品

内联模板

在使用组件时,写上特殊的特性:inline-template,就可以直接将里面的内容作为模板而不是被分发的内容(插槽)。

<my-cmp inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-cmp>

不过,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

X-Template

另一个定义模板的方式是在一个 <script> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:

<script 
  type="text/x-template" 
  id="hello-world-template"
>
  <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})

这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

控制更新

强制更新

当 更改了某个数据,页面未重新渲染时,可以调用 $forceUpdate 来做一次强制更新。

但是在做强制更新前,需要留意数组或对象的变更检测注意事项,99.9%的情况,都是在某个地方做错了事,如果做了上述检查,仍未发现问题,那么可以通过 $forceUpdate来更新。

通过v-once创建低开销的静态组件

渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来,就像这样:

Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})

试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。

组件_通信

prop

父组件传递数据给子组件时,可以通过特性传递。

推荐使用这种方式进行父->子通信。

$emit

子组件传递数据给父组件时,触发事件,从而抛出数据。

推荐使用这种方式进行子->父通信。

v-model

.sync

$attrs

祖先组件传递数据给子孙组件时,可以利用$attrs传递。

demo或小型项目可以使用$attrs进行数据传递,中大型项目不推荐,数据流会变的难于理解。

$attrs的真正目的是撰写基础组件,将非Prop特性赋予某些DOM元素。

$listeners

可以在子孙组件中执行祖先组件的函数,从而实现数据传递。

demo或小型项目可以使用$listeners进行数据传递,中大型项目不推荐,数据流会变的难于理解。

$listeners的真正目的是将所有的事件监听器指向这个组件的某个特定的子元素。

$root

可以在子组件中访问实例的数据。

对于 demo 或非常小型的有少量组件的应用来说这是很方便的。中大型项目不适用。会使应用难于调试和理解。

$parent

可以在子组件中访问实例的数据。

对于 demo 或非常小型的有少量组件的应用来说这是很方便的。中大型项目不适用。会使应用难于调试和理解。

$children

可以在父组件中访问实例的数据。

对于 demo 或非常小型的有少量组件的应用来说这是很方便的。中大型项目不适用。会使应用难于调试和理解。

ref

可以在父组件中访问实例的数据。

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的,适用于demo或小型项目。

provide & inject

祖先组件提供数据(provide),子孙组件按需注入(inject)。

会将组件的阻止方式,耦合在一起,从而使组件重构困难,难以维护。不推荐在中大型项目中使用,适用于一些小组件的编写。

eventBus(事件总线)

Vue.prototype.$bus = new Vue();
Vue.component('cmp-a', {
  data () {
    return {
      a: 'a'
    }
  },
  methods: {
    onClick () {
      this.$bus.$on('click', this.a)
    }
  },
  template: `
    <div>
      <button @click="onClick">点击</button>
    </div>
  `,
})
Vue.component('cmp-a', {
  mounted () {
    this.$bus.$on('click', data => {
      console.log(data);
    })
  },
  template: `
    <div>b</div>
  `,
})

非父子组件通信时,可以使用这种方法,但仅针对于小型项目。中大型项目使用时,会造成代码混乱不易维护。

Vuex

状态管理,中大型项目时强烈推荐使用此种方式,日后再学~

混入

基础

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

var minxin = {
  created () {
    this.hello();
  },
  methods: {
    hello () {
      console.log('hello,我是混入中的函数');
    },
  }
}

Vue.component('my-cmp', {
  mixins: [mixin],
  template: `
    <div>xx</div>
  `
})

选项合并

当组件和混入对象含有同名选项时,这些选项会以恰当的方式进行“合并”。

合并数据,以组件数据优先:

var mixin = {
  data () {
    return {
      msg: 'hello',
    }
  }
}
new Vue({
  mixins: [mixin],
  data: {
    msg: 'goodbye',
  },
  created: function () {
    console.log(this.msg)
})

合并钩子函数,将合并为一个数组。先调用混入对象的钩子,再调用组件自身钩子。

var mixin = {
  created () {
    console.log('混入对象钩子')
  }
}

new Vue({
  el: '#app',
  mixins: [mixin],
  created () {
    console.log('组件钩子')
  }
})

合并值为对象的选项,如 methods、components 等,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

全局混入

混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。

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

new Vue({
  myOption: 'hello!'
})

谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项。

自定义指令

简介

我们可以自己写一个自定义指令去操作DOM元素,以达到代码复用的目的。注意,在 Vue 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

全局注册指令:

Vue.directive('focus', {/** */})

局部注册指令

const vm = new Vue({
  el: '#app',
  directives: {
    focus: {/** */}
  }
})

使用:

<input v-focus></input>

例如,写一个自动聚焦的输入框:

Vue.directive('focus', {
  // 当被绑定的元素插入到DOM时执行
  inserted: function (el) {
    el.focus();
  }
})

此时,在input元素上使用 v-focus 指令就可以实现自动聚焦了。

钩子函数

自定义指令对象提供了钩子函数供我们使用,这些钩子函数都为可选。

bind

只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

inserted

被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。

update

所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前

componentUpdated

指令所在组件的 VNode 及其子 VNode 全部更新后调用。

unbind

只调用一次,指令与元素解绑时调用(被绑定的Dom元素被Vue移除)。

钩子函数参数

  • el: 指令所绑定的元素,可以用来直接操作DOM。
  • binding:对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive=“1 + 1” 中,绑定值为 2。
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive=“1 + 1” 中,表达式为 “1 + 1”。
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 “foo”。
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

练习

模拟 v-show

// 绑定的值为false,display为none,值为true,display为""
Vue.directive('myshow', {
  bind (el, binding, vnode, oldVnode) {
    var display = binding.value ? '' : 'none';
    el.style.display = display;
  },
  update (el, binding, vnode, oldVnode) {
    var display = binding.value ? '' : 'none';
    el.style.display = display;
  }
})

模拟 v-model

// 1. 通过绑定的数据,给元素设置value
// 2. 当触发input事件时,去更改数据的值
// 3. 更改数据后,同步input的value值
Vue.directive('mymodel', {
  bind (el, binding, vnode) {
    const vm = vnode.context;
    const { value, expression } = binding;
    el.value = value;

    el.oninput = function (e) {
      const inputVal = el.value;
      vm[expression] = inputVal;
    }
  },
  update (el, binding) {
    const { value } = binding;
    el.value = value;
  },
})

写一个 v-slice(截取文本框)

Vue.directive('slice', {
  bind (el, binding, vnode) {
    const vm = vnode.context;
    let { value, expression, arg, modifiers } = binding;

    if(modifiers.number) {
      value = value.replace(/[^0-9]/g, '');
    }


    el.value = value.slice(0, arg);
    vm[expression] = value.slice(0, arg);

    el.oninput = function (e) {
      let inputVal = el.value;

      if(modifiers.number) {
        inputVal = inputVal.replace(/[^0-9]/g, '');
      }

      el.value = inputVal.slice(0, arg);
      vm[expression] = inputVal.slice(0, arg);
    }
  },
  update (el, binding, vnode) {
    const vm = vnode.context;
    let { value, arg, expression, modifiers } = binding;

    if(modifiers.number) {
      value = value.replace(/[^0-9]/g, '');
    }

    el.value = value.slice(0, arg);
    vm[expression] = value.slice(0, arg);
  },
})

动态指令参数

指令的参数可以是动态的。如:v-directive:[arguments]="valueargument参数可以根据组件实例数据进行更新。

重写 v-slice

Vue.directive('slice', {
  bind (el, binding, vnode) {
    const vm = vnode.context;
    let { value, expression, arg, modifiers } = binding;

    if(modifiers.number) {
      value = value.replace(/[^0-9]/g, '');
    }


    el.value = value.slice(0, arg);
    vm[expression] = value.slice(0, arg);

    el.oninput = function (e) {
      let inputVal = el.value;

      if(modifiers.number) {
        inputVal = inputVal.replace(/[^0-9]/g, '');
      }

      el.value = inputVal.slice(0, arg);
      vm[expression] = inputVal.slice(0, arg);
    }
  },
  update (el, binding, vnode) {
    const vm = vnode.context;
    let { value, arg, expression, modifiers } = binding;
    
    if(modifiers.number) {
      value = value.replace(/[^0-9]/g, '');
    }

    el.value = value.slice(0, arg);
    vm[expression] = value.slice(0, arg);

    el.oninput = function (e) {
      let inputVal = el.value;

      if(modifiers.number) {
        inputVal = inputVal.replace(/[^0-9]/g, '');
      }

      el.value = inputVal.slice(0, arg);
      vm[expression] = inputVal.slice(0, arg);
    }
  },
})

函数简写

当想在 bind 和 update 中触发相同行为,而不关心其他钩子时,可以写成函数的形式:

Vue.directive('myshow', (el, binding) => {
  const { value } = binding;
  const display = value ? '' : 'none';
  el.style.display = display;
})
Vue.directive('slice', (el, binding, vnode) => {
  const vm = vnode.context;
  let { value, expression, arg, modifiers } = binding;

  if(modifiers.number) {
    value = value.replace(/[^0-9]/g, '');
  }


  el.value = value.slice(0, arg);
  vm[expression] = value.slice(0, arg);

  el.oninput = function (e) {
    let inputVal = el.value;

    if(modifiers.number) {
      inputVal = inputVal.replace(/[^0-9]/g, '');
    }

    el.value = inputVal.slice(0, arg);
    vm[expression] = inputVal.slice(0, arg);
  }
})

对象字面量

如果自定义指令需要多个值,可以传入一个 JS 对象字面量。指令函数能够接受所有合法的 JS 表达式。

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', function (el, binding) {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text)  // => "hello!"
})
——后续还会更新。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
组件Vue.js 中最重要的概念之一。组件可以让我们将 UI 拆分为独立、可复用的部件,使得代码更加清晰、易于维护。在 Vue.js 中,组件可以分为全局组件和局部组件,其中全局组件可在任何地方使用,而局部组件只能在其父组件中使用。 定义组件时,需要使用 Vue.component() 方法,该方法需要传入两个参数:组件名称和组件配置对象。组件名称应该采用 kebab-case(短横线分隔命名)格式,以便在 HTML 中使用。 示例代码如下: ```javascript // 定义一个名为 button-counter 的新组件 Vue.component('button-counter', { data: function () { return { count: 0 } }, template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>' }) ``` 在上述代码中,我们定义了一个名为 button-counter 的组件,该组件包含一个计数器,每次点击按钮计数器加一。 在 HTML 中使用组件时,需要使用组件名称作为自定义标签来调用组件。示例代码如下: ```html <div id="app"> <button-counter></button-counter> </div> ``` 在上述代码中,我们调用了 button-counter 组件,并将其渲染到了 id 为 app 的 div 元素中。 除了组件的 data 和 template 属性外,还可以使用 props 属性来传递组件之间的数据。使用 props 时,需要在组件的配置对象中定义 props 属性,并在 HTML 中使用 v-bind 指令来传递数据。 示例代码如下: ```javascript // 定义一个名为 todo-item 的新组件 Vue.component('todo-item', { props: ['todo'], template: '<li>{{ todo.text }}</li>' }) // 创建一个 Vue 实例 var app = new Vue({ el: '#app', data: { groceryList: [ { id: 0, text: '蔬菜' }, { id: 1, text: '水果' }, { id: 2, text: '奶酪' } ] } }) ``` 在上述代码中,我们定义了一个名为 todo-item 的组件,并使用 props 属性定义了一个名为 todo 的 prop。在 HTML 中,我们使用 v-bind 指令将 groceryList 数组中的每个对象传递给了 todo-item 组件。示例代码如下: ```html <div id="app"> <ul> <todo-item v-for="item in groceryList" v-bind:todo="item" v-bind:key="item.id"></todo-item> </ul> </div> ``` 在上述代码中,我们使用 v-for 指令遍历 groceryList 数组,并使用 v-bind 指令将数组中的每个对象传递给了 todo-item 组件。注意,我们还需要使用 v-bind:key 指令来为每个列表项指定一个唯一的 key 值。 插槽是 Vue.js 中另一个重要的概念。插槽可以让父组件在子组件中插入任意的 HTML 内容,使得组件更加灵活、可复用。 在子组件中,使用 <slot> 标签来定义插槽。在父组件中,使用子组件的自定义标签来调用组件,并在标签内部插入 HTML 内容。示例代码如下: ```javascript // 定义一个名为 alert-box 的新组件 Vue.component('alert-box', { template: ` <div class="alert-box"> <strong>Error!</strong> <slot></slot> </div> ` }) // 创建一个 Vue 实例 var app = new Vue({ el: '#app' }) ``` 在上述代码中,我们定义了一个名为 alert-box 的组件,并在组件中定义了一个插槽。在 HTML 中,我们调用了 alert-box 组件,并在标签内部插入了一些 HTML 内容。示例代码如下: ```html <div id="app"> <alert-box> <p>Something bad happened.</p> </alert-box> </div> ``` 在上述代码中,我们调用了 alert-box 组件,并在标签内部插入了一些 HTML 内容。该 HTML 内容会被插入到 alert-box 组件的插槽中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值