学习VUE3——组件(二)

打印 上一主题 下一主题

主题 549|帖子 549|积分 1657

组件插槽slots

插槽内容与出口

在某些场景中,我们大概想要为子组件传递一些模板片断,让子组件在它们的组件中渲染这些片断。这时就需要用到插槽。
如下例所示:
  1. <!-- Parent.vue -->
  2. <FancyButton>
  3.   Click me! <!-- 插槽内容 -->
  4. </FancyButton>
  5. <!-- Child FancyButton.vue -->
  6. <button class="fancy-btn">
  7.   <slot></slot> <!-- 插槽出口 -->
  8. </button>
复制代码
 <slot>  元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

最终渲染出的 DOM :
  1. <button class="fancy-btn">Click me!</button>
复制代码
插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件:
  1. <!-- Parent.vue -->
  2. <FancyButton>
  3.   <span style="color:red">Click me!</span>
  4.   <AwesomeIcon name="plus" />
  5. </FancyButton>
  6. <!-- FancyButton.vue -->
  7. <button class="fancy-btn">
  8.   <slot></slot> <!-- 插槽出口 -->
  9. </button>
  10. <!-- 渲染出 -->
  11. <button class="fancy-btn">
  12.   <span style="color:red">Click me!</span>
  13.   <AwesomeIcon name="plus" />
  14. </button>
复制代码
Vue 组件的插槽机制是受原生 Web Component <slot> 元素的启发而诞生。
渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中界说的。如下例:
  1. <span>{{ message }}</span>
  2. <FancyButton>{{ message }}</FancyButton>
复制代码
这里的两个 {{ message }} 插值表达式渲染的内容都是一样的。
插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其界说时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。
默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。如下例:
  1. <!-- submitButton.vue -->
  2. <button type="submit">
  3.   <slot>
  4.     Submit <!-- 默认内容 -->
  5.   </slot>
  6. </button>
  7. <!-- Parent.vue -->
  8. <!-- 父组件中没有提供任何插槽内容时 -->
  9. <SubmitButton />
  10. <!-- 将被渲染为 -->
  11. <button type="submit">Submit</button>
复制代码
如果上例中,父组件提供了插槽内容:
  1. <!-- Parent.vue -->
  2. <SubmitButton>Save</SubmitButton>
  3. <!-- 将被渲染为 -->
  4. <button type="submit">Save</button>
复制代码
具名插槽

偶然一个组件中需要包含多个插槽出口,这时就需要给插槽定名来区分开:
  1. <div class="container">
  2.   <header>
  3.     <slot name="header"></slot>
  4.   </header>
  5.   <main>
  6.     <!-- 没命名的为default -->
  7.     <slot></slot>
  8.   </main>
  9.   <footer>
  10.     <slot name="footer"></slot>
  11.   </footer>
  12. </div>
复制代码
这类带 name 的插槽被称为具名插槽 (named slots)。
要为具名插槽传入内容,我们需要利用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:
  1. <BaseLayout>
  2.   <template v-slot:header>
  3.     <!-- header 插槽的内容放这里 -->
  4.   </template>
  5.   <template v-slot:default>
  6.   <!-- default 插槽的内容放这里 -->
  7.   </template>
  8.   <template v-slot:footer>
  9.   <!-- footer 插槽的内容放这里 -->
  10.   </template>
  11. </BaseLayout>
复制代码
v-slot 可简写为 # ,如 <template v-slot:header> 可以简写为 <template #header> 。其意思就是“将这部分模板片断传入子组件的 header 插槽中”。
上例可简写为:
  1. <BaseLayout>
  2.   <template #header>
  3.     <h1>Here might be a page title</h1>
  4.   </template>
  5.   <template #default>
  6.     <p>A paragraph for the main content.</p>
  7.     <p>And another one.</p>
  8.   </template>
  9.   <template #footer>
  10.     <p>Here's some contact info</p>
  11.   </template>
  12. </BaseLayout>
复制代码
所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容,所以上例中的默认插槽也可写为:
  1. <BaseLayout>
  2.   <template #header>
  3.     <h1>Here might be a page title</h1>
  4.   </template>
  5.   <!-- 隐式的默认插槽 -->
  6.   <p>A paragraph for the main content.</p>
  7.   <p>And another one.</p>
  8.   <template #footer>
  9.     <p>Here's some contact info</p>
  10.   </template>
  11. </BaseLayout>
