5.2 组件Vnode创建

组件注册过后,会在实例options.component增加一个组件的配置属性,这个属性是一个子的Vue构造器。然而这个组件何时创建,何时进行实例化,何时渲染,何时挂载基础钩子是这一小节分析的核心。

5.2.1 Vnode创建流程图

5.2 组件Vnode创建 - 图1

5.2.2 具体流程分析

我们将上图的流程简单概括为以下几点:

    • Vue根实例初始化会执行 vm.$mount(vm.$options.el)实例挂载的过程,按照之前深入剖析Vue源码 - 完整渲染过程所讲的逻辑,完整流程会经历render函数生成Vnode,以及Vnode生成真实DOM的过程。
    • render函数生成Vnode过程中,子会优先父执行生成Vnode过程,子执行过程中遇到子组件占位符如(<test></test>)时,会判断该占位符是否是注册过的组件标签,如果符合条件,则进入createComponent创建子组件的过程,如果为一般标签,则执行new Vnode过程。
    • createComponent是创建组件Vnode的过程,创建过程会合并子和父构造器的选项配置,并安装组件相关的钩子,最后通过new Vnode()生成以vue-component开头的Virtual DOM
    • render函数执行过程也是一个循环递归调用创建Vnode的过程,执行3,4步之后,完整的生成了一个包含各个子组件的Vnode tree这其中一二步的源码是前一节所分析过的,我们重点分析遇到子组件占位符时差异的处理。
  1. // 内部执行将render函数转化为Vnode的函数
  2. function _createElement(context,tag,data,children,normalizationType) {
  3. ···
  4. if (typeof tag === 'string') {
  5. // 子节点的标签为普通的html标签,直接创建Vnode
  6. if (config.isReservedTag(tag)) {
  7. vnode = new VNode(
  8. config.parsePlatformTagName(tag), data, children,
  9. undefined, undefined, context
  10. );
  11. // 子节点标签为注册过的组件标签名,则子组件Vnode的创建过程
  12. } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  13. // 创建子组件Vnode
  14. vnode = createComponent(Ctor, data, context, children, tag);
  15. }
  16. }
  17. }

其中核心是在判断该子组件的占位符是否为已注册过的组件,在介绍全局注册时我们已经知道了,一个组件全局注册后,Vue实例的options.component对象上会新增一个带有构造器的组件选项。因此是否拥有这个选项也成为判断组件是否注册的标准。

  1. // 需要明确组件是否已经被注册
  2. function resolveAsset (options,type,id,warnMissing) {
  3. // 标签为字符串
  4. if (typeof id !== 'string') {
  5. return
  6. }
  7. // 这里是 options.component
  8. var assets = options[type];
  9. // 这里的分支分别支持大小写,驼峰的命名规范
  10. if (hasOwn(assets, id)) { return assets[id] }
  11. var camelizedId = camelize(id);
  12. if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
  13. var PascalCaseId = capitalize(camelizedId);
  14. if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
  15. // fallback to prototype chain
  16. var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
  17. if (warnMissing && !res) {
  18. warn(
  19. 'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
  20. options
  21. );
  22. }
  23. // 最终返回子类的构造器
  24. return res
  25. }

子组件创建Vnode的过程是调用createComponent方法。

  1. // 创建子组件过程
  2. function createComponent (
  3. Ctor, // 子类构造器
  4. data,
  5. context, // vm实例
  6. children, // 子节点
  7. tag // 子组件占位符
  8. ) {
  9. ···
  10. // Vue.options里的_base属性存储Vue构造器
  11. var baseCtor = context.$options._base;
  12. // 针对局部组件注册场景
  13. if (isObject(Ctor)) {
  14. Ctor = baseCtor.extend(Ctor);
  15. }
  16. data = data || {};
  17. // 构造器配置合并
  18. resolveConstructorOptions(Ctor);
  19. // 挂载组件钩子
  20. installComponentHooks(data);
  21. // return a placeholder vnode
  22. var name = Ctor.options.name || tag;
  23. // 创建子组件vnode,名称以 vue-component- 开头
  24. 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);
  25. return vnode
  26. }

这里将大部分的代码都拿掉了,只留下创建vnode相关的代码,最终会通过new Vue实例化一个名称以vue-component-开头标记名称的Vnode节点返回。其中两个关键的步骤是配置合并和安装组件钩子函数,选项合并的内容可以查看这个系列的前两节,这里看看installComponentHooks安装组件钩子函数时发生了什么。

  1. // 组件内部自带钩子
  2. var componentVNodeHooks = {
  3. init: function init (vnode, hydrating) {
  4. },
  5. prepatch: function prepatch (oldVnode, vnode) {
  6. },
  7. insert: function insert (vnode) {
  8. },
  9. destroy: function destroy (vnode) {
  10. }
  11. };
  12. var hooksToMerge = Object.keys(componentVNodeHooks);
  13. // 将componentVNodeHooks 钩子函数合并到组件data.hook中
  14. function installComponentHooks (data) {
  15. var hooks = data.hook || (data.hook = {});
  16. for (var i = 0; i < hooksToMerge.length; i++) {
  17. var key = hooksToMerge[i];
  18. var existing = hooks[key];
  19. var toMerge = componentVNodeHooks[key];
  20. // 如果钩子函数存在,则执行mergeHook$1方法合并
  21. if (existing !== toMerge && !(existing && existing._merged)) {
  22. hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
  23. }
  24. }
  25. }
  26. function mergeHook$1 (f1, f2) {
  27. // 返回一个依次执行f1,f2的函数
  28. var merged = function (a, b) {
  29. f1(a, b);
  30. f2(a, b);
  31. };
  32. merged._merged = true;
  33. return merged
  34. }

组件默认自带几个钩子函数,这些钩子函数在后续patch过程中会在不同阶段执行,installComponentHooks函数的目的是将这些默认的钩子函数和自定义的钩子函数合并,合并的原则是如果钩子函数存在,则合并两个函数,在执行阶段会依次执行。

5.2.3 局部注册和全局注册的区别

在说到全局注册和局部注册的用法时留下了一个问题,局部注册和全局注册两者的区别在哪里。上文源码分析讲到全局注册却没有提及局部注册,其实局部注册的原理同样简单,我们使用局部注册组件时会通过在父组件选项配置中的component添加子组件的对象配置,这和全局注册后在Vue的options.component添加子组件构造器的结果很相似。区别在于:

  • 1.局部注册添加的对象配置是在某个组件下,而全局注册添加的子组件是在根实例下。
  • 2.局部注册添加的是一个子组件的配置对象,而全局注册添加的是一个子类构造器。因此局部注册中缺少了一步构建子类构造器的过程,这个过程放在哪里进行呢? 回到createComponent的源码,源码中根据传入对象和构造器的分类区分局部和全局注册组件,而局部注册依然会调用 父类的extend方法去创建子类构造器。
  1. // 针对局部组件注册场景
  2. if (isObject(Ctor)) {
  3. Ctor = baseCtor.extend(Ctor);
  4. }