Vue 官方笔记--组件

中文版(英文浓缩)

组件是什么?

组件:具有预定义选项实例
组件是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用
通过 prop ,我们可以实现父子组件之间的解耦,有了属性,子组件中就可以使用更为复杂的模板和逻辑,比如如下

<!-- 复制到 html 中可以直接运行哦,看看是什么结果吧 -->
<div id="main">
    <ol>	
        <!-- 子组件 -->
        <todo-item v-for="todo in groceryList" 
                   :key="todo.id" 
                   :todo="todo">
        </todo-item>
    </ol>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
    const TodoList = {
        data() {
            return {
                groceryList: [
                    { id: 0, text: 'Vegetables' },
                    { id: 1, text: 'Cheese' },
                    { id: 2, text: 'Meat' }
                ]
            }
        }
    }
    const app = Vue.createApp(TodoList)
    // 父组件       
    // 试想,如果没有定义在父组件中定义 todo 这个属性,该怎样实现同样的功能呢? 不好实现呀
    app.component('todo-item', {
        props: ['todo'],
        template: `<li>{{ todo.text }}</li>`
    })
    app.mount('#main')
</script>

创建应用实例

通过 createApp 函数创建创建了应用实例(application instance),它表明 Vue 应用的开始

这个应用实例未来会通过 mount 函数变成根组件实例,这就是应用实例最终的命运吗?有点科幻色彩~

const app = Vue.createApp(RootComponent)

这个应用实例用来注册一个全局信息,在整个 Vue 应用中的所有组件都可以使用这个全局信息

应用实例相当于一个进程,而组件就相当于一个线程,线程之间可以相互合作,并共享进程的信息

根组件

const RootComponentConfig = { 
    // data() 函数是用于配置根组件的其中之一的选项,此外还有 methods()、computed() 等
    data() {
        return {
            a: 123
        }
    }
}
// RootComponentConfig 用于配置根组件实例,虽然根组件是调用 mount() 才返回的,但是就好像提前占一个坑,预定一样 
const applicationInstance = Vue.createApp(RootComponentConfig)
const rootComponentInstance = applicationInstance.mount('#app')

当我们为我们的应用实例使用 mount 方法(即挂载)时,表明这个应用实例(此时应该是根组件了)被用作渲染的起点

应用实例调用方法(除了mount 方法)后,还是会返回一个应用实例(比如 app 是一个应用实例,调用 app.component( /* xxx */ ) 之后,还是会返回一个应用实例)

但是,mount 方法不会返回应用实例,而是会返回根组件实例

应用实例最终需要挂载到一个 DOM 元素中() 如<div id="app"></div>,于是我们给 mount 传递 #app

mount 内部是这样实现的

const res = document.querySelector('#app')
return res

每个组件都会有自己的组件实例,比如

// 会有一个组件实例
app.component('haha', {
  template: `
    <h1>
      haha
    </h1>`
})

在应用中的组件实例都会共享根组件实例

我们可以在组件实例中添加用户自定义的属性,如methods, props, computed, inject and setup

这些所有的自定义属性都可以在模板中使用,比如

<!-- 可复制直接运行 -->
<div id="main">
    <!-- 在模板中使用了组件实例中的 data() 属性 -->
    {{ a }}
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                a: 123
            }
        },
    })
    app.mount('#main')
</script>

英文原版

看英文版是真的过瘾,比中文版更好理解

what is the Component?

component is an abstraction that allows us to build large-scale applications composed of small, self-contained, and often reusable components.

component :instance with pre-defined options

In Vue, a component is essentially an ❤️instance with pre-defined options. Registering a component in Vue is straightforward

// Create Vue application
const app = Vue.createApp(...)

// Define a new component called todo-item
app.component('todo-item', {
  template: `<li>This is a todo</li>`
})

// Mount Vue application
app.mount(...)				// mount will returns the root component instance

But this would render the same text for every todo, which is not super interesting. We should be able to pass data from the parent scope into child components. Let’s modify the component definition to make it accept a prop

app.component('todo-item', {
  props: ['todo'],
  template: `<li>{{ todo.text }}</li>`
})

we have managed to separate our app into two smaller units, and the child is reasonably well-decoupled from the parent via the ⭐️props interface. We can now further improve our <todo-item> component with more complex template and logic without affecting the parent app.

