Vue中的組件從初始化到掛載經歷了什麼

NO IMAGE

下面的所有解析都以這段代碼為基準:

new Vue({
el: "#app",
render: h => h(AppSon)
});

其中 AppSon 就是組件,它是一個對象:

const AppSon = {
name: "app-son",
data() {
return {
msg: 123
};
},
render(h) {
return h("span", [this.msg]);
}
};

這樣一段代碼,在 Vue 內部組件化的流程順序:

  1. $createElement,其實 render 接受的參數 h 就是this.$createElement的別名
  2. createElement,做一下參數的整理,就進入下一步
  3. _createElement,比較關鍵的一步,在這個方法裡會判斷組件是span這樣的 html 標籤,還是用戶寫的自定義組件。
  4. createComponent,生成組件的 vnode,安裝一些 vnode 的生命週期,返回 vnode

其實,render 函數最終返回的就是vnode

流程解析

$createElement

調用createElement方法,第一個參數是 vm 實例自身,剩餘的參數原封不動的透傳。

vm.$createElement = function(a, b, c, d) {
return createElement(vm, a, b, c, d, true);
};

createElement

function createElement (
// 上一步傳進來的vm實例,在哪個組件的render裡調用,context就是哪個組件的實例。
context,
// 在例子中,就是AppSon這個對象
tag,
// 可以傳入props等交給子組件的選項
data,
// 子組件中間的內容
children,
...
)

之後有一個判斷

if (typeof tag === "string") {
// html標籤流程
} else {
// 組件化流程
vnode = createComponent(tag, data, context, children);
}

createComponent接受的四個參數就是上文的方法傳進去的

createComponent

function createComponent(
// 還是上文中的tag,本文中是AppSon對象
Ctor,
// 下面的都一致
data,
context,
children
) {
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// 給vnode安裝一些生命週期函數(注意這裡是vnode的生命週期,而不是created那些組件聲明週期)
installComponentHooks(data);
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
);
return vnode;
}

下面有一個邏輯

if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}

其中baseCtor.extend(Ctor)就可以暫時理解為 Vue.extend,這是一個全局共用方法,從名字也可以看出它主要是做一些繼承,讓子組件的也擁有父組件的一些能力,這個方法返回的是一個新的構造函數。

組件對象最終都會用 extend 這個 api 變成一個組件構造函數,這個構造函數繼承了父構造函數 Vue 的一些屬性

extend 函數具體做了什麼呢?

createComponent / Vue.extend

Vue.extend = function(extendOptions) {
extendOptions = extendOptions || {};
// this在這個例子其實就是Vue。
var Super = this;
// Appson這個組件的構造函數
var Sub = function VueComponent(options) {
// 這個_init就是調用的Vue.prototype._init
this._init(options);
};
// 把Vue.prototype生成一個
// { __proto__: Vue.prototype }這樣的對象,
// 直接賦值給子組件構造函數的prototype
// 此時子組件構造函數的原型鏈上就可以拿到Vue的原型鏈的屬性了
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
// 合併Vue.option上的一些全局配置
Sub.options = mergeOptions(Super.options, extendOptions);
Sub["super"] = Super;
// 拷貝靜態函數
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
// 返回子組件的構造函數
return Sub;
};

到了這一步,我們一開始定義的 Appson 組件對象,已經變成了一個函數,可以通過 new AppSon()來生成一個組件實例了,並且組件配置對象被合併到了Sub.options這個構造函數的靜態屬性上。

createComponent / installComponentHooks

installComponentHooks這個方法是為了給 vnode 上加入一些生命週期函數,

其中有一個init生命週期,這個週期後面被調用的時候再講解。

createComponent / new VNode

可以看出,主要是生成 vnode 的實例,並且賦值給vnode.componentInstance,並且調用$mount方法掛載 dom 節點,注意這個init生命週期此時還沒有調用。

到這為止render的流程就講完了,現在我們擁有了一個vnode節點,它有一些關鍵的屬性

  1. vnode.componentOptions.Ctor: 上一步extend生成的子組件構造函數。
  2. vnode.data.hook: 裡面保存了init等 vnode 生命週期方法
  3. vnode.context: 調用$createElement 的是哪個實例,這個 context 就是誰。

$mount

最外層的組件調用了$mount後,組件在初次渲染的時候其實是遞歸去調用createElm的,而createElm中會去調用組件 vnode 的init鉤子。

if (isDef((i = i.hook)) && isDef((i = i.init))) {
i(vnode);
}

然後就會走進 vnode 的init生命週期的邏輯

const child = (vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
));
child.$mount(vnode.elm);

createComponentInstanceForVnode:

createComponentInstanceForVnode (
vnode: any,
parent: any,
): Component {
const options: InternalComponentOptions = {
// 標記這是一個組件節點
_isComponent: true,
// Appson組件的vnode
_parentVnode: vnode,
// 當前正在活躍的父組件實例,在本例中就是根Vue實例
// new Vue({
//   el: "#app",
//   render: h => h(AppSon)
// });
parent
}
return new vnode.componentOptions.Ctor(options)
}

可以看出,最終調用組件構造函數,然後調用\_init 方法,它接受到的 options 不再是

{
data() {
},
props: {
},
methods() {
}
}

這樣的傳統 Vue 對象了,而是

{
_isComponent: true,
_parentVnode: vnode,
parent,
}

這樣的一個對象,然後_init 內部會針對這樣特徵的對象,調用initInternalComponent做一些特殊的處理,
這裡有一個疑惑點,那剛剛子組件聲明的 data 那些選項哪去了呢?
其實是被保存在Ctor.options裡了。

然後在initInternalComponent中,把子組件構造函數上保存的 options 再轉移到vm.$options.__proto__上。

var opts = (vm.$options = Object.create(vm.constructor.options));

之後生成了子組件的實例後,又會調用child.$mount(vnode.elm),繼續的去遞歸這個初始化的過程。

相關文章

手把手帶你入門AST抽象語法樹

淺析Node進程與線程

Github標星19K+Star,10分鐘自建對象存儲服務!

拖拽排列卡片組件