Vue源码阅读(26):slot 插槽的源码解析

 我的开源库:

建议先复习一下插槽的用法,官方文档点击这里

今天和大家讲讲 Vue 中的插槽功能,主要从普通插槽、后备内容、具名插槽、作用域插槽这四个知识点进行解析。

1,普通插槽

对应文档点击这里

首先从插槽最简单的用法讲起,实现功能的重点是子组件的实例子组件的 render 函数,我们以下面这个简单的例子进行解析。

Vue.component('hello-world', {
  template: `
    <div class="hello_world">
      <h2>我是 HelloWorld 组件</h2>
      <slot></slot>
    </div>
  `,
  methods: {}
})

new Vue({
  el: '#app',
  data() {
    return {}
  },
  methods: {},
  template: `
    <div class="app_container">
      <h1>我是APP</h1>
      <hello-world>
        <h3>我是 slot</h3>
      </hello-world>
    </div>
  `
})

该例子渲染的页面如下所示:

1-1, 父组件的 vnode

父组件生成的 vnode 如下所示,该 vnode 节点有三个子节点,第三个子节点就是 hello-world 组件的 vnode 节点。在 Vue 中,每一个使用的组件都会有一个相对应的 Vue 实例,在目前的阶段,该组件的实例还没有被创建出来,所以 vnode.componentInstance 的值是 undefined。vnode.componentOptions 对象中的数据用于创建该组件的实例,可以发现在 vnode.componentOptions 对象中,有一个 children 属性,该属性保存着 hello-world 组件的子节点,并且该子节点已经被编译成了 vnode。

1-2,初始化子组件实例的 $slots 属性

在 1-1 小节中,父组件的 vnode 已经创建出来了,所以之后会进行父组件的 patch 操作,在 patch 到第三个子节点的时候,发现该 vnode 是一个组件节点,此时会进行子组件 vnode 的实例化操作,这部分的代码看 src/core/instance/init.js。

Vue.prototype._init = function (options?: Object) {
  // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
  const vm: Component = this

  // 初始化与插槽有关的内容
  initRender(vm)
}

 在 _init 方法中,与插槽有关的代码是 initRender(vm),我们看下 initRender() 函数。

export function initRender (vm: Component) {
  // 对 options._renderChildren 进行解析,并将解析的结果赋值到 vm.$slots
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
}

在我们的例子中,vm.$slots 的值为:

vm.$slots = {
  default: [{
    // h3 vnode 节点
    tag: "h3",
    children: [{
      // h3 vnode 节点下面的文本子节点
      text: "我是 slot"
    }]
  }]
}

1-3,子组件执行 render 函数,生成子组件的 vnode

子组件的 render 函数如下所示:

with(this){
  return _c(
    'div',
    {staticClass:"hello_world"},
    [
      _c('h2',[_v("我是 HelloWorld 组件")]),
      _v(" "),
      _t("default")
    ],
    2
  )
}

在子组件的 render 函数中,与 <slot></slot> 模板字符串有关的代码字符串是 _t("default"),_t  是 renderSlot 方法的别名,我们看 renderSlot 函数。

export function installRenderHelpers (target: any) {
  ......
  target._t = renderSlot
  ......
}

在 renderSlot 函数中,通过 "default" slot 名称从子组件实例的 $slots 属性中获取 <hello-world></hello-world> 的子节点数组。

export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  const scopedSlotFn = this.$scopedSlots[name]
  if (scopedSlotFn) { // scoped slot
    ......
  } else {
    // 代码执行到这里
    // 从 this.$slots 对象中获取指定插槽的数组,在这里 name == "default"
    const slotNodes = this.$slots[name]
    // warn duplicate slot usage
    if (slotNodes && process.env.NODE_ENV !== 'production') {
      slotNodes._rendered && warn(
        `Duplicate presence of slot "${name}" found in the same render tree ` +
        `- this will likely cause render errors.`,
        this
      )
      slotNodes._rendered = true
    }
    // 将 slotNodes 返回
    return slotNodes || fallback
  }
}

执行子组件 render 函数最终生成的 vnode 如下所示:

可以发现,子组件中的 <slot></slot> 已经变成了 <h3>我是 slot</h3>。

