vue3学习随便记8-属性、事件、插槽、动态组件

组件基础

我们其实前面已经看过很多组件的例子,都是用 app.component(...) 注册一个全局组件,组件的“视图”是直接用 template 定义的字符串模板。这样的组件主要用来举例和学习原理,实际工程中通常会使用单文件组件,即一个组件是单个文件定义的(在编译构建系统中常常是 .vue 文件)。

组件是自定义元素,可以像元素一样复用,复用时,每个组件都是新的实例,它的属性方法都是它自己的。

组件通常可以嵌套组织成树状。

模板中要使用组件,必须先注册。注册分为全局注册和局部注册。我们用 app.component(...) 注册的都是全局注册,全局注册的组件可以在应用中的任何组件的模板中使用。用 app.component(...) 方法注册的组件是全局组件,但创建app时在其配置 components 中定义的组件是 app 的组件,却不是全局组件(属于局部注册),因为组件在其子组件中不可用。

const app = Vue.createApp({
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

上面的代码中,可以在app中使用 ComponentA 和 ComponentB,但不能在 ComponetB 中使用 ComponentA,如果希望 ComponentB 可以使用 ComponentA,必须明确声明

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

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

通过 prop (属性)向子组件传递数据

组件的 prop 就是组件中自定义的 attribute,它是组件向外暴露的通信接口,是外部向子组件传递数据的方式。组件 prop 就是在组件配置对象的配置项 props: [...] 中定义,在组件模板中使用,然后外部像寻常的 attribute 一样绑定数据。

<html>

<head>
    <script src="vue.global.js"></script>
</head>

<body>
    <div id="app">
        <blog-post v-for="post in posts" :key="post.id" :title="post.title"></blog-post>
    </div>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    posts: [
                        {id:1, title:'Vue3 之旅'},
                        {id:2, title:'Vue3 博客'},
                        {id:3, title:'Vue3 学习之趣'}
                    ]
                }
            }
        })
        app.component('blog-post', {
            props: ['title'],
            template: `<h4>{{ title }}</h4>`
        })
        const vm = app.mount('#app')
    </script>
</body>

</html>

 监听子组件事件

子组件是自定义的元素,除了可以用 props 扩展属性(attribute),还可以用 emits 扩展事件。我们为 blog-post 组件扩展 enlarge-text事件(在该事件触发后放大所有博文的字号):先给app添加一个属性变量表示博文的字号

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

然后模板中用该变量控制博文的字号,触发 blog-post 的 enlarge-text 事件时,增加该变量的值

    <div id="app">
        <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>

我们来修改 blog-post 组件,它应该向外暴露 enlarge-text 事件,同时,扩展的事件 enlarge-text 不会自动能触发,它要么手动用代码去触发,要么转嫁到组件已有的 HTML 事件上,在 HTML事件中用代码去触发。我们修改组件模板,给组件添加一个按钮,在该按钮的 click 事件中用代码去触发 enlarge-text事件。(在JS代码中,使用camelCased驼峰写法,如大驼峰的组件名BlogPost,小驼峰的变量 enlargeText,在HTML中,使用kebab-case短横分隔写法,如blog-post,enlarge-text )

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

 上面的代码中,我们放大文本的步进量是使用组件的一方决定的,如果我们希望这个步进是组件自身决定的,那么我们在用vm的API方法$emit触发enlargeText事件时,可以同时抛出一个值,然后让使用方使用这个值。

<button @click="$emit('enlargeText', 0.1)">放大文本</button>

使用方可以通过 $event 参数访问到抛出的值

            <blog-post v-for="post in posts" :key="post.id" 
                :title="post.title" @enlarge-text="postFontSize += $event">
            </blog-post>

如果事件处理函数是一个方法,那么抛出的值会成为该方法的第一个参数

    <div id="app">
        <div :style="{ fontSize: postFontSize + 'em' }">
            <blog-post v-for="post in posts" :key="post.id" 
                :title="post.title" @enlarge-text="onEnlargeText">
            </blog-post>
        </div>
    </div>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    posts: [
                        /* ... */
                    ],
                    postFontSize: 1
                }
            },
            methods: {
                onEnlargeText(amount) {
                    this.postFontSize += amount
                }
            }
        })
        app.component('blog-post', {
            /* ... */
        })
        const vm = app.mount('#app')
    </script>

组件作为自定义的元素,也可以是自定义的输入元素,从而可以使用 v-model。首先我们要来了解 v-model 双向绑定的含义:值绑定到vm变量,input事件处理中把vm变量设置为输入元素的值

<input v-model="searchText" />

上述代码等价于

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

当在自定义的组件custom-input上使用 v-model 时,

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

等价于

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

上面的代码意味着,自定义元素(组件)必须向外暴露一个名为 modelValue 的 prop(属性),因为这是一个输入元素,所以,组件内部的 input 元素的 value 属性(attribute)应该绑定到 modelValue 属性上。自定义元素(组件)必须向外暴露一个名为 update:modelValue 的事件,组件内部 input 的 input 事件中,用vm API方法 $emit(...) 发射 update:modelValue 事件。即组件应定义如下

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

