• 10.1 普通插槽
    • 10.1.1 基础用法
    • 10.1.2 组件挂载原理
    • 10.1.3 父组件处理
    • 10.1.4 子组件流程

    10.1 普通插槽

    插槽将<slot></slot>作为子组件承载分发的载体,简单的用法如下

    10.1.1 基础用法

    1. var child = {
    2. template: `<div class="child"><slot></slot></div>`
    3. }
    4. var vm = new Vue({
    5. el: '#app',
    6. components: {
    7. child
    8. },
    9. template: `<div id="app"><child>test</child></div>`
    10. })
    11. // 最终渲染结果
    12. <div class="child">test</div>

    10.1.2 组件挂载原理

    插槽的原理,贯穿了整个组件系统编译到渲染的过程,所以首先需要回顾一下对组件相关编译渲染流程,简单总结一下几点:

    1. 从根实例入手进行实例的挂载,如果有手写的render函数,则直接进入$mount挂载流程。
    2. 只有template模板则需要对模板进行解析,这里分为两个阶段,一个是将模板解析为AST树,另一个是根据不同平台生成执行代码,例如render函数。
    3. $mount流程也分为两步,第一步是将render函数生成Vnode树,如果遇到子组件会先生成子组件,子组件会以vue-componet-tag标记,另一步是把Vnode渲染成真正的DOM节点。
    4. 创建真实节点过程中,如果遇到子的占位符组件会进行子组件的实例化过程,这个过程又将回到流程的第一步。

    接下来我们对slot的分析将围绕这四个具体的流程展开。

    10.1.3 父组件处理

    回到组件实例流程中,父组件会优先于子组件进行实例的挂载,模板的解析和render函数的生成阶段在处理上没有特殊的差异,这里就不展开分析。接下来是render函数生成Vnode的过程,在这个阶段会遇到子的占位符节点(即:child),因此会为子组件创建子的VnodecreateComponent执行了创建子占位节点Vnode的过程。我们把重点放在最终Vnode代码的生成。

    1. // 创建子Vnode过程
    2. function createComponent (
    3. Ctor, // 子类构造器
    4. data,
    5. context, // vm实例
    6. children, // 父组件需要分发的内容
    7. tag // 子组件占位符
    8. ){
    9. ···
    10. // 创建子vnode,其中父保留的children属性会以选项的形式传递给Vnode
    11. var vnode = new VNode(
    12. ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    13. data, undefined, undefined, undefined, context,
    14. { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    15. asyncFactory
    16. );
    17. }
    18. // Vnode构造器
    19. var VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) {
    20. ···
    21. this.componentOptions = componentOptions; // 子组件的选项相关
    22. }

    createComponent函数接收的第四个参数children就是父组件需要分发的内容。在创建子Vnode过程中,会以会componentOptions配置传入Vnode构造器中。最终Vnode中父组件需要分发的内容以componentOptions属性的形式存在,这是插槽分析的第一步

    10.1.4 子组件流程

    父组件的最后一个阶段是将Vnode渲染为真正的DOM节点,在这个过程中如果遇到子Vnode会优先实例化子组件并进行一系列子组件的渲染流程。子组件初始化会先调用_init方法,并且和父组件不同的是,子组件会调用initInternalComponent方法拿到父组件拥有的相关配置信息,并赋值给子组件自身的配置选项。

    1. // 子组件的初始化
    2. Vue.prototype._init = function(options) {
    3. if (options && options._isComponent) {
    4. initInternalComponent(vm, options);
    5. }
    6. initRender(vm)
    7. }
    8. function initInternalComponent (vm, options) {
    9. var opts = vm.$options = Object.create(vm.constructor.options);
    10. var parentVnode = options._parentVnode;
    11. opts.parent = options.parent;
    12. opts._parentVnode = parentVnode;
    13. // componentOptions为子vnode记录的相关信息
    14. var vnodeComponentOptions = parentVnode.componentOptions;
    15. opts.propsData = vnodeComponentOptions.propsData;
    16. opts._parentListeners = vnodeComponentOptions.listeners;
    17. // 父组件需要分发的内容赋值给子选项配置的_renderChildren
    18. opts._renderChildren = vnodeComponentOptions.children;
    19. opts._componentTag = vnodeComponentOptions.tag;
    20. if (options.render) {
    21. opts.render = options.render;
    22. opts.staticRenderFns = options.staticRenderFns;
    23. }
    24. }

    最终在子组件实例的配置中拿到了父组件保存的分发内容,记录在组件实例$options._renderChildren中,这是第二步的重点

    接下来是子组件的实例化会进入initRender阶段,在这个过程会将配置的_renderChildren属性做规范化处理,并将他赋值给子实例上的$slot属性,这是第三步的重点

    1. function initRender(vm) {
    2. ···
    3. vm.$slots = resolveSlots(options._renderChildren, renderContext);// $slots拿到了子占位符节点的_renderchildren(即需要分发的内容),保留作为子实例的属性
    4. }
    5. function resolveSlots (children,context) {
    6. // children是父组件需要分发到子组件的Vnode节点,如果不存在,则没有分发内容
    7. if (!children || !children.length) {
    8. return {}
    9. }
    10. var slots = {};
    11. for (var i = 0, l = children.length; i < l; i++) {
    12. var child = children[i];
    13. var data = child.data;
    14. // remove slot attribute if the node is resolved as a Vue slot node
    15. if (data && data.attrs && data.attrs.slot) {
    16. delete data.attrs.slot;
    17. }
    18. // named slots should only be respected if the vnode was rendered in the
    19. // same context.
    20. // 分支1为具名插槽的逻辑,放后分析
    21. if ((child.context === context || child.fnContext === context) &&
    22. data && data.slot != null
    23. ) {
    24. var name = data.slot;
    25. var slot = (slots[name] || (slots[name] = []));
    26. if (child.tag === 'template') {
    27. slot.push.apply(slot, child.children || []);
    28. } else {
    29. slot.push(child);
    30. }
    31. } else {
    32. // 普通插槽的重点,核心逻辑是构造{ default: [children] }对象返回
    33. (slots.default || (slots.default = [])).push(child);
    34. }
    35. }
    36. return slots
    37. }

    其中普通插槽的处理逻辑核心在(slots.default || (slots.default = [])).push(child);,即以数组的形式赋值给default属性,并以$slot属性的形式保存在子组件的实例中。

    随后子组件也会走挂载的流程,同样会经历template模板到render函数,再到Vnode,最后渲染真实DOM的过程。解析AST阶段,slot标签和其他普通标签处理相同,不同之处在于AST生成render函数阶段,对slot标签的处理,会使用_t函数进行包裹。这是关键步骤的第四步

    子组件渲染的大致流程简单梳理如下:

    1. // ast 生成 render函数
    2. var code = generate(ast, options);
    3. // generate实现
    4. function generate(ast, options) {
    5. var state = new CodegenState(options);
    6. var code = ast ? genElement(ast, state) : '_c("div")';
    7. return {
    8. render: ("with(this){return " + code + "}"),
    9. staticRenderFns: state.staticRenderFns
    10. }
    11. }
    12. // genElement实现
    13. function genElement(el, state) {
    14. // 针对slot标签的处理走```genSlot```分支
    15. if (el.tag === 'slot') {
    16. return genSlot(el, state)
    17. }
    18. }
    19. // 核心genSlot原理
    20. function genSlot (el, state) {
    21. // slotName记录着插槽的唯一标志名,默认为default
    22. var slotName = el.slotName || '"default"';
    23. // 如果子组件的插槽还有子元素,则会递归调执行子元素的创建过程
    24. var children = genChildren(el, state);
    25. // 通过_t函数包裹
    26. var res = "_t(" + slotName + (children ? ("," + children) : '');
    27. // 具名插槽的其他处理
    28. ···
    29. return res + ')'
    30. }

    最终子组件的render函数为:

    1. "with(this){return _c('div',{staticClass:"child"},[_t("default")],2)}"

    第五步到了子组件渲染为Vnode的过程。render函数执行阶段会执行_t()函数,_t函数是renderSlot函数简写,它会在Vnode树中进行分发内容的替换,具体看看实现逻辑。

    1. // target._t = renderSlot;
    2. // render函数渲染Vnode函数
    3. Vue.prototype._render = function() {
    4. var _parentVnode = ref._parentVnode;
    5. if (_parentVnode) {
    6. // slots的规范化处理并赋值给$scopedSlots属性。
    7. vm.$scopedSlots = normalizeScopedSlots(
    8. _parentVnode.data.scopedSlots,
    9. vm.$slots, // 记录父组件的插槽内容
    10. vm.$scopedSlots
    11. );
    12. }
    13. }

    normalizeScopedSlots的逻辑较长,但并不是本节的重点。拿到$scopedSlots属性后会执行真正的render函数,其中_t的执行逻辑如下:

    1. // 渲染slot组件内容
    2. function renderSlot (
    3. name,
    4. fallback, // slot插槽后备内容(针对后备内容)
    5. props, // 子传给父的值(作用域插槽)
    6. bindObject
    7. ) {
    8. // scopedSlotFn拿到父组件插槽的执行函数,默认slotname为default
    9. var scopedSlotFn = this.$scopedSlots[name];
    10. var nodes;
    11. // 具名插槽分支(暂时忽略)
    12. if (scopedSlotFn) { // scoped slot
    13. props = props || {};
    14. if (bindObject) {
    15. if (!isObject(bindObject)) {
    16. warn(
    17. 'slot v-bind without argument expects an Object',
    18. this
    19. );
    20. }
    21. props = extend(extend({}, bindObject), props);
    22. }
    23. // 执行时将子组件传递给父组件的值传入fn
    24. nodes = scopedSlotFn(props) || fallback;
    25. } else {
    26. // 如果父占位符组件没有插槽内容,this.$slots不会有值,此时vnode节点为后备内容节点。
    27. nodes = this.$slots[name] || fallback;
    28. }
    29. var target = props && props.slot;
    30. if (target) {
    31. return this.$createElement('template', { slot: target }, nodes)
    32. } else {
    33. return nodes
    34. }
    35. }

    renderSlot执行过程会拿到父组件需要分发的内容,最终Vnode树将父元素的插槽替换掉子组件的slot组件。

    最后一步就是子组件真实节点的渲染了,这点没有什么特别点,和以往介绍的流程一致

    至此,一个完整且简单的插槽流程分析完毕。接下来看插槽深层次的用法。