In a large application, it is necessary to divide the whole app into components to make development manageable. We will talk a lot more about components later in the guide, but here’s an (imaginary) example of what an app’s template might look like with components:

<div id="app">
  <app-nav></app-nav>
  <app-view>
    <app-sidebar></app-sidebar>
    <app-content></app-content>
  </app-view>
</div>

Creating an Application Instance

Every Vue application starts by creating a new application instance with the createApp function:

// app is a new application instance
const app = Vue.createApp({	
  /* options */
})

The application instance is used to register ‘globals’ that can then be used by components within that application. We’ll discuss that in detail later in the guide but as a quick example:

const app = Vue.createApp({})
// application instance is used by components
app.component('SearchInput', SearchInputComponent)
app.directive('focus', FocusDirective)
app.use(LocalePlugin)

Most of the methods exposed by the application instance return that same instance (chain)

The Root Component

The options passed to createApp are used to configure the root component.

That component is used as the ​ 🔰 starting ​point for rendering when we mount the application.

An application needs to be mounted into a DOM element. For example, if we want to mount a Vue application into <div id="app"></div>, we should pass #app

Unlike most of the application methods, mount does not return the application. Instead it ⭐️returns the root component instance.

Although not strictly associated with the MVVM pattern (opens new window), Vue’s design was partly inspired by it.

As a convention, we often use the variable vm (short for ViewModel) to refer to a ⭐️component instance.

While all the examples on this page only need a single component, most real applications are organized into a tree of nested, reusable components. For example, a Todo application’s component tree might look like this:

Root Component
└─ TodoList
   ├─ TodoItem
   │  ├─ DeleteTodoButton
   │  └─ EditTodoButton
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

Each component will have its own component instance, vm. For some components, such as TodoItem, there will likely be multiple instances rendered at any one time. All of the component instances in this application will share the same application instance.

We’ll talk about the component system in detail later. For now, just be aware that the root component isn’t really any different from any other component. The configuration options are the same, as is the behavior of the corresponding component instance


difference between an instance and an obejct ?

Once you instantiate a class (using new), that instantiated thing becomes an object.

Instance refers to the copy of the object at a particular time whereas object refers to the memory address of the class.

an object represents a set of instances while an instance is a certain, specific representation.

Component Instance Properties

Earlier in the guide we met data properties. Properties defined in data are exposed via the component instance:

const app = Vue.createApp({
  data() {
    return { count: 4 }
  }
})

const vm = app.mount('#app')

console.log(vm.count) // => 4

There are various other component options that add user-defined properties to the component instance

such as methods, props, computed, inject and setup.

All of the properties of the component instance, no matter how they are defined, will be accessible in the component's template.

Vue also exposes some built-in properties via the component instance, such as $attrs and $emit.

These properties all have a $ prefix to avoid conflicting with user-defined property names

Lifecycle Hooks

Each component instance goes through a series of initialization steps when it’s created

for example, it needs to set up data observation, compile the template, mount the instance to the DOM, and update the DOM when data changes.

Along the way, it also runs functions called lifecycle hooks, giving users the opportunity to add their own code at specific stages.

For example, the created hook can be used to run code after an instance is created:

Vue.createApp({
  data() {
    return { count: 1 }
  },
  created() {
    // `this` points to the vm instance
    console.log('count is: ' + this.count) // => "count is: 1"
  }
})

There are also other hooks which will be called at different stages of the instance’s lifecycle, such as mounted, updated, and unmounted. All lifecycle hooks are called with their this context pointing to the current active instance invoking it.

TIP

Don’t use arrow functions (opens new window)on an options property or callback, such as created: () => console.log(this.a) or vm.$watch('a', newValue => this.myMethod()). Since an arrow function doesn’t have a this, this will be treated as any other variable and lexically looked up through parent scopes until found, often resulting in errors such as Uncaught TypeError: Cannot read property of undefined or Uncaught TypeError: this.myMethod is not a function.

Components Basics

Base Example

Components are reusable instances with a name

We can use component as a custom element inside a root instance:

Since components are reusable instances, they accept the same options as a root instance, such as data, computed, watch, methods, and lifecycle hooks

Reusing Components

Components can be reused as many times as you want

⭐️ each time you use a component, a new instance of it is created.(separate )

Organizing Components

To use these components in templates, they must be registered so that Vue knows about them. There are two types of component registration: global and local

Globally registered