复制代码
上面所有例子最终都渲染出:
  1. <div class="container">
  2.   <header>
  3.     <h1>Here might be a page title</h1>
  4.   </header>
  5.   <main>
  6.     <p>A paragraph for the main content.</p>
  7.     <p>And another one.</p>
  8.   </main>
  9.   <footer>
  10.     <p>Here's some contact info</p>
  11.   </footer>
  12. </div>
复制代码
条件插槽

偶然你需要根据插槽是否存在来渲染某些内容。如希望包装插槽以提供额外的样式,就需要判断插槽内容是否存在:
  1. <template>
  2.   <div class="card">
  3.     <div v-if="$slots.header" class="card-header">
  4.       <slot name="header" />
  5.     </div>
  6.    
  7.     <div v-if="$slots.default" class="card-content">
  8.       <slot />
  9.     </div>
  10.    
  11.     <div v-if="$slots.footer" class="card-footer">
  12.       <slot name="footer" />
  13.     </div>
  14.   </div>
  15. </template>
复制代码

动态插槽名

动态指令参数在 v-slot 上也是有效的,即可以界说下面这样的动态插槽名:
  1. <base-layout>
  2.   <template v-slot:[dynamicSlotName]>
  3.     ...
  4.   </template>
  5.   <!-- 缩写为 -->
  6.   <template #[dynamicSlotName]>
  7.     ...
  8.   </template>
  9. </base-layout>
复制代码
作用域插槽

如上渲染作用域所述,插槽内容无法访问子组件域内的数据。但偶然我们又需要显示子组件域内的数据,这时就需要在子组件的插槽出口中,像props传递一样的形式向一个插槽的出口上传递 attributes:
  1. <!-- myComponent.vue -->
  2. <div>
  3.   <slot :text="greetingMessage" :count="1"></slot>
  4. </div>
  5. <!-- Parent.vue -->
  6. <!-- 默认插槽的写法 -->
  7. <MyComponent v-slot="slotProps">
  8.   {{ slotProps.text }} {{ slotProps.count }}
  9. </MyComponent>
复制代码

 v-slot="slotProps" 可以类比这里的函数签名,和函数的参数雷同,我们也可以在 v-slot 中利用解构:
  1. <MyComponent v-slot="{ text, count }">
  2.   {{ text }} {{ count }}
  3. </MyComponent>
复制代码
具名作用域插槽

上面的例子是默认插槽的,具名作用域插槽的工作方式也是雷同的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name="slotProps"。当利用缩写时是这样:
  1. <!-- myComponent.vue -->
  2. <slot name="header" message="hello"></slot>
  3. <!-- Parent.vue -->
  4. <MyComponent>
  5.   <template #header="headerProps">
  6.     {{ headerProps }}
  7.   </template>
  8.   <template #default="defaultProps">
  9.     {{ defaultProps }}
  10.   </template>
  11.   <template #footer="footerProps">
  12.     {{ footerProps }}
  13.   </template>
  14. </MyComponent>
复制代码
   留意:
  

  • 插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的效果是 { message: 'hello' }。
  • 如果你同时利用了具名插槽与默认插槽并且需要传递数据,则需要为默认插槽利用显式的 <template> 标签。
  无渲染组件

一些组件大概只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。我们将这种类型的组件称为无渲染组件
如下的无渲染例子,是一个封装了追踪当前鼠标位置逻辑的组件:
  1. <!-- App.vue -->
  2. <script setup>
  3. import MouseTracker from './MouseTracker.vue'
  4. </script>
  5. <template>
  6.         <MouseTracker v-slot="{ x, y }">
  7.           Mouse is at: {{ x }}, {{ y }}
  8.         </MouseTracker>
  9. </template>
  10. <!-- MouseTracker.vue -->
  11. <script setup>
  12. import { ref, onMounted, onUnmounted } from 'vue'
  13.   
  14. const x = ref(0)
  15. const y = ref(0)
  16. const update = e => {
  17.   x.value = e.pageX
  18.   y.value = e.pageY
  19. }
  20. onMounted(() => window.addEventListener('mousemove', update))
  21. onUnmounted(() => window.removeEventListener('mousemove', update))
  22. </script>
  23. <template>
  24.   <slot :x="x" :y="y"/>
  25. </template>
复制代码


依赖注入