子节点的 vnode 渲染到页面上,最终的效果如下所示:

2,后备内容

对应文档点击这里

实现该功能的重点主要看子组件的 render 函数

我们以下面的代码为例。

Vue.component('hello-world', {
  template: `
    <div class="hello_world">
      <h2>我是 HelloWorld 组件</h2>
      <slot>
        <h3>slot 默认内容</h3>
      </slot>
    </div>
  `,
  methods: {}
})

new Vue({
  el: '#app',
  data() {
    return {}
  },
  methods: {},
  template: `
    <div class="app_container">
      <h1>我是APP</h1>
      <hello-world></hello-world>
    </div>
  `
})

子组件的模板字符串中,"<slot></slot>" 节点的下面有 "<h3>slot 默认内容</h3>" 子节点,这个 h3 节点是这个 slot 节点的后备内容。

2-1,子节点的抽象语法树

该子节点的抽象语法树如下所示:

2-2,子节点的代码字符串

子节点的代码字符串如下所示:

with(this){
  return _c(
    'div',
    {staticClass:"hello_world"},
    [
      _c('h2',[_v("我是 HelloWorld 组件")])
      ,_v(" "),
      _t(
        "default",
        [_c('h3',[_v("slot 默认内容")])]
      )
    ],
    2
  )
}

可以发现 _t 函数有了第二个参数 [_c('h3',[_v("slot 默认内容")])],这正好对应 <slot> 节点的后备内容,接下来看 renderSlot 函数。

2-3,renderSlot

export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  const scopedSlotFn = this.$scopedSlots[name]
  if (scopedSlotFn) { // scoped slot
    ......
  } else {
    // 从 this.$slots 对象中获取指定插槽的数组,在这里 name == "default"
    const slotNodes = this.$slots[name]
    // warn duplicate slot usage
    if (slotNodes && process.env.NODE_ENV !== 'production') {
      slotNodes._rendered && warn(
        `Duplicate presence of slot "${name}" found in the same render tree ` +
        `- this will likely cause render errors.`,
        this
      )
      slotNodes._rendered = true
    }
    // 将 slotNodes 返回,如果 slotNodes 不存在的话,则返回 fallback
    // fallback 是 VNode 节点的数组,是当前处理 <slot> 的后备内容
    return slotNodes || fallback
  }
}

renderSlot 函数的最后一行可以发现,如果 slotNodes 不存在的话,则返回 fallback,这个 fallback 就是 slot 后备内容的 vnode。

2-4,子组件的 vnode

子组件最终生成的 vnode 如下所示:

页面渲染的效果见下图:

3,具名插槽

对应的文档点击这里

如果已经理解了普通插槽的话,那理解具名插槽也就非常简单了,它们之间并没有什么本质的区别,我们直接以一个简单的例子进行讲解。

Vue.component('hello-world', {
  template: `
    <div class="hello_world">
      <h2>我是 HelloWorld 组件</h2>
      <slot name="header"></slot>
      <slot></slot>
      <slot name="footer"></slot>
    </div>
  `,
  methods: {}
})

new Vue({
  el: '#app',
  data() {
    return {}
  },
  methods: {},
  template: `
    <div class="app_container">
      <h1>我是APP</h1>
      <hello-world>
        <template v-slot:header>
          <h3>header slot</h3>
        </template>
        <template v-slot:default>
          <h4>default slot</h4>
        </template>
        <template v-slot:footer>
          <h5>footer slot</h5>
        </template>
      </hello-world>
    </div>
  `
})

该例子渲染的页面如下所示:

3-1,子组件实例的 $slots 属性

可以发现,子组件实例的 $slots 属性是一个对象,并且该对象里面有三个属性,分别对应默认插槽的内容、header插槽的内容、footer插槽的内容。在这里,和第一小节的普通插槽并没有什么本质的区别,只不过 $slots 属性对象中的内容多了两条而已。

3-2,子组件的 render 函数

 子组件的 render 函数如下所示:

with(this){
  return _c(
    'div',
    {staticClass:"hello_world"},
    [
      _c('h2',[_v("我是 HelloWorld 组件")]),
      _v(" "),
      _t("header"),
      _v(" "),
      _t("default"),
      _v(" "),
      _t("footer")
    ],
    2
  )
}