Globally registered components can be used in the template of any component within the app.

const app = Vue.createApp({})

app.component('my-component-name', {
  // ... options ...
})
Local Registration
const app = Vue.createApp({
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

locally registered components are not also available in subcomponents.

if you wanted ComponentA to be available in ComponentB, you’d have to use:

const ComponentA = {
  /* ... */
}

const ComponentB = {
  components: {
    'component-a': ComponentA
  }
  // ...
}

Local Registration in a Module System

If you’re still here, then it’s likely you’re using a module system, such as with Babel and Webpack. In these cases, we recommend creating a components directory, with each component in its own file.

Then you’ll need to import each component you’d like to use, before you locally register it. For example, in a hypothetical ComponentB.js or ComponentB.vue file:

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  }
  // ...
}

Now both ComponentA and ComponentC can be used inside ComponentB's template.

Passing Data to Child Components with Props

component won’t be useful unless you can pass data to it, such as the title and content of the specific post we want to display. That’s where props come in.

Props are custom attributes you can register on a component. To pass a title to our blog post component, we can include it in the list of props this component accepts, using the props option:

const app = Vue.createApp({})

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-post-demo')

When a value is passed to a prop attribute, it becomes a property on that component instance. The value of that property is accessible within the template, just like any other component property.

A component can have as many props as you like and, by default, any value can be passed to any prop.

Once a prop is registered, you can pass data to it as a custom attribute, like this:

<div id="blog-post-demo" class="demo">
  <blog-post title="My journey with Vue"></blog-post>
  <blog-post title="Blogging with Vue"></blog-post>
  <blog-post title="Why Vue is so fun"></blog-post>
</div>

we can use v-bind to dynamically pass props.

This is especially useful when you don’t know the exact content you’re going to render ahead of time.

Prop Types

you’ll want every prop to be a specific type of value. In these cases, you can list props as an object,

where the properties’ names and values contain(分别包含) the prop names and types, respectively:

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

Passing Static or Dynamic Props

So far, you’ve seen props passed a static value, like in:

<blog-post title="My journey with Vue"></blog-post>

You’ve also seen props assigned dynamically with v-bind or its shortcut, the : character, such as in:

<!-- Dynamically assign the value of a variable -->
<blog-post :title="post.title"></blog-post>

<!-- Dynamically assign the value of a complex expression -->
<blog-post :title="post.title + ' by ' + post.author.name"></blog-post>

In the two examples above, we happen to pass string values, but any type of value can actually be passed to a prop.

Passing a Number

<!-- Even though `42` is static, we need v-bind to tell Vue that -->
<!-- this is a JavaScript expression rather than a string.       -->
<blog-post :likes="42"></blog-post>

<!-- Dynamically assign to the value of a variable. -->
<blog-post :likes="post.likes"></blog-post>

Passing a Boolean

<!-- Including the prop with no value will imply `true`. -->
<blog-post is-published></blog-post>

<!-- Even though `false` is static, we need v-bind to tell Vue that -->
<!-- this is a JavaScript expression rather than a string.          -->
<blog-post :is-published="false"></blog-post>

<!-- Dynamically assign to the value of a variable. -->
<blog-post :is-published="post.isPublished"></blog-post>

Passing an Array

<!-- Even though the array is static, we need v-bind to tell Vue that -->
<!-- this is a JavaScript expression rather than a string.            -->
<blog-post :comment-ids="[234, 266, 273]"></blog-post>

<!-- Dynamically assign to the value of a variable. -->
<blog-post :comment-ids="post.commentIds"></blog-post>

Passing an Object

<!-- Even though the object is static, we need v-bind to tell Vue that -->
<!-- this is a JavaScript expression rather than a string.             -->
<blog-post
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- Dynamically assign to the value of a variable. -->
<blog-post :author="post.author"></blog-post>

Passing the Properties of an Object

If you want to pass all the properties of an object as props, you can use v-bind without an argument (v-bind instead of :prop-name). For example, given a post object:

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

The following template:

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

Will be equivalent to:

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

One-Way Data Flow

All props form a one-way-down binding between the child property and the parent one:

when the parent property updates, it will flow down to the child, but not the other way around.

In addition, every time the parent component is updated, all props in the child component will be refreshed with the latest value.

This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console.