通常情况下,当我们需要从父组件向子组件传递数据时,会利用 props。如果有一个嵌套层级比力深的组件,需要从第一级传数据到最后一级时,如果用 props 来传递,则必须将其沿着组件链逐级传递下去,这会使得工作非常贫苦:

中心的 <Footer> 组件虽然用不着porps数据,但为了 <DeepChild>  能正常利用porps数据,仍然需要界说并向下传递。如果组件链路非常长,大概会影响到更多这条路上的组件。
provideinject 可以帮助我们办理这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

Provide (提供)

要为组件后代提供数据,需要利用到 provide() 函数:
  1. <script setup>
  2. import { provide } from 'vue'
  3. provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
  4. </script>
复制代码
在setup选项中利用:
  1. import { provide } from 'vue'
  2. export default {
  3.   setup() {
  4.     provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
  5.   }
  6. }
复制代码
 provide() 函数吸收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),利用差别的注入名,注入差别的依赖值。
第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:
  1. import { ref, provide } from 'vue'
  2. const count = ref(0)
  3. provide('key', count)
复制代码
提供的响应式状态使后代组件可以由此和提供者创建响应式的联系。

除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
  1. import { createApp } from 'vue'
  2. const app = createApp({})
  3. app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
复制代码
在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会利用组件形式来提供值。

Inject (注入)

要注入上层组件提供的数据,需利用 inject() 函数:
  1. <script setup>
  2. import { inject } from 'vue'
  3. const message = inject('message')
  4. </script>
复制代码
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会主动解包为其内部的值。这使得注入方组件可以或许通过 ref 对象保持了和供给方的响应性链接
在setup选项中利用:
  1. import { inject } from 'vue'
  2. export default {
  3.   setup() {
  4.     const message = inject('message')
  5.     return { message }
  6.   }
  7. }
复制代码
provide()  inject() 的完备例子:
  1. // App.vue
  2. <script setup>
  3. import { ref, provide } from 'vue'
  4. import Child from './Child.vue'
  5. // by providing a ref, the GrandChild
  6. // can react to changes happening here.
  7. const message = ref('hello')
  8. provide('message', message)
  9. </script>
  10. <template>
  11.   <input v-model="message">
  12.   <Child />
  13. </template>
复制代码
  1. // Child.vue
  2. <script setup>
  3. import GrandChild from './GrandChild.vue'
  4. </script>
  5. <template>
  6.   <GrandChild />
  7. </template>
复制代码
  1. // GrandChild.vue<script setup>
  2. import { inject } from 'vue'
  3. const message = inject('message')
  4. </script><template>  <p>    Message to grand child: {{ message }}  </p></template>
复制代码

注入默认值

默认情况下,inject 假设传入的注入名会被某个祖先链上的组件提供。如果该注入名简直没有任何组件提供,则会抛出一个运行时告诫。
如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 雷同:
  1. // 如果没有祖先组件提供 "message"
  2. // `value` 会是 "这是默认值"
  3. const value = inject('message', '这是默认值')
复制代码
在一些场景中,默认值大概需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下举行不必要的盘算或产生副作用,我们可以利用工厂函数来创建默认值:
  1. const value = inject('key', () => new ExpensiveClass(), true)
复制代码
第三个参数表示默认值应该被当作一个工厂函数。

如安在注入更改提供的响应式状态

当提供 / 注入响应式的数据时,建议尽大概将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
如果需要再注入方组件中更改数据,建议在供给方组件内声明并提供一个更改数据的方法函数:
  1. <!-- 在供给方组件内 -->
  2. <script setup>
  3. import { provide, ref } from 'vue'
  4. const location = ref('North Pole')
  5. function updateLocation() {
  6.   location.value = 'South Pole'
  7. }
  8. provide('location', {
  9.   location,
  10.   updateLocation
  11. })
  12. </script>
复制代码
  1. <!-- 在注入方组件 -->
  2. <script setup>
  3. import { inject } from 'vue'
  4. const { location, updateLocation } = inject('location')
  5. </script>
  6. <template>
  7.   <button @click="updateLocation">{{ location }}</button>
  8. </template>
复制代码
如果你想确保提供的数据不能被注入方的组件更改,你可以利用 readonly() 来包装提供的值。
  1. <script setup>
  2. import { ref, provide, readonly } from 'vue'
  3. const count = ref(0)
  4. provide('read-only-count', readonly(count))
  5. </script>
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

泉缘泉

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表