引言
Vue.js
中的一個核心思想是元件化
。所謂元件化,就是把頁面拆分成多個元件 (component
),每個元件依賴的 CSS
、JavaScript
、模板
、圖片
等資源放在一起開發和維護。元件化思想允許我們使用小型、獨立和通常可複用的元件構建大型應用。幾乎任意型別的應用介面都可以抽象為一個元件樹,這裡參考官網的一張圖來說明:
接下來的幾篇文章,我會帶大家一起來看下元件化
相關的原始碼,瞭解這塊有助於我們瞭解元件化的思想。
本小節我們先來看下createComponent
函式的實現。
從一個簡單示例開始
回顧Vue 原始碼探祕(五)(_render 函式的實現,我們是這麼編寫render
函式的:
new Vue({
// 這裡的 h 是 createElement 方法
render: function(h) {
return h(
"div",
{
attrs: {
id: "app"
}
},
this.message
);
}
});
複製程式碼
而如果使用單檔案元件,我們需要這樣編寫render
函式:
import Vue from "vue";
import App from "./App.vue";
var app = new Vue({
el: "#app",
// 這裡的 h 是 createElement 方法
render: h => h(App)
});
複製程式碼
上面兩種編寫方式有什麼不同呢?很顯然兩種編寫render
函式的方式都是通過 render
函式去渲染的,不同的是這次通過 createElement
傳的引數是一個元件而不是一個原生的標籤。下面我們就結合上面這個例子開始分析。
createComponent
回顧Vue 原始碼探祕(七)(createElement),我們在分析_createElement
函式時,有這麼一段程式碼:
// 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
方法建立一個元件 VNode
。createComponent
函式定義在 src/core/vdom/create-component.js
中,我們分段來分析:
// 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
函式有這麼一段邏輯:
// 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.js
裡 Vue
原型上的 _init
方法中處理的:
// 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
函式對 extendOptions
的 name
屬性(也就是元件名)進行校驗。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
等;並且對配置中的 props
和 computed
做了初始化工作。
繼續看 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
解析建構函式 Ctor
的 options
。
接下來這一段涉及到了 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;
}
}
}
複製程式碼
這裡的hooksToMerge
和componentVNodeHooks
是什麼呢?來看下它們的定義:
// 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
,它的一個特點是在VNode
的patch
流程中對外暴露了各種時機的鉤子函式,方便我們做一些額外的事情。
整個 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
的第三個引數 children
是 undefined
。
總結
這一節我們分析了 createComponent
函式的執行流程,它有三個關鍵的步驟:
構建子類建構函式 安裝元件鉤子函式 建立 VNode
並返回
createComponent
後返回的是元件 vnode
,它也一樣走到 vm._update
方法,進而執行了 patch
函式。我們已經研究過針對普通 VNode
節點的情況了,下一節我們將研究 __patch__
怎麼把元件的 VNode
轉換成真實 DOM
。