There are usually two cases where it’s tempting to mutate a prop:

  1. The prop is used to pass in an initial value; the child component wants to use it as a local data property afterwards. In this case, it’s best to define a local data property that uses the prop as its initial value:
props: ['initialCounter'],
data() {
  return {
    counter: this.initialCounter
  }
}
<div id="main">
    <my-component count='0'></my-component>
    <my-component count='1'></my-component>
    <my-component count='2'></my-component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
    const app = Vue.createApp({})
    app.component('myComponent', {
        props: ['count'],
        data() {
            return {
                aCount: this.count
            }
        },
        template: `
        <button @click="aCount++">add</button>
        {{aCount}} <br/><br/>
        `
    })
    app.mount('#main')
</script>
  1. The prop is passed in as a raw value that needs to be transformed. In this case, it’s best to define a computed property using the prop’s value:
props: ['size'],
computed: {
  normalizedSize() {
    return this.size.trim().toLowerCase()
  }
}
<div id="main">
    <my-component text='ASD'></my-component>
    <my-component text='QWE'></my-component>
    <my-component text='ZXC'></my-component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
    const app = Vue.createApp({})
    app.component('myComponent', {
        props: ['text'],
        computed:{
            changeCount(){
                return this.text.trim().toLowerCase()
            }
        },
        template: `
        <div>{{changeCount}} </div>
        <br/><br/>
        `
    })
    app.mount('#main')
</script>

Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state.

<div id="main">
    <my-component :haha="['dwa','sdaw','daw']"></my-component>
    <my-component :haha="['dwa','sdaw','daw']"></my-component>
    <my-component :haha="['dwa','sdaw','daw']"></my-component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
    const app = Vue.createApp({})
    app.component('myComponent', {
        props: ['haha'],
        methods:{
            pushHaha(){
                console.log(this.haha);
                this.haha.push("asd")
                // this.haha will change
                console.log(this.haha);
            }
        },
        template: `
        <button @click="pushHaha()">pushHaha</button>
        this will not change: {{haha}}
        <br/><br/>
        `
    })
    app.mount('#main')
</script>

Prop Validation

Components can specify requirements for their props, such as the types you’ve already seen. If a requirement isn’t met, Vue will warn you in the browser’s JavaScript console. This is especially useful when developing a component that’s intended to be used by others.

To specify prop validations, you can provide an object with validation requirements to the value of props, instead of an array of strings. For example:

app.component('my-component', {
    props: {
        // Basic type check (`null` and `undefined` values will pass any type validation)
        propA: Number,
        // Multiple possible types
        propB: [String, Number],
        // Required string
        propC: {
            type: String,
            required: true
        },
        // Number with a default value
        propD: {
            type: Number,
            default: 100
        },
        // Object with a default value
        propE: {
            type: Object,
            // Object or array defaults must be returned from
            // a factory function
            default() {
                return { message: 'hello' }
            }
        },
        // Custom validator function
        propF: {
            validator(value) {
                // The value must match one of these strings
                return ['success', 'warning', 'danger'].includes(value)
            }
        },
        // Function with a default value
        propG: {
            type: Function,
            // Unlike object or array default, this is not a factory function - this is a function to serve as a default value
            default() {
                return 'Default function'
            }
        }
    }
})

In JavaScript, any function can return an object.

But if without the new keyword, it’s a factory function.

Prop Casing (camelCase vs kebab-case)

HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you’re using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents:

const app = Vue.createApp({})

