Vue 組件渲染詳情
前言
Vue中組件分為全局組件和局部組件:
- 全局組件:通過(guò)
Vue.component(id,definition)
方法進(jìn)行注冊(cè),并且可以在任何組件中被訪問(wèn) - 局部組件:在組件內(nèi)的
components
屬性中定義,只能在組件內(nèi)訪問(wèn)
下面是一個(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' }; } });
頁(yè)面中會(huì)渲染全局定義的my-button
組件和局部定義的aa
組件:
接下來(lái)筆者會(huì)詳細(xì)講解全局組件和局部組件到底是如何渲染到頁(yè)面上的,并實(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í)例上,可以通過(guò)vm.$options._base直接使用 Vue.options._base = Vue; // 定義全局組件 Vue.options.components = {}; initExtend(Vue); Vue.mixin = function (mixin) { this.options = mergeOptions(this.options, mixin); }; // 通過(guò)Vue.components來(lái)注冊(cè)全局組件 Vue.components = function (id, definition) { const name = definition.name = definition.name || id; // 通過(guò)Vue.extend來(lái)創(chuàng)建Vue的子類 definition = this.options._base.extend(definition); // 將Vue子類添加到Vue.options.components對(duì)象中,key為name this.options.components[name] = definition; }; }
Vue.component
幫我們做了倆件事:
- 通過(guò)
Vue.extend
利用傳入的definition
生成Vue
子類 - 將
Vue
子類放到全局Vue.options.components
中
那么Vue.extend
是如何創(chuàng)建出Vue
的子類呢?下面我們來(lái)實(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ì)失去原來(lái)的值 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ì)失去原來(lái)的值 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ì)通過(guò)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
中通過(guò)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)先渲染局部組件,如果沒(méi)有局部組件就會(huì)去渲染全局組件。
合并完components
之后,接下來(lái)要?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)的過(guò)程中,遇到了子組件的自定義標(biāo)簽。它的定義放到了父組件的components中,所有通過(guò)父組件的$options來(lái)進(jìn)行獲取 // 這里包括全局組件和自定義組件,內(nèi)部通過(guò)原型鏈進(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)中做了下面幾件事:
- 通過(guò)
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
屬性來(lái)存放組件的一些選項(xiàng)
在生成虛擬節(jié)點(diǎn)之后,便會(huì)通過(guò)虛擬節(jié)點(diǎn)來(lái)創(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ì)通過(guò)new Ctor()
來(lái)進(jìn)行子組件的一系列初始化工作:
this._init
initState
- ...
Ctor
是通過(guò)Vue.extend
來(lái)生成的,而在執(zhí)行Vue.extend
的時(shí)候,我們已經(jīng)將組件對(duì)應(yīng)的配置項(xiàng)傳入。但是由于配置項(xiàng)中缺少el
選項(xiàng),所以要手動(dòng)執(zhí)行$mount
方法來(lái)掛載組件。
在執(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)上,渲染到頁(yè)面中。
整個(gè)渲染流程畫(huà)圖總結(jié)一下:
總結(jié)
明白了組件渲染流程之后,最后我們來(lái)看一下父子組件的生命周期函數(shù)的執(zhí)行過(guò)程:
<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
鉤子 - 接下來(lái)會(huì)掛載父組件,在掛載之前會(huì)先執(zhí)行
beforeMount
鉤子 - 當(dāng)父組件開(kāi)始掛載時(shí),首先會(huì)生成組件虛擬節(jié)點(diǎn),之后在創(chuàng)建真實(shí)及節(jié)點(diǎn)時(shí),要
new SubComponent
來(lái)創(chuàng)建子組件,得到子組件掛載后的真實(shí)DOM
:vm.$el
- 而在實(shí)例化子組件的過(guò)程中,會(huì)執(zhí)行子組件的
beforeCreate,created,beforeMount,mounted
鉤子 - 在子組件掛載完畢后,繼續(xù)完成父組件的掛載,執(zhí)行父組件的
mounted
鉤子
到此這篇關(guān)于Vue 組件渲染詳情的文章就介紹到這了,更多相關(guān)Vue 組件渲染內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Bootstrap輸入框組件簡(jiǎn)單實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Bootstrap輸入框組件的簡(jiǎn)單實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03解決ie img標(biāo)簽內(nèi)存泄漏的問(wèn)題
下面小編就為大家?guī)?lái)一篇解決ie img標(biāo)簽內(nèi)存泄漏的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10深入理解JavaScript系列(4) 立即調(diào)用的函數(shù)表達(dá)式
大家學(xué)JavaScript的時(shí)候,經(jīng)常遇到自執(zhí)行匿名函數(shù)的代碼,今天我們主要就來(lái)想想說(shuō)一下自執(zhí)行2012-01-01js自動(dòng)下載文件到本地的實(shí)現(xiàn)代碼
其實(shí)就是利用了Microsoft.XMLHTTP實(shí)現(xiàn)遠(yuǎn)程文件的保存,不過(guò)需要修改下才可以運(yùn)行,地址傳參問(wèn)題2013-04-04元素未顯示設(shè)置width/height時(shí)IE中使用currentStyle獲取為auto
元素未顯示設(shè)置width/height時(shí)IE中無(wú)法使用currentStyle獲取,默認(rèn)獲取值為auto,需要的朋友可以參考下2014-05-05JS循環(huán)中正確使用async、await的姿勢(shì)分享
async?/?await是ES7的重要特性之一,也是目前社區(qū)里公認(rèn)的優(yōu)秀異步解決方案,下面這篇文章主要給大家介紹了關(guān)于JS循環(huán)中正確使用async、await的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2021-12-12微信小程序 this.triggerEvent()的具體使用
這篇文章主要介紹了微信小程序 this.triggerEvent()的具體使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12