子组件模板中的三个 slot。

<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>

分别对应。

_t("header"),
_t("default"),
_t("footer")

 _t 是 renderSlot 函数的别名,借助该函数,可以从 vm.$slots 对象中获取指定 slot 应该渲染的 vnode。

3-3,子组件最终的 vnode

在最终得到的 vnode 中,可以发现,子组件模板中的三个 <slot></slot> 已经被转换成了 <h3></h3>、<h4></h4>、<h5></h5>。接下来,进行 patch 操作,渲染到页面上即可。

4,作用域插槽

对应的文档点击这里

上面的插槽都有一个共同的特点,就是组件子节点的 vnode 都是在父组件 render 函数执行时生成的。在这一点上,作用域插槽和它们完全不同,在父组件 render 函数执行获取的 vnode 中,使用了作用域插槽的组件子节点被编译成了一个函数,保存在子组件 vnode 节点的 data.scopedSlots 对象中。当子组件的 Vue 实例创建出来后,这个函数被保存在 vm.$scopedSlots 对象中,我们以一个例子进行讲解,就很好懂了,例子代码如下:

Vue.component('hello-world', {
  template: `
    <div class="hello_world">
      <h2>我是 HelloWorld 组件</h2>
      <slot v-bind:user="user"></slot>
    </div>
  `,
  data() {
    return {
      user: {
        name: 'tom'
      }
    }
  },
  methods: {}
})

new Vue({
  el: '#app',
  data() {
    return {}
  },
  methods: {},
  template: `
    <div class="app_container">
      <h1>我是APP</h1>
      <hello-world>
        <template v-slot:default="slotProps">
          <h3>{{slotProps.user.name}}</h3>
        </template>
      </hello-world>
    </div>
  `
})

该例子渲染出来的页面如下所示:

4-1,子组件的 vue 实例以及作用域插槽节点编译成的函数

父组件的 vnode 生成之后,会进行 patch 操作,patch 处理到子节点 vnode 时,会进行子组件实例的初始化,在我们这个例子中,子组件的实例如下所示:

在这个子组件的实例中,与本节有关的是 $scopedSlots 属性,该属性是一个对象,对象的 key 是插槽的名称,对象的 value 是一个函数,我们看看这个函数的函数体。

function(slotProps){
  return [
    _c(
      'h3',
      [_v(_s(slotProps.user.name))]
    )
  ]
}

该函数有一个参数,参数的名称是 "slotProps",该名称取自 <template v-slot:default="slotProps">,这个函数的作用是创建并返回 h3 vnode 节点,并且 h3 节点中的文本取自 slotProps.user.name。

好了,至此我们知道了使用作用域插槽的节点会被编译成一个函数,并且这个函数会被保存到 vm.$scopedSlots 属性中,接下来看子组件的 render 函数。

4-2,子组件的 render 函数

本例子中,子组件的 render 函数如下所示:

with(this){
  return _c(
    'div',
    {staticClass:"hello_world"},
    [
      _c('h2',[_v("我是 HelloWorld 组件")]),
      _v(" "),
      _t("default",null,{"user":user})
    ],
    2
  )
}

与作用域插槽有关的部分是:

_t("default",null,{"user":user})

接下来看 renderSlot 函数。

4-3,renderSlot

如果是作用域插槽的话,会执行到 if(){} 分支,该分支很简单,将 props 作为参数执行 scopedSlotFn 函数,并将该函数的返回值 return 出去即可。

export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  // 从 vm.$scopedSlots 中获取指定插槽对应的函数,在这里 name = "default"
  const scopedSlotFn = this.$scopedSlots[name]
  if (scopedSlotFn) { // scoped slot
    // 作用域插槽 代码执行到这里
    props = props || {}

    // 将 props 作为参数执行 scopedSlotFn 函数,并将函数的返回值 return 出去
    // scopedSlotFn 函数的作用是创建并返回作用域插槽中节点的 vnode。
    return scopedSlotFn(props) || fallback
  } else {
    ......
  }
}

4-4,子节点最终的 vnode

子节点最终的 vnode 如下所示:

可以看到子组件中的 <slot v-bind:user="user"></slot> 最终被转换成了 <h3>tom</h3>。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值