app.component('blog-post', {
  // camelCase in JavaScript
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
<!-- kebab-case in HTML -->
<blog-post post-title="hello!"></blog-post>

if you’re using string templates, this limitation does not apply.

字符串模板就是写在vue中的template中定义的模板,如.vue的单文件组件模板和定义组件时template属性值的模板。

字符串模板不会在页面初始化参与页面的渲染,会被vue进行解析编译之后再被浏览器渲染,所以不受限于html结构和标签的命名。

Non-Prop Attributes

A component non-prop attribute is an attribute or event listener that is passed to a component, but does not have a corresponding property defined in props or emits. Common examples of this include class, style, and id attributes. You can access those attributes via $attrs property.

Attribute Inheritance

When a component returns a single root node, non-prop attributes will automatically be added to the root node's attributes.

For example, in the instance of a date-picker component:

app.component('date-picker', {
  template: `
    <div class="date-picker">
      <input type="datetime-local" />
    </div>
  `
})

In the event we need to define the status of the date-picker component via a data-status property, it will be applied to the root node (i.e., div.date-picker).

<!-- Date-picker component with a non-prop attribute -->
<date-picker data-status="activated"></date-picker>

<!-- Rendered date-picker component -->
<div class="date-picker" data-status="activated">
  <input type="datetime-local" />
</div>

Same rule applies to the event listeners:

<date-picker @change="submitChange"></date-picker>
app.component('date-picker', {
  created() {
    console.log(this.$attrs) // { onChange: () => {}  }
  }
})

@xxx => onXxx

This might be helpful when we have an HTML element with change event as a root element of date-picker.

app.component('date-picker', {
  template: `
    <select>
      <option value="1">Yesterday</option>
      <option value="2">Today</option>
      <option value="3">Tomorrow</option>
    </select>
  `
})

In this case, change event listener is passed from the parent component to the child and it will be triggered on native <select> change event. We won’t need to emit an event from the date-picker explicitly:

<div id="date-picker" class="demo">
  <date-picker @change="showChange"></date-picker>
</div>
const app = Vue.createApp({
  methods: {
    showChange(event) {
      console.log(event.target.value) // will log a value of the selected option
    }
  }
})
Disabling Attribute Inheritance

If you do not want a component to automatically inherit attributes, you can set inheritAttrs: false in the component’s options.

The common scenario for disabling an attribute inheritance is when attributes need to be applied to other elements besides the root node.

By setting the inheritAttrs option to false, you can control to apply to other elements attributes to use the component’s $attrs property, which includes all attributes not included to component props and emits properties (e.g., class, style, v-on listeners, etc.).

Using our date-picker component example from the previous section, in the event we need to apply all non-prop attributes to the input element rather than the root div element, this can be accomplished by using the v-bind shortcut.

app.component('date-picker', {
  inheritAttrs: false,
  template: `
    <div class="date-picker">
      <input type="datetime-local" v-bind="$attrs" />
    </div>
  `
})

With this new configuration, our data-status attribute will be applied to our input element!

<!-- Date-picker component with a non-prop attribute -->
<date-picker data-status="activated"></date-picker>

<!-- Rendered date-picker component -->
<div class="date-picker">
  <input type="datetime-local" data-status="activated" />
</div>
Attribute Inheritance on Multiple Root Nodes

Unlike single root node components, components with multiple root nodes do not have an automatic attribute fallthrough behavior.

If $attrs are not bound explicitly, a runtime warning will be issued.

<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// This will raise a warning
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  `
})

// No warnings, $attrs are passed to <main> element
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main v-bind="$attrs">...</main>
    <footer>...</footer>
  `
})

Listening to Child Components Events

As we develop our <blog-post> component, some features may require communicating back up to the parent. For example, we may decide to include an accessibility feature to enlarge the text of blog posts, while leaving the rest of the page its default size.

In the parent, we can support this feature by adding a postFontSize data property:

const App = {
    data() {
        return {
            posts: [
                /* ... */
            ],
            postFontSize: 1
        }
    }
}

Which can be used in the template to control the font size of all blog posts:

<div id="blog-posts-events-demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
      v-for="post in posts"
      :key="post.id"
      :title="post.title"
    ></blog-post>
  </div>
</div>

Now let’s add a button to enlarge the text right before the content of every post:

app.component('blog-post', {
  props: ['title'],
  template: `
    <div class="blog-post">
      <h4>{{ title }}</h4>
      <button>
        Enlarge text
      </button>
    </div>
  `
})

The problem is, this button doesn’t do anything:

<button>
    Enlarge text
</button>

When we click on the button, we need to communicate to the parent that it should enlarge the text of all posts. To solve this problem, component instances provide a custom events system. The parent can choose to listen to any event on the child component instance with v-on or @, just as we would with a native DOM event:

<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>

Then the child component can emit an event on itself by calling the built-in $emit method, passing the name of the event:

<button @click="$emit('enlargeText')">
    Enlarge text
</button>

Thanks to the @enlarge-text="postFontSize += 0.1" listener, the parent will receive the event and update the value of postFontSize.

<div id="blog-posts-events-demo" class="demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
       v-for="post in posts"
       :key="post.id"
       :title="post.title"
       @enlarge-text="postFontSize += 0.1"
    ></blog-post>
  </div>
