Vue源码探秘(九)(createComponent)

引言

Vue.js 中的一个核心思想是组件化。所谓组件化,就是把页面拆分红多个组件 (component),每一个组件依赖的 CSSJavaScript模板图片等资源放在一块儿开发和维护。组件化思想容许咱们使用小型、独立和一般可复用的组件构建大型应用。几乎任意类型的应用界面均可以抽象为一个组件树,这里参考官网的一张图来讲明: javascript

接下来的几篇文章,我会带你们一块儿来看下组件化相关的源码,了解这块有助于咱们了解组件化的思想。html

本小节咱们先来看下createComponent函数的实现。vue

从一个简单示例开始

回顾Vue 源码探秘(五)(_render 函数的实现,咱们是这么编写render函数的:html5

new Vue({
// 这里的 h 是 createElement 方法
render: function(h) {
return h(
"div",
{
attrs: {
id: "app"
}
},
this.message
);
}
});
复制代码

而若是使用单文件组件,咱们须要这样编写render函数:java

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

var app = new Vue({
el: "#app",
// 这里的 h 是 createElement 方法
render: h => h(App)
});
复制代码

上面两种编写方式有什么不一样呢?很显然两种编写render函数的方式都是经过 render 函数去渲染的,不一样的是此次经过 createElement 传的参数是一个组件而不是一个原生的标签。下面咱们就结合上面这个例子开始分析。node

createComponent

回顾Vue 源码探秘(七)(createElement),咱们在分析_createElement函数时,有这么一段代码:ios

// src/core/vdom/create-element.js

export function _createElement(): VNode | Array<VNode> {
// ...
// ...

if (typeof tag === "string") {
// ...
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}

// ...
}
复制代码

这里对参数 tag 进行了判断,若是是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,不然经过 createComponent 方法建立一个组件 VNodecreateComponent 函数定义在 src/core/vdom/create-component.js 中,咱们分段来分析:git

// 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;
}

const baseCtor = context.$options._base;

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}

// ...
}
复制代码

函数一开始将 vm.$option_base 属性赋给 baseCtor。在这里 baseCtor 实际上就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数有这么一段逻辑:github

// src/core/global-api/index.js

export function initGlobalAPI(Vue: GlobalAPI) {
// ...

// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue;

// ...
}
复制代码

能够看到这里定义的是Vue.options,而咱们在createComponent中取的是context.$options。这块实际上是在src/core/instance/init.jsVue 原型上的 _init 方法中处理的:web

// src/core/instance/init.js

Vue.prototype._init = function(options?: Object) {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
};
复制代码

这里调用了mergeOptions函数,将Vue.options合并到vm.$options上,所以这里就能够经过vm.$options._base 拿到 Vue 构造函数。

回到createComponent函数,接下来判断Ctor是否是对象。这里的Ctor是指什么呢?先来看一下咱们平时常常编写的单文件组件:

<template>
// ...
</template>
<script>
export default {
name: 'App'
}
</script>

复制代码

Ctor 就是单文件组件导出的对象。这里调用了 Vue.extend(Ctor)

Vue.extend( options )使用基础 Vue 构造器,建立一个“子类”。参数是一个包含组件选项的对象。具体参考https://cn.vuejs.org/v2/api/#Vue-extend

extend 定义在 src/core/global-api/extend.js 文件中,咱们分段来分析它的实现原理:

// 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);
}

// ...
};
复制代码

extend 函数首先作了一些初始化工做,这里定义的 cachedCtors 的具体做用在下文会介绍。而后调用 validateComponentName 函数对 extendOptionsname 属性(也就是组件名)进行校验。validateComponentName 函数代码以下:

// src/core/util/options.js
export function validateComponentName(name: string) {
if (
!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)
) {
warn(
'Invalid component name: "' +
name +
'". Component names ' +
"should conform to valid custom element name in html5 specification."
);
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
"Do not use built-in or reserved HTML elements as component " +
"id: " +
name
);
}
}
复制代码