我们观察模板部分,value值绑定 和 input事件,这本就是 v-model 的含义,我们可以换一种方式,使用 计算属性 及其 getter/setter 来定义组件

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)
      }
    }
  }
})

通常计算属性是getter,对上述组件的 getter,动作路径是:使用者视图输入-> modelValue -> 内部 value,而内部value变化影响到使用者视图输入框则是 setter,动作路径是:内部value -> modelValue -> 使用者视图输入

通过slot插槽分发内容

props 是组件这个自定义元素的属性,它是变量的概念,我们有时候希望和HTML元素一样,把自定义元素的内容(元素的innerHTML)从外部传递到组件内部, 这是静态内容概念。实现内容从外到内的传递,可以使用 slot插槽,即 innerHTML  --->   <slot></slot>

我们给前面的blog-post组件插入slot,用来存放正文内容

    <div id="app">
        <div :style="{ fontSize: postFontSize + 'em' }">
            <blog-post v-for="post in posts" :key="post.id" 
                :title="post.title" @enlarge-text="onEnlargeText">
                <div>{{ post.body }}</div>
            </blog-post>
        </div>
    </div>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    posts: [
                        {id:1, title:'Vue3 之旅', body: 'Vue3 之旅正文'},
                        {id:2, title:'Vue3 博客', body: 'Vue3 博客开启新篇章'},
                        {id:3, title:'Vue3 学习之趣', body: 'Vue3 学习之趣无穷无尽'}
                    ],
                    postFontSize: 1
                }
            },
            methods: {
                onEnlargeText(amount) {
                    this.postFontSize += amount
                }
            }
        })
        app.component('blog-post', {
            props: ['title'],
            emits: ['enlargeText'],
            template: `
                <div class="blog-post">
                    <h4>{{ title }}</h4>
                    <slot></slot>
                    <button @click="$emit('enlargeText', 0.1)">放大文本</button>
                </div>
            `
        })
        const vm = app.mount('#app')
    </script>

红色部分是 slot 分发的内容

动态组件

所谓动态组件,就是一个组件容器,可以动态确定要渲染的具体组件,一般用 :is 属性指定组件名(或者一个组件的选项对象,即组件的完整配置)。下面的例子,根据用按钮选择的当前的tab名称,动态渲染对应组件

    <div id="app">
        <button v-for="tab in tabs" :key="tab.name"
            :class="['tab-button', {active: currentTab === tab.name}]"
            @click="currentTab = tab.name">
            {{ tab.text }}
        </button>
        <component :is="currentTabComponent" class="tab"></component>
    </div>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    currentTab: 'Home',
                    tabs: [
                        {name:'Home', text:'首页'},
                        {name:'Posts', text:'帖子'},
                        {name:'Archive', text:'归档'}
                    ]
                }
            },
            computed: {
                currentTabComponent() {
                    return 'tab-' + this.currentTab.toLowerCase()
                }
            }
        })
        app.component('tab-home', {
            template: `<div class="demo-post">首页内容</div>`
        })
        app.component('tab-posts', {
            template: `<div class="demo-post">帖子……</div>`
        })
        app.component('tab-archive', {
            template: `<div class="demo-post">(归档)</div>`
        })
        const vm = app.mount('#app')
    </script>

Vue解析DOM模板的注意事项

如果想在 DOM 中直接书写 Vue 模板, Vue 就不得不从 DOM 中获取字符串,这时,会因为原生 HTML的解析行为带来一些小问题。这句话的意思是,我们在以下情况使用组件时,组件都是一个独立的整体:

  • 注册组件时使用字符串模板(例如 template: `...`)
  • 使用单文件组件
  • 使用 <script type="text/x-template">定义的组件

Vue 解析模板没有问题。而原生的 HTML DOM 和 Vue模板混在一起, Vue解析时势必要从混合物中捞出Vue模板,这样,Vue模板必须迁就原生HTML的一些特性。哪些特性呢?

元素位置受限

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

<table>内部允许的元素是有限制的,这样,上述代码中的<blog-post-row>会被认为是无效的内容提升到外部,导致渲染结果是错误的。这种情况,可以使用 is 属性来变通:

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

这里is的值必须用 vue: 开头,表示这个不是HTML原生的自定义元素,而是Vue组件。

和这个例子类似的元素还有 <ul>、<ol>、<select>,而 <li>、<tr>、<option>只能出现在特定元素内部。

大小写不敏感

HTML 属性名不区分大小写的,从而浏览器将所有大写字母解释为小写,这个问题带来的一点就是JS代码中组件名称之类的是有大小写驼峰写法,驼峰写法的组件名不能直接在HTML中使用(TabA组件和taba组件会被认为是相同组件),解决办法我们前面已经看到过了,就是HTML中用驼峰写法等价的kebab-cased(短横分隔)写法(TabA对应tab-a)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值