浅曦Vue源码-31-挂载阶段-$mount-genSlot(20)

375 阅读5分钟

「这是我参与2022首次更文挑战的第35天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

零零总总的,更文活动写了 35 天了,说句实话,要不是有个活动盯着,真的很难坚持下去,不多说啦——致敬未来~

上一篇小作文的重点放在了 v-if/v-else-if/v-else 这几个条件渲染指令,条件渲染的实现核心流程如下:

  1. 生成 astparse 阶段 parseHTML 的过程中会将 v-if、v-else-if、v-else 解析成 el.if/el.elseif/el.else,同时会条件和条件成立时渲染的元素组成对象: { exp, block } pushel.ifConditions 中;
  2. 接着就是利用 ast 生成 render 函数generate 阶段,调用 genElement,当判断 el.if 存在时调用 genIf 处理条件渲染并标记 el.ifProcessedtrue 防止重复处理;

这个阶段我们讲的是 generate 阶段,所谓 generate 就是利用前面 parse 解析 html 模板获得的 ast 节点,将其还原成真实的 DOM 节点的过程。这个过程分为两部分:

  • 一仍属于部分属于编译时工作,将 ast 编译成 DOM 对应的渲染函数,所谓渲染函数是描述调用运行时辅助函数生成真正 DOM 的代码;
  • 另一部分属于运行时的辅助,这一部分是 Vue 调用第一步生成的 render 函数时生效,render 函数的执行就会生成真正 DOM

在日常开发通用组件时,大家肯定用过的一个标签 —— <slot></slot>,今天的笔墨就说说她的 render 函数

二、genElement 的调用

在说 genSlot 的调用过程之前,先来个例子:

  • test.html 中引用 some-com 组件,并且 some-come 组件中的 i 标签作为插槽内容分发给组件的具名插槽 nameSlot
<some-com :some-key="forProp">
  <slot aaa="b" bbb="c" name="namedSlot"><div>哒哒哒哒哒哒哒哒</div></slot>
  {{ someKey.a + foo }}
</some-com>
  • 注册 some-com 组件,组件内置了一个具名插槽 namedSlot
const someCom = {

  template: `
    <div style="color: red;background: #5cb85c;display: inline-block">
     <slot aaa="b" bbb="c" name="namedSlot"><div>哒哒哒哒哒哒哒哒</div></slot>
     {{ someKey.a + foo }}
 </div>`,
  props: {
    someKey: {
      type: Object,
      default: () => 'hhhhhhh'
    }
  },
  inject: ['foo']
}

2.1 genElement 调用 genSlot

genElement 如果判断 el.tag 即标签名为 slot,说明就是一个预置的插槽,则调用 genSlot 处理并返回结果;

export function genElement (): string {
  if (el.parent) {
  } else if (el.tag === 'slot') {
    // 调用 genSlot 获取 slot 标签的渲染函数,形如:slotName, children, attrs, bind)

    return genSlot(el, state)
  } else {
    return code
  }
}

2.2 genSlot

方法位置:src/compiler/codegen/index.js -> genSlot

方法参数:

  1. elast 节点对象;
  2. stateCodegenState 对象

方法作用:处理 slot 标签的 ast 对象,生成插槽对应的渲染函数,形如:

_t(slotName, children, attrs, bind)_t 也是一个运行时渲染函数的辅助函数,这个当然会放到后面一起讲;具体工作如下:

  1. 获取插槽名,匿名的 slot 插槽自动兜底一个 default 作为插槽名;
  2. 拼接 slot 的渲染函数主体:_t(....)
function genSlot (el: ASTElement, state: CodegenState): string {
  // 获取具名插槽的插槽名称,如果没有则兜底 default
  const slotName = el.slotName || '"default"'

  // 处理 slot 标签的所有子节点,生成子节点的 render 函数
  const children = genChildren(el, state)

  // <slot /> 标签渲染函数结果字符串 _t(slogName, children
  let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  
  // slot 标签上的 attrs
  const attrs = el.attrs || el.dynamicAttrs
    ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
        // slot props are camelized
        name: camelize(attr.name),
        value: attr.value,
        dynamic: attr.dynamic
      })))
    : null

  获取 slot 标签上的 v-bind
  const bind = el.attrsMap['v-bind']
  if ((attrs || bind) && !children) {
    res += `,null`
  }

  if (attrs) {
    res += `,${attrs}`
  }

  if (bind) {
    res += `${attrs ? '' : ',null'},${bind}`
  }
  
  // _t(slotName, children, attrs, bind)
  return res + ')'
}

从上面的代码中我们可以看出,slot 标签的子元素仍然会被处理成渲染函数。但是大家使用某个组件的插槽时,你会发现原来插槽标签 slot 和它的子元素都会被替换成分发内容,那这个 children 渲染还有什么意义呢?

它的意义在于当组件没有接收到分发的插槽内容,比如 <some-com></some-com>,此时some-com 就没有分发插槽内容,即some-com标签是空的;这个时候 slot 的子元素仍然会被渲染处理,起到默认的占位提示作用。

  • slot 标签的 children 渲染函数如图: image.png

  • some-com 没有插槽内容时的占位

image.png

2.3 说明

我写 Vue 源码阅读的方式和其他人最大的不同在文章的顺序和方法的列出顺序是按照代码的执行顺序组织的,可以这么说,Vue 的代码在浏览器中以什么样的顺序运行,我的文章就是以什么顺序组织的。

但是这一篇有一点例外,是因为接下来就准备聊一聊渲染函数的运行时帮助函数(_c/_v/_c/_t 等)的作用,所以这里是提前说了 genSlot 方法,并不代表我们的例子代码已经执行到 genSlot 了。为什么这么说?

这是因为 slot 标签处于子组件 <some-com /> 内部,一个子组件也是一个全新的 Vue 实例,这个实例现在还没有创建,只有当根实例(test.html 中的 script 标签下的 new Vue 是根实例)的渲染函数被执行时,执行渲染 some-com 组件的时候才会重新走创建子组件实例,编译子组件模板、生成子组件的渲染函数,生成子组件渲染函数时就会解析到这个 slot 标签,此时则会调用 genSlot 处理他;

三、总结

本篇小作文作为一个独立的小部分存在,提前介绍了 Vue 在编译时对 <slot> 这个占位符标签的一个处理,编译 slot 标签最终得到一个叫做 _t 的渲染函数的运行时帮助函数的调用:_t(slotName, children, attrs, bind)

其中 slotName 即为具名插槽 slot 标签上的 name 属性值,如果是匿名插槽,则 slotName"default"

childrenslot 标签的子元素;虽然插槽最终使用时不会展示,但是当组件被引用时没有传递插槽内容时,slot 的子元素仍然会被渲染用以占位提示。