插槽

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-02-19

插槽内容

示例:my-comp 组件使用 <slot> 定义插槽。

<template>
  <div class="my-comp-wrap">
    <p>我是组件 my-comp</p>
    <slot></slot>
  </div>
</template>

1.分发DOM内容

示例: 业务处使用 my-comp 组件

<template>
  <my-comp :num='2'>
    <!-- 也可以在插槽中使用数据 -->
    <div>分发的内容 {{ msg }}</div>
  </my-comp>
</template>

<script>
export default {
  data() {
    return {
      msg: 'hello'
    }
  }
}
</script>

需要注意的是:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。所以这里的插槽能取到父级模板的值msg,无法取到父级模板传给 my-comp 组件的 num props。

会被编译成如下:

<template>
  <div class="my-comp-wrap">
    <p>我是组件 my-comp</p>
    <div>分发的内容 hello</div>
  </div>
</template>

2.分发组件内容

示例: 业务处使用 my-comp 组件

<template>
  <my-comp>
    <other-comp></other-comp>
  </my-comp>
</template>

会被编译成如下:

<template>
  <div class="my-comp-wrap">
    <p>我是组件 my-comp</p>
    <!-- 这里是示意,真实会被渲染成other-comp对应的dom -->
    <other-comp></other-comp>
  </div>
</template>

插槽后备内容

示例:my-comp 组件

<template>
  <div class="my-comp-wrap">
    <p>我是组件 my-comp</p>
    <slot>123</slot>
  </div>
</template>

这样使用:

<template>
  <my-comp></my-comp>
</template>

会被编译成 :

<template>
  <div class="my-comp-wrap">
    <p>我是组件 my-comp</p>
    123
  </div>
</template>

如果父级模板设置了插槽内容就会替换123。

具名插槽

可以给 slot 元素通过 name attribute 来定义额外的插槽。

示例: my-comp 组件

<template>
  <div class="container">
    <div>
      <slot name="header"></slot>
    </div>
    <div>
      <!-- 一个不带 name属性的<slot>会带有隐含的 name="default" -->
      <slot></slot>
    </div>
    <div>
      <slot name="footer"></slot>
    </div>
  </div>
</template>

这样使用:

<template>
  <my-comp>
    <template v-slot:header>
      <h1>我是渲染到name="header"插槽中的内容</h1>
    </template>

    <!-- 这部分会被渲染到name="default"的slot处 -->
    <p> some message ... </p>

    <template v-slot:footer>
      <p>我是渲染到name="footer"插槽中的内容</p>
    </template>
  </my-comp>
</template>

会被渲染成:

<div class="container">
  <div>
    <h1>我是渲染到name="header"插槽中的内容</h1>
  </div>
  <div>
    <p> some message ... </p>
  </div>
  <div>
    <p>我是渲染到name="footer"插槽中的内容</p>
  </div>
</div>

然而,如果你希望更明确一些,仍然可以在一个 <template> 中包裹默认插槽的内容,比如上例中也可以写成这样,会得到同样渲染的内容。

<template>
  <!-- ... -->

  <template v-slot:default>
    <p> some message ... </p>
  </template>

  <!-- ... -->
</template>

1.具名插槽的缩写

<template>
  <my-comp>
    <template #header>
      <h1>我是渲染到name="header"插槽中的内容</h1>
    </template>

    <!-- 这部分会被渲染到name="default"的slot处 -->
    <p> some message ... </p>

    <template #footer>
      <p>我是渲染到name="footer"插槽中的内容</p>
    </template>
  </my-comp>
</template>

作用域插槽(插槽传值)

之前我们提到过,父级模板使用组件设置插槽内容的时候,只能访问父级模板中的数据,但是有时我们希望在父级模板层面访问到子组件中有的数据,可以如下使用作用域插槽。

示例: my-comp 组件

<template>
  <div class="my-comp-wrap">
    <p>我是组件 my-comp</p>
    <slot :user="obj">
      <!-- 后备内容 -->
      {{ obj.lastName }}
    </slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: {
        firstName: 'jack',
        lastName: 'ma',
        age: 18
      }
    }
  }
}
</script>

这样使用:

<template>
  <my-comp>
    <template v-slot:default="slotProps">
      <!-- slot传值时是使用 :user,父级使用就是 slotProps.user -->
      {{ slotProps.user.firstName }}
    </template>
  </my-comp>
</template>

需要注意的slot传值时是使用 :user,所以这里访问 slotProps.user

