Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue插槽是如何运作的? #82

Open
GGXXMM opened this issue Jan 9, 2021 · 0 comments
Open

vue插槽是如何运作的? #82

GGXXMM opened this issue Jan 9, 2021 · 0 comments
Labels

Comments

@GGXXMM
Copy link
Owner

GGXXMM commented Jan 9, 2021

一、插槽有哪几类?

样例代码基于vue 2.5.x版本

1. 默认插槽

父组件:

<template>
  <div class="container">
    <!-- message组件 -->
    <message :show="isShow">
      默认插槽
    </message>
  </div>
</template>

<script>
import message from './message'
export default {
  data () {
    return {
      isShow: true
    }
  },
  components: {
    message
  }
}
</script>

子组件:

<template>
  <div class="message-box" v-if="show">
    <!-- 通过slot获取传入内容 -->
    <slot></slot>
    <span class="close-icon">X</span>
  </div>
</template>

<script>
export default {
  name: 'message',
  props: ['show']
}
</script>

2. 具名插槽

父组件:

<template>
  <div class="container">
    <!-- message组件 -->
    <message :show="isShow">
      <!-- 具名插槽 -->
      <template v-slot:title>
        <strong>具名插槽-标题</strong>
      </template>
    </message>
  </div>
</template>

<script>
import message from './message'

export default {
  data () {
    return {
      isShow: true
    }
  },
  components: {
    message
  }
}
</script>

vue 2.6.0 新增:

  • v-slot:title 替换成 #title

子组件:

<template>
  <div class="message-box" v-if="show">
    <slot name="title"></slot>
    <span class="close-icon">X</span>
  </div>
</template>

<script>
export default {
  name: 'message',
  props: ['show']
}
</script>

3. 作用域插槽

父组件:

<template>
  <div class="container">
    <!-- message组件 -->
    <message :show="isShow">
      <!-- 作用域插槽 -->
      <template slot-scope="slotProps">
        <strong>{{slotProps.title}}</strong>
      </template>
    </message>
  </div>
</template>

<script>
import message from './message'

export default {
  data () {
    return {
      isShow: true
    }
  },
  components: {
    message
  }
}
</script>

子组件:

<template>
  <div class="message-box" v-if="show">
    <slot :title="title"></slot>
    <span class="close-icon">X</span>
  </div>
</template>

<script>
export default {
  name: 'message',
  props: ['show'],
  data () {
    return {
      title: '来自于message的标题'
    }
  }
}
</script>

vue 2.6.0 废弃了slot-scope

二、插槽原理

原理简述

当子组件vm 实例化时,获取到父组件传入的slot 标签的内容,存放在vm.$slot 中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot 标签,使用$slot 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

运作流程分析

调试demo代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue slot插槽</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="demo"> 
      <h1>插槽处理机制</h1>
      <comp1>
        <span>abc</span>
      </comp1>
    </div>

    <script>
      Vue.component('comp1', {
        template: `<div><slot></slot></div>`
      })
      const app = new Vue({
        el: '#demo',
      })
      console.log(app.$options.render)
    </script>

  </body>
</html>

可以从render函数追溯源码:
core/instance/render.js

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

image
继续追溯resolveSlots函数,这个函数是从父组件中获取渲染节点VNode,存入default,这就是默认插槽的内容。函数renderContext参数就是父组件实例,显然如果有动态内容要从它里面获取。
src/core/instance/render-helpers/resolve-slots.js

import type VNode from 'core/vdom/vnode'

/**
 * Runtime helper for resolving raw children VNodes into a slot object.
 */
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]
    }
  }
  // 返回插槽的内容,VNode对象
  return slots
}

function isWhitespace (node: VNode): boolean {
  return (node.isComment && !node.asyncFactory) || node.text === ' '
}

那么谁使用$slots呢,显然是comp1组件的渲染函数,打印其渲染函数:

function anonymous(
) {
with(this){return _c('div',[_t("default")],2)}
}

这里的_t源码src/core/instance/render-helpers/index.js中可以找到是renderSlot()函数的别名,其函数的源码如下:
src/core/instance/render-helpers/render-slot.js

import { extend, warn, isObject } from 'core/util/index'

/**
 * Runtime helper for rendering <slot>
 */
export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  const scopedSlotFn = this.$scopedSlots[name]
  let nodes
  if (scopedSlotFn) { // scoped slot
    props = props || {}
    if (bindObject) {
      if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
        warn(
          'slot v-bind without argument expects an Object',
          this
        )
      }
      props = extend(extend({}, bindObject), props)
    }
    nodes = scopedSlotFn(props) || fallback
  } else {
    nodes = this.$slots[name] || fallback
  }

  const target = props && props.slot
  if (target) {
    return this.$createElement('template', { slot: target }, nodes)
  } else {
    return nodes
  }
}

参考:febobo/web-interview#16

@GGXXMM GGXXMM added the vue label Dec 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant