什么是作用域插槽?插槽与作用域插槽的区别

本文详细讲解了Vue插槽的两种类型——具名插槽与作用域插槽,包括它们的定义、使用场景、源码解析以及实际应用实例。重点阐述了插槽的渲染机制,以及如何通过源码追踪插槽的处理过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概念

// 有name的属于具名插槽,没有name属于匿名插槽
<app>
  <div slot="a">xxxx</div>
  <div slot="b">xxxx</div>
</app>
<slot name="a"></slot>
<slot name="b"></slot>

普通插槽渲染的位置是在它的父组件里面,而不是在子组件里面
作用域插槽渲染是在子组件里面

1.插槽slot

在渲染父组件的时候,会将插槽中的先渲染。

创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿 子进行分类 {a:[vnode],b[vnode]}

渲染组件时会拿对应的slot属性的节点进行替换操作。(插槽的作用域为父组件,插槽中HTML模板显示不显示、以及怎样显示由父组件来决定)

有name的父组件通过html模板上的slot属性关联具名插槽。没有slot属性的html模板默认关联匿名插槽。

2.作用域插槽slot-scope

作用域插槽在解析的时候,不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。

或者可以说成作用域插槽是子组件可以在slot标签上绑定属性值,在父组件可以拿到子组件的数据,通过子组件绑定数据传递给父组件。(插槽的作用域为子组件

子组件:
<slot :nickName="'wthreesix'"></slot>

父组件:
<slot-child>
  <template slot-scope="scope">
     <div>{{scope.nickName}}</div>
  </template>
</slot-child>

二、源码图

三、插槽渲染分析

1.插槽

// 父组件:
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
  <my-component>
    <div slot="header">node</div>
    <div>react</div>
    <div slot="footer">vue</div>
   </my-component>
`)

将上面组件编译后:
/**
with(this) {
  return _c('my-component', // _c 创建虚拟dom
    [ // 组件中包含子节点
      _c('div', // 第一个是一个插槽,插槽叫header
        { 
          attrs: {
            "slot": "header"
          },
          slot: "header"
        },
        [_v("node")] // 将里面内容存起来了(_v是createTextVNode创建文本节点)
       ),
     _v(" "),
     _c('div',[_v("react")]),
     _v(" "),
     _c('div', {
       attrs: {
         "slot": "footer"
       },
       slot: "footer"
     }, [_v("vue")])
    ]
  )
}
*/
在调render方法的时候已经将组件全部渲染好了

// 子组件
let ele = VueTemplateCompiler.compile(`
  <div>
    <slot name="header"></slot>
    <slot name="footer"></slot>
    <slot></slot>
  </div>
`);
/**
with(this) {
  // 渲染的时候会找header对应的是谁
  return _c('div', [
     _t("header"), _v(" "),
     _t("footer"), _v(" "),
     _t("default")], 2)
  }
  当找到就会换过来,如下:
  return _c('div', [_v("node"), _v(" "), _v("vue"), _v(" "),
   _t("default")], 2)
  }
}
**/
// _t是renderSlot

插槽就是一个替换的过程,将父组件渲染好的结果直接替换到自己的上面,创建的过程相当于在父组件渲染的

2.作用域插槽

父组件:
let ele = VueTemplateCompiler.compile(`
  <app>
    <div slot-scope="msg" slot="footer">{{msg.a}}</div>
   </app>
`);
/**
with(this) {
  // 编译出的不是一个child,而是一个属性scopedSlots
  return _c('app', {
    scopedSlots: _u([{ 
      key: "footer",
      fn: function (msg) { 将子节点变成一个函数,这个函数不调用就不会去渲染这个子节点
          return _c('div', {}, [_v(_s(msg.a))])
      }
    }])
  })
 }
}
在初始化的时候并不会渲染子节点
*/

子组件:
const VueTemplateCompiler = require('vue-template-compiler');
VueTemplateCompiler.compile(`
  // 当我们在写slot去执行的时候进行渲染
  <div>
    <slot name="footer" a="1" b="2"></slot>
  </div>
`);
/**
with(this) {
  // 去找footer,找见会调用上面的那个函数,并且将属性传入到这个函数里面,这时候才会把这节点进行渲染,完成之后替换调
  return _c('div', [_t("footer", null, {
     "a": "1",
     "b": "2"
  })], 2)
}
**/

// 作用域插槽的内容会被渲染成一个函数
// 作用域插槽渲染是在当前组件的内部,不是在父组件中

四、源码

1.initRender(初始化render,构建vm.$slots)

export function initRender (vm: Component) {
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
}

2.resolveSlots(映射slot名字和对应的vnode)

export function resolveSlots (
  children: ?Array<VNode>,
  context: ?Component
): { [key: string]: Array<VNode> } {
  if (!children || !children.length) {
    return {}
  }
  const slots = {}
  for (let i = 0, l = children.length; i < l; i++) {
    const child = children[i]
    const data = child.data
    // remove slot attribute if the node is resolved as a Vue slot node
    if (data && data.attrs && data.attrs.slot) {
      delete data.attrs.slot
    }
    // named slots should only be respected if the vnode was rendered in the
    // same context.
    if ((child.context === context || child.fnContext === context) &&
      data && data.slot != null
    ) {
      const name = data.slot
      const slot = (slots[name] || (slots[name] = []))
      if (child.tag === 'template') {
        slot.push.apply(slot, child.children || [])
      } else {
        slot.push(child)
      }
    } else {
      (slots.default || (slots.default = [])).push(child)
    }
  }
  // ignore slots that contains only whitespace
  for (const name in slots) {
    if (slots[name].every(isWhitespace)) {
      delete slots[name]
    }
  }
  return slots
}

3.normalizeScopedSlots(core/vdom/helpers/normalize-scoped-slot.js)

export function normalizeScopedSlots (
  slots: { [key: string]: Function } | void,
  normalSlots: { [key: string]: Array<VNode> },
  prevSlots?: { [key: string]: Function } | void
): any {
  let res
  const hasNormalSlots = Object.keys(normalSlots).length > 0
  const isStable = slots ? !!slots.$stable : !hasNormalSlots
  const key = slots && slots.$key
  if (!slots) {
    res = {}
  } else if (slots._normalized) {
    // fast path 1: child component re-render only, parent did not change
    return slots._normalized
  } else if (
    isStable &&
    prevSlots &&
    prevSlots !== emptyObject &&
    key === prevSlots.$key &&
    !hasNormalSlots &&
    !prevSlots.$hasNormal
  ) {
    // fast path 2: stable scoped slots w/ no normal slots to proxy,
    // only need to normalize once
    return prevSlots
  } else {
    res = {}
    for (const key in slots) {
      if (slots[key] && key[0] !== '$') {
        res[key] = normalizeScopedSlot(normalSlots, key, slots[key]) // 作用域插槽
      }
    }
  }
  // expose normal slots on scopedSlots
  for (const key in normalSlots) {
    if (!(key in res)) {
      res[key] = proxyNormalSlot(normalSlots, key) // 普通插槽
    }
  }
  // avoriaz seems to mock a non-extensible $scopedSlots object
  // and when that is passed down this would cause an error
  if (slots && Object.isExtensible(slots)) {
    (slots: any)._normalized = res
  }
  def(res, '$stable', isStable)
  def(res, '$key', key)
  def(res, '$hasNormal', hasNormalSlots)
  return res
}

4.proxyNormalSlot(将slot代理到scopeSlots上)

function proxyNormalSlot(slots, key) {
  return () => slots[key]
}

5.normalizeScopedSlot(将scopeSlots对应属性和方法挂载到scopeSlots)

function normalizeScopedSlot(normalSlots, key, fn) {
  const normalized = function () {
    let res = arguments.length ? fn.apply(null, arguments) : fn({})
    res = res && typeof res === 'object' && !Array.isArray(res)
      ? [res] // single vnode
      : normalizeChildren(res)
    return res && (
      res.length === 0 ||
      (res.length === 1 && res[0].isComment) // #9658
    ) ? undefined
      : res
  }
  // this is a slot using the new v-slot syntax without scope. although it is
  // compiled as a scoped slot, render fn users would expect it to be present
  // on this.$slots because the usage is semantically a normal slot.
  if (fn.proxy) {
    Object.defineProperty(normalSlots, key, {
      get: normalized,
      enumerable: true,
      configurable: true
    })
  }
  return normalized
}

6.最后调用renderSlot用函数的返回值进行渲染。

<think>好的,我现在要帮用户了解Vue.js中的插槽概念,包括具名插槽作用域插槽的用法。首先,我需要回忆一下Vue插槽相关知识,并结合用户提供的引用内容来组织回答。 用户提供的引用里提到了Vue2和Vue3的插槽使用,特别是默认插槽、具名插槽作用域插槽。我需要确保回答覆盖这些类型,并且给出清晰的示例。 首先,插槽的基本概念是什么?根据引用3,插槽允许在组件内部预留位置,让父组件传递模板代码进去,增加组件的灵活性。Vue3中使用<slot>元素声明插槽。默认插槽没有name属性,作为备用内容的位置。 接下来是具名插槽,引用3和4提到具名插槽使用name属性来区分不同的插槽位置。父组件需要使用v-slot指令或者简写的#来指定插槽名称。比如在子组件中有多个具名插槽,父组件可以分别插入内容到对应的位置。 然后是作用域插槽,引用5提到作用域插槽可以让子组件向父组件传递数据。这在需要根据子组件的数据动态渲染不同内容时非常有用。子组件通过v-bind绑定数据,父组件通过v-slot接收这些数据,并在模板中使用。 用户可能需要具体的示例来理解这些概念。我需要分别编写默认插槽、具名插槽作用域插槽的代码示例,确保语法正确,并且符合Vue2或Vue3的规范。根据引用2和3,Vue3中使用<template #slotName>的语法,而Vue2可能使用slot-scope,但用户的问题可能更关注Vue3,所以示例应基于Vue3的语法。 同时,要注意用户提供的引用中有Vue3的具体用法,比如作用域插槽Vue3中使用v-slot:name="props"的方式。需要确保示例中的语法正确,比如使用#简写,以及作用域变量的传递。 可能还需要对比默认插槽和具名插槽区别,说明作用域插槽的应用场景,比如列表渲染的不同样式需求,如引用5中的案例,子组件提供数据,父组件决定如何展示。 最后,生成相关问题时要覆盖插槽的类型、用法、应用场景以及常见问题,比如如何传递多个数据、动态插槽名等。确保问题能够帮助用户进一步深入学习。 检查是否有遗漏点,比如匿名插槽作为默认备用,具名插槽的多个插槽使用,作用域插槽的数据传递机制。确保回答结构清晰,分点说明,示例代码正确,并且引用标记正确。</think>### Vue.js 插槽的概念及使用方法 #### 一、插槽的基本概念 插槽(Slot)是 Vue.js 中用于组件化开发的核心特性,允许父组件向子组件传递模板内容,增强组件的灵活性和复用性。通过插槽,子组件可以预留一个或多个“占位区域”,父组件根据需求填充不同的内容[^3]。 #### 二、默认插槽(匿名插槽) **定义**:未使用 `name` 属性的插槽称为默认插槽,用于接收父组件未指定名称的内容。 **示例**: 子组件 `ChildComponent.vue`: ```html <template> <div> <slot>默认内容(当父组件未提供内容时显示)</slot> </div> </template> ``` 父组件使用: ```html <ChildComponent> <p>父组件传递的内容</p> </ChildComponent> ``` 若父组件未填充内容,子组件将显示默认内容[^4]。 --- #### 三、具名插槽 **定义**:通过 `name` 属性命名的插槽,用于区分多个插槽位置。 **示例**: 子组件 `LayoutComponent.vue`: ```html <template> <div> <slot name="header"></slot> <slot></slot> <!-- 默认插槽 --> <slot name="footer"></slot> </div> </template> ``` 父组件使用(Vue3 语法): ```html <LayoutComponent> <template #header> <h1>标题内容</h1> </template> <p>默认插槽内容</p> <template #footer> <p>页脚内容</p> </template> </LayoutComponent> ``` 父组件通过 `#插槽名` 指定内容插入位置。 --- #### 四、作用域插槽 **定义**:允许子组件向父组件传递数据,父组件基于数据动态渲染内容。 **应用场景**:如列表组件需要支持多种渲染方式(水平排列、列表、网格等)[^5]。 **示例**: 子组件 `ListComponent.vue`: ```html <template> <ul> <li v-for="item in items" :key="item.id"> <slot :item="item"></slot> </li> </ul> </template> <script> export default { props: ['items'] } </script> ``` 父组件使用: ```html <ListComponent :items="pLanguages"> <template #default="{ item }"> <span>{{ item }}</span> <!-- 自定义渲染逻辑 --> </template> </ListComponent> ``` 子组件通过 `v-bind` 绑定数据 `item`,父组件通过 `#default="{ item }"` 接收并使用数据[^5]。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值