</div>
const app = Vue.createApp({
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue'},
        { id: 2, title: 'Blogging with Vue'},
        { id: 3, title: 'Why Vue is so fun'}
      ],
      postFontSize: 1
    }
  }
})

app.component('blog-post', {
  props: ['title'],
  template: `
    <div class="blog-post">
      <h4>{{ title }}</h4>
      <button @click="$emit('enlargeText')">
        Enlarge text
      </button>
    </div>
  `
})

app.mount('#blog-posts-events-demo')

We can list emitted events in the component’s emits option:

app.component('blog-post', {
    props: ['title'],
    emits: ['enlargeText']
})

This will allow you to check all the events that a component emits and optionally validate them.

When a native event (e.g., click) is defined in the emits option, the component event will be used instead of a native event listener.

Validate Emitted Events

Similar to prop type validation, an emitted event can be validated if it is defined with the Object syntax instead of the array syntax.

To add validation, the event is assigned a function that receives the arguments passed to the $emit call and returns a boolean to indicate whether the event is valid or not.

app.component('custom-form', {
    emits: {
        // No validation
        click: null,

        // Validate submit event
        submit: ({ email, password }) => {
            if (email && password) {
                return true
            } else {
                console.warn('Invalid submit event payload!')
                return false
            }
        }
    },
    methods: {
        submitForm(email, password) {
            this.$emit('submit', { email, password })
        }
    }
})

Emitting a Value With an Event

It’s sometimes useful to emit a specific value with an event. For example, we may want the <blog-post> component to be in charge of how much to enlarge the text by. In those cases, we can pass a second parameter to $emit to provide this value:

<button @click="$emit('enlargeText', 0.1)">
  Enlarge text
</button>

Then when we listen to the event in the parent, we can access the emitted event’s value with $event:

<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>

Or, if the event handler is a method:

<blog-post ... @enlarge-text="onEnlargeText"></blog-post>

Then the value will be passed as the first parameter of that method:

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

Using v-model on Components

Custom events can also be used to create custom inputs that work with v-model. Remember that:

<input v-model="searchText" />

does the same thing as:

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

When used on a component, v-model instead does this:

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

WARNING

Please note we used model-value with kebab-case here because we are working with in-DOM templates. You can find a detailed explanation on kebab-cased vs camelCased attributes in the DOM Template Parsing Caveats section

For this to actually work though, the <input> inside the component must:

  • Bind the value attribute to the modelValue prop
  • On input, emit an update:modelValue event with the new value

Here’s that in action:

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
})
v-model arguments

We can modify these names passing an argument to v-model:

<my-component v-model:title="bookTitle"></my-component>

In this case, child component will expect a title prop and emits update:title event to sync:

app.component('my-component', {
  props: {
    title: String
  },
  emits: ['update:title'],
  template: `
    <input
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)">
  `
})

Now v-model should work perfectly with this component:

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

