[Vue 源码] 动态组件 和 内置组件

182 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情

动态组件

当我们需要在不同的组件之间进行状态切换时,动态组件可以很好的满足需求。动态组件的核心是 component 标签和 is 属性的使用

动态组件的基本使用

<div id="app">
  <button @click="changeTabs('child1')">child1</button>
  <button @click="changeTabs('child2')">child2</button>
  <button @click="changeTabs('child3')">child3</button>
  <component :is="chooseTabs">
  </component>
</div>

<script>
var child1 = {
  template: '<div>content1</div>',
}
var child2 = {
  template: '<div>content2</div>'
}
var child3 = {
  template: '<div>content3</div>'
}
var vm = new Vue({
  el: '#app',
  components: {
    child1,
    child2,
    child3
  },
  methods: {
    changeTabs(tab) {
      this.chooseTabs = tab;
    }
  }
})
</script>

AST 解析

<component> 标签的处理基本上和之前的步骤基本一致。针对动态组件解析的差异,集中在 processComponent 上, 由于标签上 is 属性的存在,最终会在 AST 树上添加 component 标志

// 针对动态组件进行解析
function processComponent (el) {
  let binding
  // 拿到 is 属性对应的值
  if ((binding = getBindingAttr(el, 'is'))) {
    // 在 AST 树上添加 component 属性
    el.component = binding
  }
  if (getAndRemoveAttr(el, 'inline-template') != null) {
    el.inlineTemplate = true
  }
}

最终生成的 AST 中包含了 component 属性,属性值为组件名称

render 函数

有了 AST 树,接下来是根据 AST 树生成可执行的 render 函数,由于有 component 属性, render 函数的产生过程会走 genComponent 分支

// render函数生成函数
var code = generate(ast, options);
// generate函数的实现
function generate (ast,options) {
  var state = new CodegenState(options);
  var code = ast ? genElement(ast, state) : '_c("div")';
  return {
    render: ("with(this){return " + code + "}"),
    staticRenderFns: state.staticRenderFns
  }
}
function genElement(el, state) {
  ···
  var code;
  // 动态组件分支
  if (el.component) {
    code = genComponent(el.component, el, state);
  }
}

针对动态组件的处理逻辑比较简单,当没有内联模版标志是,拿到后续的字节点进行拼接,和普通组件唯一的区别在于, _c 的第一个参数不再是一个指定的字符串,而是一个代表组件的变量。

// 针对动态组件的处理
function genComponent (
  componentName,
  el,
  state
) {
  // 拥有inlineTemplate属性时,children为null
  var children = el.inlineTemplate ? null : genChildren(el, state, true);
  return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}

普通组件和动态组件的对比

普通组件的 render 函数

"with(this){return _c('div',{attrs:{"id":"app"}},[_c('child1',[_v(_s(test))])],1)}"

动态组件的 render 函数

"with(this){return _c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component"})],1)}"

动态组件和普通组件的区别在于:

    1. 动态组件在 AST 树生成阶段新增了 component 属性,这是动态组件的标志
    1. 产生 render 函数阶段由于 component 属性的窜爱,会执行 genComponent 分支 , genComponent 会针对动态组件的执行函数进行特殊的处理,和普通组件不同的是, 动态组件 render 函数中 _c 的第一个参数不再是不变的字符串,而是指定的组件名称变量。
    1. render 函数到虚拟 DOM 的阶段和普通组件的流程相同,只是字符串换成变量,并带有 {tag: 'component'}data 属性。

内置组件

内置组件是已经在源码初始化阶段就全局注册好的组件,在 Vue 官方文档中对内置组件进行了列举,分别是 componenttransitiontransition-groupkeep-aliveslot 。在前面对 slot component 进行分析后,意识到 slotcomponent 并不是真正的内置组件, 这两个没有被当成组件去处理,因此也没有组件的生命周期。 slot 只会在 render 函数阶段转换成 renderSlot 函数进行处理, 而 component 也只是借助 is 属性将 createElement 的第一个参数从字符串转换为变量。

下面,我们分析一下 transition transition-group keep-alive 这几个内置组件的注册流程,已经在编译时有什么不同。

构造器定义组件

Vue 在初始化阶段,会在构造器的 components 属性添加三个组件对象,每个组件对象的写法和自定义组件过程一直,有 render 函数,生命周期,也会定义各种数据。

// source/src/platforms/web/runtime/index.js
// Vue构造器的选项配置,compoents选项合并
extend(Vue.options.components, builtInComponents);
extend(Vue.options.components, platformComponents);

extend 方法前面已经分析过,核心逻辑就是将对象上的属性合并到源对象上,属性相同则覆盖

export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

最终 Vue 构造器拥有了三个组件的配置选项

Vue.components = {
  keepAlive: {},
  transition: {},
  transition-group: {},
}

注册内置组件

Vue 实例在初始化过程中,最重要的一步就是进行选项合并,而像内置组件这些资源类选项会有专门的选项合并策略。最终构造器上的组件选项会以原型链的形式注册到实例的 components 选贤中

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

// Vue 默认选项的合并,这些选项会合并到每一个 Vue 实例中
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null) // 创建一个🈳️对象,使其原型指向父类的资源选项,对于内置的 组件、指令、过滤器需要通过原型的方式来进行调用
  if (childVal) {
    // 开发环境下校验选项的合法性, component directive filter 这些选项需要是一个对象
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

mergeAssets 方法中关键的两步其中一个是 const res = Object.create(parentVal || null) 这会以 parentVal 为原型创建一个空对象,最后通过 extend 将用户自定义的 component 选项复制到空对象中,选项合并后,内置组件也因此在全局完成了注册。