【Vue源码】8.组件化-createComponent

1,586 阅读4分钟

组件化-createComponent

在前面几篇文章中我们写了数据驱动的流程,接下来我们就要开始了解 Vue 的组件化。组件化在 Vue 中是一个重要的组成,在官网中有一章节关于组件化的介绍 Vue 组件

所谓组件化就是把页面拆分成多个组件,组件内部包含自己的 HTMLCSSJavaScript,这样可以拼成一个模块,并且组件可以复用、拼接,等同于积木一样,一个大的页面可以由很多小的组件拼接而成。接下来我们就用一个例子来看 Vue 的组件内部是如何工作的。

import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

const app = new Vue({
  render: (h) => h(App),
}).$mount("#app");

console.log(app);

通过前面几篇文章的了解,我们知道这段代码是通过 render 函数去渲染的,render 函数调用 createElementcreateElement 根据 tag 的不同调用不同的方法生成 VNode

if (config.isReservedTag(tag)) {
  // platform built-in elements
  // 创建HTML内置类型的VNode节点
  vnode = new VNode(
    config.parsePlatformTagName(tag),
    data,
    children,
    undefined,
    undefined,
    context
  );
} else if (isDef((Ctor = resolveAsset(context.$options, "components", tag)))) {
  // component
  // 创建组件类型的VNode节点
  vnode = createComponent(Ctor, data, context, children, tag);
} else {
  // unknown or unlisted namespaced elements
  // check at runtime because it may get assigned a namespace when its
  // parent normalizes children
  vnode = new VNode(tag, data, children, undefined, undefined, context);
}

在例子中,我们可以看到传入的是一个名为 App 的对象,所以继续执行 createComponent

createComponent

createComponent 方法定义在 src/core/vdom/create-component.js 中。

export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return;
  }

  // 根实例
  // 在 initGlobalAPI 中定义的 Vue.options._base = Vue
  const baseCtor = context.$options._base;

  if (isObject(Ctor)) {
    // 一:
    // 通过 Vue 的 extend 方法,生成子类构造函数,使得子类也有Vue根实例的一些方法
    // 其实就是构造Vue的子类
    // src/core/global-api/extend.js
    Ctor = baseCtor.extend(Ctor);
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== "function") {
    if (process.env.NODE_ENV !== "production") {
      warn(`Invalid Component definition: ${String(Ctor)}`, context);
    }
    return;
  }

  // 异步组件
  let asyncFactory;
  if (isUndef(Ctor.cid)) {
    // ...
  }

  data = data || {};

  // 解析构造函数选项,如果全局混合后应用
  // 创建组件构造函数
  resolveConstructorOptions(Ctor);

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag);

  // 函数式组件
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children);
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on;
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn;

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot;
    data = {};
    if (slot) {
      data.slot = slot;
    }
  }

  // 二:安装组件钩子函数
  installComponentHooks(data);

  // return a placeholder vnode
  // 三:实例化VNode并返回。需要注意组件vnode没有children,这点在之后的patch在分析。
  const name = Ctor.options.name || tag;
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  );
  return vnode;
}

createComponent 在这里主要做了三件事

  1. 把传入的组件对象构造成 Vue 的子类
  2. 安装组件钩子函数
  3. 实例化 VNode 并返回

把传入的组件对象构造成 Vue 的子类

// 根实例
// 在 initGlobalAPI 中定义的 Vue.options._base = Vue
const baseCtor = context.$options._base;

if (isObject(Ctor)) {
  // 一:
  // 通过 Vue 的 extend 方法,生成子类构造函数,使得子类也有Vue根实例的一些方法
  // 其实就是构造Vue的子类
  // src/core/global-api/extend.js
  Ctor = baseCtor.extend(Ctor);
}

在我们书写组件的时候是这样的

import HelloWorld from "./components/HelloWorld";

export default {
  name: "app",
  components: {
    HelloWorld,
  },
};

其实最后导出的是一个对象,因为是个对象,所以代码逻辑会走到 Ctor = baseCtor.extend(Ctor) 在这里 baseCtorVue 的根实例。 然后执行 baseCtor.extend 将我们传入的对象,转化为构造函数。

extend 方法定义在 src/core/global-api/extend.js

  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      // 读取缓存里的
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

    // Vue实例的初始化逻辑,初始化的同时添加全局api
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 扩展options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    // 对 props 和 computed 初始化
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // 允许进一步使用扩展/mixin/插件
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // 缓存起来
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

extend 方法主要下面几件事

  1. 查看是否有缓存,有则直接返回
  2. 调用_init方法构造组件的构造函数赋值给 Sub
  3. 扩展 Suboptions
  4. 初始化 Subpropscomputed
  5. Sub 构造函数缓存起来

安装组件钩子函数

// install component management hooks onto the placeholder node
installComponentHooks(data);
const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ));
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions;
    const child = (vnode.componentInstance = oldVnode.componentInstance);
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  },

  insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode;
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true;
      callHook(componentInstance, "mounted");
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance);
      } else {
        activateChildComponent(componentInstance, true /* direct */);
      }
    }
  },

  destroy(vnode: MountedComponentVNode) {
    const { componentInstance } = vnode;
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy();
      } else {
        deactivateChildComponent(componentInstance, true /* direct */);
      }
    }
  },
};

const hooksToMerge = Object.keys(componentVNodeHooks);

function installComponentHooks(data: VNodeData) {
  const hooks = data.hook || (data.hook = {});
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i];
    const existing = hooks[key];
    const toMerge = componentVNodeHooks[key];
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
    }
  }
}

function mergeHook(f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b);
    f2(a, b);
  };
  merged._merged = true;
  return merged;
}

作用:把 componentVNodeHooks 的钩子函数合并到 data.hook 里,在 VNode 执行 patch 过程中执行相关钩子函数。 注意:合并过程中如果已经存在,那么执行 mergeHook 返回一个 merged 函数,最终执行的时候,依次执行这两个钩子函数 下面是具体代码。

实例化 VNode 并返回

const name = Ctor.options.name || tag;
const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
  data,
  undefined,
  undefined,
  undefined,
  context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
);

实例化 VNode 并返回。需要注意组件 vnode 没有 children,这点在之后的 patch 在分析。

总结

08.组件化-createComponent.png

这一节我们分析了当传入组件(对象)时,createElement 是如何调用 createComponent 处理的,他先将对象(组件)转换成 Vue 的子类构造函数,然后安装钩子函数,最后实例化 VNode 并返回。那么在_render 方法中就得到了 VNode,最终要执行的是 _update,也就是 patch 函数,我们之前了解过了 patch 函数,接下来我们进一步的深入 patch 函数。

源码分析 GitHub 地址