Vue 組件渲染詳情
前言
Vue中組件分為全局組件和局部組件:
- 全局組件:通過
Vue.component(id,definition)方法進(jìn)行注冊(cè),并且可以在任何組件中被訪問 - 局部組件:在組件內(nèi)的
components屬性中定義,只能在組件內(nèi)訪問
下面是一個(gè)例子:
<div id="app">
{{ name }}
<my-button></my-button>
<aa></aa>
</div>
Vue.components('my-button', {
template: `<button>my button</button>`
});
Vue.components('aa', {
template: `<button>global aa</button>`
});
const vm = new Vue({
el: '#app',
components: {
aa: {
template: `<button>scoped aa</button>`
},
bb: {
template: `<button>bb</button>`
}
},
data () {
return {
name: 'ss'
};
}
});頁面中會(huì)渲染全局定義的my-button組件和局部定義的aa組件:

接下來筆者會(huì)詳細(xì)講解全局組件和局部組件到底是如何渲染到頁面上的,并實(shí)現(xiàn)相關(guān)代碼。
全局組件
Vue.component是定義在Vue構(gòu)造函數(shù)上的一個(gè)函數(shù),它接收id和definition作為參數(shù):
id: 組件的唯一標(biāo)識(shí)definition: 組件的配置項(xiàng)
在src/global-api/index.js中定義Vue.component方法:
export function initGlobalApi (Vue) {
Vue.options = {};
// 最終會(huì)合并到實(shí)例上,可以通過vm.$options._base直接使用
Vue.options._base = Vue;
// 定義全局組件
Vue.options.components = {};
initExtend(Vue);
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin);
};
// 通過Vue.components來注冊(cè)全局組件
Vue.components = function (id, definition) {
const name = definition.name = definition.name || id;
// 通過Vue.extend來創(chuàng)建Vue的子類
definition = this.options._base.extend(definition);
// 將Vue子類添加到Vue.options.components對(duì)象中,key為name
this.options.components[name] = definition;
};
}Vue.component幫我們做了倆件事:
- 通過
Vue.extend利用傳入的definition生成Vue子類 - 將
Vue子類放到全局Vue.options.components中
那么Vue.extend是如何創(chuàng)建出Vue的子類呢?下面我們來實(shí)現(xiàn)Vue.extend函數(shù)
Vue.extend
Vue.extend利用JavaScript原型鏈實(shí)現(xiàn)繼承,我們會(huì)將Vue.prototype指向Sub.prototype.__proto__,這樣就可以在Sub的實(shí)例上調(diào)用Vue原型上定義的方法了:
Vue.extend = function (extendOptions) {
const Super = this;
const Sub = function VueComponent () {
// 會(huì)根據(jù)原型鏈進(jìn)行查找,找到Super.prototype.init方法
this._init();
};
Sub.cid = cid++;
// Object.create將Sub.prototype的原型指向了Super.prototype
Sub.prototype = Object.create(Super.prototype);
// 此時(shí)prototype為一個(gè)對(duì)象,會(huì)失去原來的值
Sub.prototype.constructor = Sub;
Sub.options = mergeOptions(Super.options, extendOptions);
Sub.component = Super.component;
return Sub;
};如果有小伙伴對(duì)JavaScript原型鏈不太了解的話,可以看筆者的這篇文章: 一文徹底理解JavaScript原型與原型鏈
核心的繼承代碼如下:
const Super = Vue
const Sub = function VueComponent () {
// some code ...
};
// Object.create將Sub.prototype的原型指向了Super.prototype
Sub.prototype = Object.create(Super.prototype);
// 此時(shí)prototype為一個(gè)對(duì)象,會(huì)失去原來的值
Sub.prototype.constructor = Sub;Object.create會(huì)創(chuàng)建一個(gè)新對(duì)象,使用一個(gè)已經(jīng)存在的對(duì)象作為新對(duì)象的原型。這里將創(chuàng)建的新對(duì)象賦值給了Sub.prototype,相當(dāng)于做了如下倆件事:
Sub.prototype = {}Sub.prototype.__proto__ = Super.prototype
為Sub.prototype賦值后,其之前擁有的constructor屬性便會(huì)被覆蓋,這里需要再手動(dòng)指定一下Sub.prototype.constructor = Sub
最終Vue.extend會(huì)將生成的子類返回,當(dāng)用戶實(shí)例化這個(gè)子類時(shí),便會(huì)通過this._init執(zhí)行子類的初始化方法創(chuàng)建組件
組件渲染流程
在用戶執(zhí)行new Vue創(chuàng)建組件的時(shí)候,會(huì)執(zhí)行this._init方法。在該方法中,會(huì)將用戶傳入的配置項(xiàng)和Vue.options中定義的配置項(xiàng)進(jìn)行合并,最終放到vm.$options中:
function initMixin (Vue) {
Vue.prototype._init = function (options = {}) {
const vm = this;
// 組件選項(xiàng)和Vue.options或者 Sub.options進(jìn)行合并
vm.$options = mergeOptions(vm.constructor.options, options);
// ...
};
// ...
}執(zhí)行到這里時(shí),mergeOptoins會(huì)將用戶傳入options中的components和Vue.options.components中通過Vue.component定義的組件進(jìn)行合并。
在merge-options.js中,我們?yōu)?code>strategies添加合并components的策略:
strategies.components = function (parentVal, childVal) {
const result = Object.create(parentVal); // 合并后的原型鏈為parentVal
for (const key in childVal) { // childVal中的值都設(shè)置為自身私有屬性,會(huì)優(yōu)先獲取
if (childVal.hasOwnProperty(key)) {
result[key] = childVal[key];
}
}
return result;
};components的合并利用了JavaScript的原型鏈,將Vue.options.components中的全局組件放到了合并后對(duì)象的原型上,而將options中components 屬性定義的局部組件放到了自身的屬性上。這樣當(dāng)取值時(shí),首先會(huì)從自身屬性上查找,然后再到原型鏈上查找,也就是優(yōu)先渲染局部組件,如果沒有局部組件就會(huì)去渲染全局組件。
合并完components之后,接下來要?jiǎng)?chuàng)建組件對(duì)應(yīng)的虛擬節(jié)點(diǎn):
function createVComponent (vm, tag, props, key, children) {
const baseCtor = vm.$options._base;
// 在生成父虛擬節(jié)點(diǎn)的過程中,遇到了子組件的自定義標(biāo)簽。它的定義放到了父組件的components中,所有通過父組件的$options來進(jìn)行獲取
// 這里包括全局組件和自定義組件,內(nèi)部通過原型鏈進(jìn)行了合并
let Ctor = vm.$options.components[tag];
// 全局組件:Vue子類構(gòu)造函數(shù),局部組件:對(duì)象,合并后的components中既有對(duì)象又有構(gòu)造函數(shù),這里要利用Vue.extend統(tǒng)一處理為構(gòu)造函數(shù)
if (typeof Ctor === 'object') {
Ctor = baseCtor.extend(Ctor);
}
props.hook = { // 在渲染真實(shí)節(jié)點(diǎn)時(shí)會(huì)調(diào)用init鉤子函數(shù)
init (vNode) {
const child = vNode.componentInstance = new Ctor();
child.$mount();
}
};
return vNode(`vue-component-${Ctor.id}-${tag}`, props, key, undefined, undefined, { Ctor, children });
}
function createVElement (tag, props = {}, ...children) {
const vm = this;
const { key } = props;
delete props.key;
if (isReservedTag(tag)) { // 是否為html的原生標(biāo)簽
return vNode(tag, props, key, children);
} else {
// 創(chuàng)建組件虛擬節(jié)點(diǎn)
return createVComponent(vm, tag, props, key, children);
}
}在創(chuàng)建虛擬節(jié)點(diǎn)時(shí),如果tag不是html中定義的標(biāo)簽,便需要?jiǎng)?chuàng)建組件對(duì)應(yīng)的虛擬節(jié)點(diǎn)。
組件虛擬節(jié)點(diǎn)中做了下面幾件事:
- 通過
vm.$options拿到合并后的components - 用
Vue.extend將components中的對(duì)象轉(zhuǎn)換為Vue子類構(gòu)造函數(shù) - 在虛擬節(jié)點(diǎn)上的
props上添加鉤子函數(shù),方便在之后調(diào)用 - 執(zhí)行
vNode函數(shù)創(chuàng)建組件虛擬節(jié)點(diǎn),組件虛擬節(jié)點(diǎn)會(huì)新增componentOptions屬性來存放組件的一些選項(xiàng)
在生成虛擬節(jié)點(diǎn)之后,便會(huì)通過虛擬節(jié)點(diǎn)來創(chuàng)建真實(shí)節(jié)點(diǎn),如果是組件虛擬節(jié)點(diǎn)要單獨(dú)處理:
// 處理組件虛擬節(jié)點(diǎn)
function createComponent (vNode) {
let init = vNode.props?.hook?.init;
init?.(vNode);
if (vNode.componentInstance) {
return true;
}
}
// 將虛擬節(jié)點(diǎn)處理為真實(shí)節(jié)點(diǎn)
function createElement (vNode) {
if (typeof vNode.tag === 'string') {
if (createComponent(vNode)) {
return vNode.componentInstance.$el;
}
vNode.el = document.createElement(vNode.tag);
updateProperties(vNode);
for (let i = 0; i < vNode.children.length; i++) {
const child = vNode.children[i];
vNode.el.appendChild(createElement(child));
}
} else {
vNode.el = document.createTextNode(vNode.text);
}
return vNode.el;
}在處理虛擬節(jié)點(diǎn)時(shí),我們會(huì)獲取到在創(chuàng)建組件虛擬節(jié)點(diǎn)時(shí)為props添加的init鉤子函數(shù),將vNode傳入執(zhí)行init函數(shù):
props.hook = { // 在渲染真實(shí)節(jié)點(diǎn)時(shí)會(huì)調(diào)用init鉤子函數(shù)
init (vNode) {
const child = vNode.componentInstance = new Ctor();
child.$mount();
}
};此時(shí)便會(huì)通過new Ctor()來進(jìn)行子組件的一系列初始化工作:
this._initinitState- ...
Ctor是通過Vue.extend來生成的,而在執(zhí)行Vue.extend的時(shí)候,我們已經(jīng)將組件對(duì)應(yīng)的配置項(xiàng)傳入。但是由于配置項(xiàng)中缺少el選項(xiàng),所以要手動(dòng)執(zhí)行$mount方法來掛載組件。
在執(zhí)行$mount之后,會(huì)將組件template創(chuàng)建為真實(shí)DOM并設(shè)置到vm.$el選項(xiàng)上。執(zhí)行props.hook.init方法時(shí),將組件實(shí)例放到了vNode的componentInstance 屬性上,最終在createComponent中會(huì)判斷如果有該屬性則為組件虛擬節(jié)點(diǎn),并將其對(duì)應(yīng)的DOM(vNode.componentInstance.$el)返回,最終掛載到父節(jié)點(diǎn)上,渲染到頁面中。
整個(gè)渲染流程畫圖總結(jié)一下:

總結(jié)
明白了組件渲染流程之后,最后我們來看一下父子組件的生命周期函數(shù)的執(zhí)行過程:
<div id="app">
{{ name }}
<aa></aa>
</div>
<script>
const vm = new Vue({
el: '#app',
components: {
aa: {
template: `<button>aa</button>`,
beforeCreate () {
console.log('child beforeCreate');
},
created () {
console.log('child created');
},
beforeMount () {
console.log('child beforeMount');
},
mounted () {
console.log('child mounted');
}
},
},
data () {
return {
name: 'ss'
};
},
beforeCreate () {
console.log('parent beforeCreate');
},
created () {
console.log('parent created');
},
beforeMount () {
console.log('parent beforeMount');
},
mounted () {
console.log('parent mounted');
}
});
</script>
在理解了Vue的組件渲染流程后,便可以很輕易的解釋這個(gè)打印結(jié)果了:
- 首先會(huì)初始化父組件,執(zhí)行父組件的
beforeCreate,created鉤子 - 接下來會(huì)掛載父組件,在掛載之前會(huì)先執(zhí)行
beforeMount鉤子 - 當(dāng)父組件開始掛載時(shí),首先會(huì)生成組件虛擬節(jié)點(diǎn),之后在創(chuàng)建真實(shí)及節(jié)點(diǎn)時(shí),要
new SubComponent來創(chuàng)建子組件,得到子組件掛載后的真實(shí)DOM:vm.$el - 而在實(shí)例化子組件的過程中,會(huì)執(zhí)行子組件的
beforeCreate,created,beforeMount,mounted鉤子 - 在子組件掛載完畢后,繼續(xù)完成父組件的掛載,執(zhí)行父組件的
mounted鉤子
到此這篇關(guān)于Vue 組件渲染詳情的文章就介紹到這了,更多相關(guān)Vue 組件渲染內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript使用SpreadJS創(chuàng)建Excel查看器
在現(xiàn)代的Web應(yīng)用開發(fā)中,Excel文件的處理和展示是一項(xiàng)常見的需求,小編今天將為大家展示如何借助SpreadJS來創(chuàng)建一個(gè)Excel查看器,感興趣的小伙伴可以了解下2023-12-12
ComboBox(下拉列表框)通過url加載調(diào)用遠(yuǎn)程數(shù)據(jù)的方法
這篇文章主要介紹了ComboBox(下拉列表框)通過url加載調(diào)用遠(yuǎn)程數(shù)據(jù)的方法 ,需要的朋友可以參考下2017-08-08
js change,propertychange,input事件小議
github上關(guān)于mootools一個(gè)issue的討論很有意思,所以就想測(cè)試記錄下。感興趣的可以點(diǎn)擊原頁面看看2011-12-12
JavaScript實(shí)現(xiàn)滑塊驗(yàn)證解鎖
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)滑塊驗(yàn)證解鎖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01
微信小程序獲取手機(jī)網(wǎng)絡(luò)狀態(tài)的方法【附源碼下載】
這篇文章主要介紹了微信小程序獲取手機(jī)網(wǎng)絡(luò)狀態(tài)的方法,涉及微信小程序wx.getNetworkType函數(shù)檢查網(wǎng)絡(luò)連接狀態(tài)的相關(guān)使用技巧,并附帶源碼供讀者下載參考,需要的朋友可以參考下2017-12-12