第一个if语句是检查组件名是否符合 HTML5 自定义元素的命名规范。第二个if语句检查组件名是否和内置 HTML 元素命名冲突。回到 extend 函数,继续往下看:

// src/core/global-api/extend.js
Vue.extend = function(extendOptions: Object): Function {
// ...

const Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
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.
if (Sub.options.props) {
initProps(Sub);
}
if (Sub.options.computed) {
initComputed(Sub);
}

// allow further extension/mixin/plugin usage
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];
});
};
复制代码

这一段代码定义了子类的构造函数 Sub,而后对 Sub 这个对象自己扩展了一些属性,如扩展 options、添加全局 API 等;而且对配置中的 propscomputed 作了初始化工做。

继续看 extend 函数最后一段代码:

// src/core/global-api/extend.js
Vue.extend = function(extendOptions: Object): Function {
// ...

// 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);

// cache constructor
cachedCtors[SuperId] = Sub;
return Sub;
};
复制代码

这里其实就是将建立好的构造函数 Sub 保存到组件的属性中做缓存,若是这个组件被其余组件屡次引用,那么这个组件会屡次做为参数传给 extend 函数,这样检查到以前的缓存就能够直接将 Sub 返回而不用从新构造了。

这样也就解释了上面提到的cachedCtors的做用了。

分析完 extend 函数,咱们回到 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 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;
}

// async component
let asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
}
}

data = data || {};

// resolve constructor options in case global mixins are applied after
// component constructor creation
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);

// functional component
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;
}
}

// ...
}
复制代码

这里首先判断若是Ctor不是函数则抛出警告并结束函数,接下来的一段是与异步组件相关,异步组件相关的我会在后面单独出一节来分析。

而后对data作初始化处理,并调用 resolveConstructorOptions 解析构造函数 Ctoroptions

接下来这一段涉及到了 v-model 指令,和异步组件同样,也会在后面单独出一节介绍 v-model

后面的代码和 props函数式组件监听器相关,这里都先略过。继续往下:

// 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
{
// ...

// install component management hooks onto the placeholder node
installComponentHooks(data);

// ...
}
复制代码

这一步是调用 installComponentHooks 函数来安装组件钩子函数,来看 installComponentHooks 函数的代码:

// src/core/vdom/create-component.js
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;
}
}
}
复制代码

这里的hooksToMergecomponentVNodeHooks是什么呢?来看下它们的定义:

// src/core/vdom/create-component.js
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);
复制代码

能够看到,componentVNodeHooks 定义了四个钩子函数。

咱们以前提到 Vue.js 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特色是在 VNodepatch 流程中对外暴露了各类时机的钩子函数,方便咱们作一些额外的事情。

整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程当中执行相关的钩子函数。

这里要注意一下合并策略mergeHook,看下代码:

// src/core/vdom/create-component.js

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;
}
复制代码

mergeHook 函数逻辑很简单,所谓合并就是先执行 componentVNodeHooks 定义的再执行 data.hooks 定义的,再将合并标志位设为 true

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
{
// ...

// return a placeholder 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
);

// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode);
}

return vnode;
}
复制代码

最后这一段的逻辑是生成一个 VNode 并返回。这里须要注意的是因为组件 VNode 是没有 children 的,因此这里 new VNode 的第三个参数 childrenundefined

总结

这一节咱们分析了 createComponent 函数的执行流程,它有三个关键的步骤:

  • 构建子类构造函数
  • 安装组件钩子函数
  • 建立 VNode并返回

createComponent 后返回的是组件 vnode,它也同样走到 vm._update 方法,进而执行了 patch 函数。咱们已经研究过针对普通 VNode 节点的状况了,下一节咱们将研究 __patch__ 怎么把组件的 VNode 转换成真实 DOM

相关文章
相关标签/搜索