会被编译为:

<div class="my-comp-wrap">
  <p>我是组件 my-comp</p>
  jack
</div>

其中,对于v-slot:default 可以简写不带 :default 参数,需要注意的是默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确。

<template v-slot="slotProps">
  {{ slotProps.user.firstName }}
</template>

对于同时存在默认插槽和其他具名插槽作用域就不能简写了,需要指明:

<template>
  <my-comp>
    <template v-slot:default="slotProps">
      {{ slotProps.user.firstName }}
    </template>

    <template v-slot:other="otherSlotProps">
      ...
    </template>
  </my-comp>
</template>

作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里:

function (slotProps) {
  // 插槽内容
}

因此在使用作用域插槽的时候还可以使用解构的写法。

1.解构插槽 Prop

比如上面例子提到的作用域传值 :user="obj",在父级模板中可以这样解构:

<template>
  <my-comp>
    <template v-slot:default="{ user }">
      {{ user.firstName }}
    </template>
  </my-comp>
</template>

同时也支持,解构时重命名:

<template>
  <my-comp>
    <template v-slot:default="{ user: person }">
      {{ person.firstName }}
    </template>
  </my-comp>
</template>

甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形

<template>
  <my-comp>
    <template v-slot:default="{ user = { firstName: 'Guest' } }">
      {{ user.firstName }}
    </template>
  </my-comp>
</template>

需要注意的是,只有子组件传值:user="obj"中的objundefined, 这个时候父级模板 obj.firstName 才会显示 Guest。

如果子组件传的这个 objfirstName 属性,就会显示这个 firstName 的值,但如果 obj 存在,却没有 firstName 这个属性,这里会显示空。

可以如下理解,举例函数解构传值:

function fn ({ user = { firstName: 'Guest'} }) {
  console.log(user.firstName);
}

fn({
  user: {
    firstName: 'jack',
    age: 19
  }
})
// 输出 jack

fn({
  user: {
    age: 19
  }
})
// 输出undefined

fn({
  user: undefined
})
// 输出 Guest

fn({
  user: false
})
// 输出 undefined

fn({
  user: 1
})
// 输出 undefined

// fn({
//   user: null
// })
// Error: Cannot read property 'firstName' of null

2.具名插槽缩写时的解构传值

<template>
  <my-comp>
    <template #default="{ user }">
      {{ user.firstName }}
    </template>
  </my-comp>
</template>

动态插槽名

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

插槽回传父组件的提供的props

示例: todo-list 组件

<template>
  <ul>
    <li
      v-for="(item, index) in todos"
      :key="index"
    >
      <!-- todos通过props接受,将其中的子项回传给父组件 -->
      <slot :todoItem="item"></slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: {
    todos: {
      type: Array,
      default: [],
    }
  }
}
</script>

业务处这样使用:

<template>
  <div>
    <todo-list :todos="todoList">
      <template v-slot="{ todoItem }">
        <span v-if="todoItem.isComplete">✓</span>
        {{ todoItem.text }}
      </template>
    </todo-list>
  </div>
</template>

<script>
export default {
  data() {
    return {
      todoList: [
        {
          isComplete: true,
          text: '早起'
        },
        {
          isComplete: true,
          text: '早餐'
        },
        {
          isComplete: false,
          text: '早睡'
        }
      ]
    }
  }
}
</script>

当每个listItem需要支持一些操作从而改变数据的时候,还可以在子组件层级定义一个自己的数据去承接传过来的props,而操作的时候改变子组件自己定义的这个数据。

<template>
  <ul>
    <li
      v-for="(item, index) in toDoData"
      :key="index"
    >
      <!-- todos通过props接受,将其中的子项todo回传给父组件 -->
      <slot :todoItem="item"></slot>
      <button @click="handleClick(index)">切换</button>
    </li>
  </ul>
</template>

<script>
export default {
  props: {
    todos: {
      type: Array,
      default: [],
    }
  },
  data() {
    return {
      // 定义子组件自己的数据
      toDoData: []
    }
  },
  watch: {
    todos: {
      handler(newVal) {
        // 承接传过来的props
        this.toDoData = newVal;
      },
      immediate: true
    }
  },
  methods: {
    // 切换操作
    handleClick(index) {
      // 操作发生时改变自己的数据
      this.$set(this.toDoData, index, {
        ...this.toDoData[index],
        isComplete: !this.toDoData[index].isComplete
      })
    }
  },
}
</script>