vue学习笔记3


title: vue3学习笔记(三)
date: 2023-03-25 13:40:50
tags:
- vue
- js
top_img: false
urlname: vue3学习笔记(三)


组件注册

全局注册

我们可以使用 Vue 应用实例的 app.component() 方法,让组件在当前 Vue 应用中全局可用。

全局注册的组件可以在此应用的任意组件的模板中使用:

import { createApp } from 'vue'

const app = createApp({})

app.component(
  // 注册的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)

单文件

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

多文件

app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)

局部注册

全局注册虽然很方便,但有以下几个问题:

  1. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。

相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。

请注意:局部注册的组件在后代组件中并不可用。在这个例子中,ComponentA 注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用。

组件名格式

在整个指引中,我们都使用 PascalCase 作为组件名的注册格式,这是因为:

  1. PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
  2. `` 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来。

在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase 的标签名在 DOM 模板中是不可用的,详情参见 DOM 模板解析注意事项

为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent 为名注册的组件,在模板中可以通过 引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。

<script>
import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA
  }
}
</script>

<template>
  <ComponentA />
</template>

Props

Props 声明

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute

props 需要使用props选项来定义:

<template>
    <h2>props使用</h2>
    <h2>{{ age }}</h2>
    <h2>{{ message }}</h2>
</template>

<script>
export default {
    props: {
        message: {
            // 类型
            type: String,
            // 默认值
            default: '你好'
        }
    },
    data() {
        return {
            age: 10
        }
    }
}
</script>

传递 prop 的细节

Prop 名字格式

如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。

静态 动态 Prop

<!-- 父组件 -->
<template>
    <div>
        <!-- 根据一个变量的值静态传入 -->
        <BlogPost greetMessage="My journey with Vue" />
        <!-- 根据一个变量的值动态传入 -->
        <PropsUser :greetMessage="greetMessage"/>
        <!-- 根据一个更复杂表达式的值动态传入 -->
		<PropsUser :greetMessage="greetMessage + ' by ' + kcldh" />
    </div>
</template>

<script>
import PropsUser from "./components/PropsUser.vue";

export default {
    components: {
        PropsUser
    },
    data() {
      return {
          greetMessage: "hello world"
      }
    }
}
</script>

<!-- 子组件 -->
<template>
    <h2>props使用</h2>
    <h2>{{ message }}</h2>
</template>

<script>
export default {
    props: {
        greetMessage: {
            type: String,
            default: '你好'
        }
    }
}
</script>

单向数据流

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:

export default {
  props: ['foo'],
  created() {
    // ❌ 警告!prop 是只读的!
    this.foo = 'bar'
  }
}

导致你想要更改一个 prop 的需求通常来源于以下两种场景:

  1. prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:

    export default {
      props: ['initialCounter'],
      data() {
        return {
          // 计数器只是将 this.initialCounter 作为初始值
          // 像下面这样做就使 prop 和后续更新无关了
          counter: this.initialCounter
        }
      }
    }
    
  2. 需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:

    export default {
      props: ['size'],
      computed: {
        // 该 prop 变更时计算属性也会自动更新
        normalizedSize() {
          return this.size.trim().toLowerCase()
        }
      }
    }
    

Prop 校验

Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。

要声明对 props 的校验,你可以向 props 选项提供一个带有 props 校验选项的对象,例如:

export default {
  props: {
    // 基础类型检查
    //(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
    propA: Number,
    // 多种可能的类型
    propB: [String, Number],
    // 必传,且为 String 类型
    propC: {
      type: String,
      required: true
    },
    // Number 类型的默认值
    propD: {
      type: Number,
      default: 100
    },
    // 对象类型的默认值
    propE: {
      type: Object,
      // 对象或者数组应当用工厂函数返回。
      // 工厂函数会收到组件所接收的原始 props
      // 作为参数
      default(rawProps) {
        return { message: 'hello' }
      }
    },
    // 自定义类型校验函数
    propF: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 函数类型的默认值
    propG: {
      type: Function,
      // 不像对象或数组的默认,这不是一个
      // 工厂函数。这会是一个用来作为默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
}

一些补充细节:

  • 所有 prop 默认都是可选的,除非声明了 required: true
  • Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
  • Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
  • 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。

当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。

组件事件

触发与监听事件

在组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件 (例如:在 v-on 的处理函数中):

<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

$emit() 方法在组件实例上也同样以 this.$emit() 的形式可用:

export default {
  methods: {
    submit() {
      this.$emit('someEvent')
    }
  }
}

父组件可以通过 v-on (缩写为 @) 来监听事件:

<MyComponent @some-event="callback" />

同样,组件的事件监听器也支持 .once 修饰符:

<MyComponent @some-event.once="callback" />

像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。

父组件

<template>
    <div>
        <Hello @injectMsg="getChildMsg"></Hello>
    </div>
</template>

<script>
import Hello from './Hello.vue'

export default {
    name: "Content",
    components: {
        Hello
    },
    data() {
        return {
            msg: '',
            content: 'content'
        }
    },
    methods: {
        getChildMsg: function (value) {
            this.msg = value
        }
    }
}
</script>

子组件

<template>
    <div>
        <p> {{ msg }}</p>
    </div>
    <button @click="sendParent">提交数据给父组件</button>
</template>

<script>
export default {
    data() {
        return {
            msg: 'world'
        }
    },
    methods: {
        sendParent: function () {
            // this.$emit('事件名称', '发送的事件参数')
            this.$emit('injectMsg', this.msg)
        }
    }
}
</script>

插槽 Slots

插槽内容与出口

在之前的章节中,我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。

举例来说,这里有一个 `` 组件,可以像这样使用:

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

而 `` 的模板是这样的:

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

`` 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VxMBwA1Y-1679897350564)(null)]

最终渲染出的 DOM 是这样:

<button class="fancy-btn">Click me!</button>

渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

这里的两个 {{ message }} 插值表达式渲染的内容都是一样的。

插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 `` 组件:

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

如果我们想在父组件没有提供任何插槽内容时在 内渲染“Submit”,只需要将“Submit”写在 标签之间来作为默认内容:

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

现在,当我们在父组件中使用 `` 且没有提供任何插槽内容时:

<SubmitButton />

“Submit”将会被作为默认内容渲染:

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

但如果我们提供了插槽内容:

<SubmitButton>Save</SubmitButton>

那么被显式提供的内容会取代默认内容:

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

使用示例:

父组件

<script>
import Content from "./components/Content.vue";

export default {
    data() {
        return {}
    },
    components: {
        Content
    },
    provide: {
        message: "hello"
    }
}
</script>

<template>
    <Content>
         <!-- 使用子组件的数据来渲染 -->
        <template v-slot:default="obj">
            <ul>
                <li v-for="item in obj.list">
                    {{ item }}
                </li>
            </ul>
        </template>
    </Content>
</template>

子组件

<template>
    <div>
        <h2>content组件</h2>
        <div>
            <!-- 传值给父组件 -->
            <slot :list="list"></slot>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            list: [1, 2, 3, 4, 5]
        }
    }
}
</script>

具名插槽

有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 `` 组件中,有如下模板:

<div class="container">
  <header>
    <!-- 标题内容放这里 -->
  </header>
  <main>
    <!-- 主要内容放这里 -->
  </main>
  <footer>
    <!-- 底部内容放这里 -->
  </footer>
</div>

对于这种场景,`` 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

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

这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 `` 出口会隐式地命名为“default”。

在父组件中使用 `` 时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:

要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 ` 元素,并将目标插槽的名字传给该指令:

<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

v-slot 有对应的简写 #,因此 可以简写为。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

具名插槽图示

下面我们给出完整的、向 `` 传递插槽内容的代码,指令均使用的是缩写形式:

<BaseLayout>
  <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>
</BaseLayout>

当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 ` 节点都被隐式地视为默认插槽的内容。所以上面也可以写成:

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- 隐式的默认插槽 -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

现在 ` 元素中的所有内容都将被传递到相应的插槽。最终渲染出的 HTML 如下:

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

使用 JavaScript 函数来类比可能更有助于你来理解具名插槽:

// 传入不同的内容给不同名字的插槽
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> 渲染插槽内容到对应位置
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

使用示例:

父组件

<script>
import Content from "./components/Content.vue";

export default {
    components: {
        Content
    }
}
</script>

<template>
    <Content>
    	<template v-slot:abc>
    		<button>按钮abc</button>
        </template>
   		<template v-slot:bcd>
     		<button>按钮bcd</button>
        </template>
    </Content>
</template>

子组件

<template>
    <div>
        <h2>content组件</h2>
        <div>
            <!--     多个slot可以使用name       -->
            <slot name="abc"></slot>
            <slot name="bcd"></slot>
        </div>
    </div>
</template>

参考文档

参考链接:vue官网

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值