Another way of implementing v-model within this component is to use the ability of computed properties to define a getter and setter. The get method should return the modelValue property and the set method should emit the corresponding event:

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input v-model="value">
  `,
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
})

That’s all you need to know about custom component events for now, but once you’ve finished reading this page and feel comfortable with its content, we recommend coming back later to read the full guide on Custom Events.

Handling v-model modifiers
<!-- 复制可直接运行 -->
<div id="app">
    <!-- For v-model bindings with arguments(no arguments:modelValue), the generated prop name will be arg + "Modifiers" -->
    <my-component v-model:haha.capitalize="myText"></my-component>
    {{myText}}
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                myText: '',
            }
        }
    })

    app.component('my-component', {
        props: {
            // ① 定义 haha
            haha: String,
            hahaModifiers: {
                default: () => ({})
            },
        },
        // ④  haha
        emits: ['update:haha'],
        methods: {
            emitValue(e) {
                let value = e.target.value
                if (this.hahaModifiers.capitalize) {
                    value = value.charAt(0).toUpperCase() + value.slice(1)
                }
                // ③ haha
                this.$emit('update:haha', value)
            }
        },
        created() {
            // haha + Modifiers
            console.log(this.hahaModifiers) // { capitalize: true }
        },
        // ② haha
        template: `<input
                    type="text"
                    :value="haha"
                    @input="emitValue">`
    })

    app.mount('#app')
</script>
DOM Template Parsing Caveats

If you are writing your Vue templates directly in the DOM, Vue will have to retrieve the template string from the DOM

Element Placement Restrictions

Some HTML elements, such as <ul>, <ol>, <table> and <select> have restrictions on what elements can appear inside them, and some elements such as <li>, <tr>, and <option> can only appear inside certain other elements.

This will lead to issues when using components with elements that have such restrictions. For example:

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

The custom component <blog-post-row> will be hoisted out as invalid content, causing errors in the eventual rendered output.

We can use the special is attribute]as a workaround

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

When the is attribute is used on a native HTML element, it will be interpreted as a Customized built-in element , which is a native web platform feature.

TIP

When used on native HTML elements, the value of is must be prefixed with vue: in order to be interpreted as a Vue component. This is required to avoid confusion with native customized built-in elements (opens new window).

Case Insensitivity

HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you’re using in-DOM templates, camelCased prop names and event handler parameters need to use their kebab-cased (hyphen-delimited) equivalents:

// camelCase in JavaScript

app.component('blog-post', {
  props: ['postTitle'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
})
<!-- kebab-case in HTML -->

<blog-post post-title="hello!"></blog-post>

Content Distribution with Slots

Just like with HTML elements, it’s often useful to be able to pass content to a component, like this:

<alert-box>
  Something bad happened.
</alert-box>

This can be achieved using Vue’s custom <slot> element:

app.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

As you’ll see above, we use the <slot> as a placeholder where we want the content to go – and that’s it. We’re done!

Slots

If <todo-button>'s template did not contain a <slot> element,

any content provided between its opening and closing tag would be discarded.

Render Scope

When you want to use data inside a slot, such as in:

<todo-button>
  Delete a {{ item.name }}
</todo-button>

That slot has access to the same instance properties (i.e. the same “scope”) as the rest of the template.

But trying to access action would not work:

<todo-button action="delete">
  Clicking here will {{ action }} an item
  <!--
  The `action` will be undefined, because this content is passed
  _to_ <todo-button>, rather than defined _inside_ the
  <todo-button> component.
  -->
</todo-button>

Everything in the parent template is compiled in parent scope; everything in the child template is compiled in the child scope

Fallback Content

We might want the text “Submit” to be rendered inside the <button> most of the time. To make “Submit” the fallback content, we can place it in between the <slot> tags:

<button type="submit">
  <slot>Submit</slot>
</button>

But if we provide content:

<submit-button>
  Save
</submit-button>

Then the provided content will be rendered instead:

<button type="submit">
  Save
</button>
Named Slots

the <slot> element has a special attribute, name, which can be used to assign a unique ID to different slots so you can determine where content should be rendered: in a <base-layout> component

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

A <slot> outlet without name implicitly has the name “default”.

To provide content to named slots, we need to use the v-slot directive on a <template> element, providing the name of the slot as v-slot's argument:order is not importance when use the v-slot on a template element

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

The rendered HTML will be:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

Note that v-slot can only be added to a <template> (with one exception)

Scoped Slots

Sometimes, it’s useful for slot content to have access to data only available in the child component. It’s a common case when a component is used to render an array of items, and we want to be able to customize the way each item is rendered.

For example, we have a component, containing a list of todo-items.

app.component('todo-list', {
  data() {
    return {
      items: ['Feed a cat', 'Buy milk']
    }
  },
  template: `
    <ul>
      <li v-for="(item, index) in items">
        {{ item }}
      </li>
    </ul>
  `
})

We might want to replace the {{ item }} with a <slot> to customize it on parent component:

<todo-list>
    <i class="fas fa-check"></i>
    <span class="green">{{ item }}</span>
</todo-list>

That won’t work, because we are providing the slot content from its parent.

To make item available to the slot content provided by the parent, we can add a <slot> element and bind it as an attribute:

<ul>
  <li v-for="( item, index ) in items">
    <slot :item="item"></slot>
  </li>
</ul>

You can bind as many attributes to the slot as you need:

<ul>
  <li v-for="( item, index ) in items">
    <slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
  </li>
</ul>

Attributes bound to a <slot> element are called slot props. Now, in the parent scope, we can use v-slot with a value to define a name for the slot props we’ve been provided:

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>
</todo-list>

In this example, we’ve chosen to name the object containing all our slot props slotProps, but you can use any name you like

<!-- 复制可直接运行 -->
<div id="app">

    <todo>
        <!-- { "aSlotProp": { "name": "OK" } } -->
        <template v-slot:default="allSlotProps">
            {{allSlotProps.aSlotProp.name}}
        </template>
    </todo>

    <!-- Abbreviated default  -->
    <todo v-slot="allSlotProps">
        {{allSlotProps.aSlotProp.name}}
    </todo>

    <!-- Named Slots Shorthand -->
    <todo #default="allSlotProps">
        {{allSlotProps.aSlotProp.name}}
    </todo>

    <!-- Named Slots Other -->
    <todo #other="allSlotProps"></todo>

    <!-- Destructuring Slot Props -->
    <todo #default="{ aSlotProp }">
        {{aSlotProp.name}}
    </todo>


</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
    const app = Vue.createApp({
    })

    app.component('todo', {
        data() {
            return {
                item: {
                    name: 'OK'
                }
            }
        },
        template: `
<button class="btn-primary">
<slot :aSlotProp="item">haha</slot>
    </button>
`
    })
    app.mount('#app')
</script>
Abbreviated Syntax for Lone Default Slots

In cases like above, when only the default slot is provided content, the component’s tags can be used as the slot’s template. This allows us to use v-slot directly on the component:

<todo-list v-slot:default="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>

This can be shortened even further. Just as non-specified content is assumed to be for the default slot, v-slot without an argument is assumed to refer to the default slot:

<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>

Note that the abbreviated syntax for default slot cannot be mixed with named slots, as it would lead to scope ambiguity:

<!-- INVALID, will result in warning -->
<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>

  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</todo-list>

Whenever there are multiple slots, use the full <template> based syntax for all slots:

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</todo-list>
Destructuring Slot Props

Internally, scoped slots work by wrapping your slot content in a function passed a single argument:

function (slotProps) {
  // ... slot content ...
}

That means the value of v-slot can actually accept any valid JavaScript expression that can appear in the argument position of a function definition. So you can also use ES2015 destructuring (opens new window)to pull out specific slot props, like so:

<todo-list v-slot="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

This can make the template much cleaner, especially when the slot provides many props. It also opens other possibilities, such as renaming props, e.g. item to todo:

<todo-list v-slot="{ item: todo }">
  <i class="fas fa-check"></i>
  <span class="green">{{ todo }}</span>
</todo-list>

You can even define fallbacks, to be used in case a slot prop is undefined:

<todo-list v-slot="{ item = 'Placeholder' }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>
Dynamic Slot Names

Dynamic directive arguments also work on v-slot, allowing the definition of dynamic slot names:

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

Similar to v-on and v-bind, v-slot also has a shorthand, replacing everything before the argument (v-slot:) with the special symbol #. For example, v-slot:header can be rewritten as #header:

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

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

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

However, just as with other directives, the shorthand is only available when an argument is provided. That means the following syntax is invalid:

<!-- This will trigger a warning -->

<todo-list #="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

Instead, you must always specify the name of the slot if you wish to use the shorthand:

<todo-list #default="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

Dynamic Components

Sometimes, it’s useful to dynamically switch between components, like in a tabbed interface:

The above is made possible by Vue’s <component> element with the special is attribute:

<!-- 复制可直接运行 -->
<div id="dynamic-component-demo" class="demo">
    <button v-for="tab in tabs" :key="tab" @click="currentTab = tab">
        {{ tab }}
    </button>
    <component :is="currentTab" class="tab"></component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                currentTab: 'Home',
                tabs: ['Home', 'Posts', 'Archive']
            }
        },
    })
    app.component('Home', {
        template: `<div>Home component</div>`
    })
    app.component('Posts', {
        template: `<div>Posts component</div>`
    })
    app.component('Archive', {
        template: `<div>Archive component</div>`
    })

    app.mount('#dynamic-component-demo')
</script>
<!-- Component changes when currentTab changes -->
<component :is="currentTab"></component>

In the example above, currentTabComponent can contain either:

  • the name of a registered component, or
  • a component’s options object

You can also use the is attribute to create regular HTML elements.

Dynamic & Async Components
Dynamic Components with keep-alive

each time you switch to a new tab, Vue creates a new instance

Recreating dynamic components is normally useful behavior, but in this case, we’d really like those tab component instances to be cached once they’re created for the first time.

To solve this problem, we can wrap our dynamic component with a <keep-alive> element

<!-- Inactive components will be cached! -->
<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值