欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

深度解析Vue的高级功能:插槽机制详解(上篇)

最编程 2024-02-15 22:50:51
...

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

Vue组件的另一个重要概念是插槽,它允许你以一种不同于严格的父子关系的方式组合组件。插槽为你提供
了一个将内容放置到新位置或使组件更通用的出口。本文按照普通插槽,具名插槽,再到作用域插槽的思
路,逐步深入内部的实现原理,有对插槽使用不熟悉的,可以先参考官网对[插槽](url)[的介绍]

普通插槽

var child = {
  template: `<div class="child"><slot></slot></div>`
}
var vm = new Vue({
  el: '#app',
  components: {
    child
  },
  template: `<div id="app"><child>test</child></div>`
})
// 最终渲染结果
<div class="child">test</div>

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

组件挂载原理

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

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

父组件处理

// 创建子Vnode过程
  function createComponent (
    Ctor, // 子类构造器
    data,
    context, // vm实例
    children, // 父组件需要分发的内容
    tag // 子组件占位符
  ){
    ···
    // 创建子vnode,其中父保留的children属性会以选项的形式传递给Vnode
    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, 
      children: children },
      asyncFactory
    );
  }
// Vnode构造器
var VNode = function VNode 
(tag,data,children,text,elm,context,componentOptions,asyncFactory) {
  ···
  this.componentOptions = componentOptions; // 子组件的选项相关
}

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

子组件流程

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

// 子组件的初始化
Vue.prototype._init = function(options) {
  if (options && options._isComponent) {
    initInternalComponent(vm, options);
  }
  initRender(vm)
}
function initInternalComponent (vm, options) {
    var opts = vm.$options = Object.create(vm.constructor.options);
    var parentVnode = options._parentVnode;
    opts.parent = options.parent;
    opts._parentVnode = parentVnode;
    // componentOptions为子vnode记录的相关信息
    var vnodeComponentOptions = parentVnode.componentOptions;
    opts.propsData = vnodeComponentOptions.propsData;
    opts._parentListeners = vnodeComponentOptions.listeners;
    // 父组件需要分发的内容赋值给子选项配置的_renderChildren
    opts._renderChildren = vnodeComponentOptions.children;
    opts._componentTag = vnodeComponentOptions.tag;

    if (options.render) {
      opts.render = options.render;
      opts.staticRenderFns = options.staticRenderFns;
    }
  }

最终在子组件实例的配置中拿到了父组件保存的分发内容,记录在组件实例$options._renderChildren中,这是第二步的重点。 接下来是子组件的实例化会进入initRender阶段,在这个过程会将配置的_renderChildren属性做规范化处理,并将他赋值给子实例上的$slot属性,这是第三步的重点

function initRender(vm) {
  ···
  vm.$slots = resolveSlots(options._renderChildren, renderContext);// $slots拿到了子占位符节点的_renderchildren(即需要分发的内容),保留作为子实例的属性
}

function resolveSlots (children,context) {
    // children是父组件需要分发到子组件的Vnode节点,如果不存在,则没有分发内容
    if (!children || !children.length) {
      return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {
      var child = children[i];
      var 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.
      // 分支1为具名插槽的逻辑,放后分析
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        if (child.tag === 'template') {
          slot.push.apply(slot, child.children || []);
        } else {
          slot.push(child);
        }
      } else {
      // 普通插槽的重点,核心逻辑是构造{ default: [children] }对象返回
        (slots.default || (slots.default = [])).push(child);
      }
    }
    return slots
  }

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

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

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

第五步到了子组件渲染为Vnode的过程。render函数执行阶段会执行_t()函数,_t函数是renderSlot函数简写,它会在Vnode树中进行分发内容的替换,具体看看实现逻辑。 至此,一个完整且简单的插槽流程分析完毕。