Vue 組件渲染詳情
前言
Vue中組件分為全局組件和局部組件:
- 全局組件:通過
Vue.component(id,definition)
方法進行註冊,並且可以在任何組件中被訪問 - 局部組件:在組件內的
components
屬性中定義,隻能在組件內訪問
下面是一個例子:
<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' }; } });
頁面中會渲染全局定義的my-button
組件和局部定義的aa
組件:
接下來筆者會詳細講解全局組件和局部組件到底是如何渲染到頁面上的,並實現相關代碼。
全局組件
Vue.component
是定義在Vue
構造函數上的一個函數,它接收id
和definition
作為參數:
id
: 組件的唯一標識definition
: 組件的配置項
在src/global-api/index.js
中定義Vue.component
方法:
export function initGlobalApi (Vue) { Vue.options = {}; // 最終會合並到實例上,可以通過vm.$options._base直接使用 Vue.options._base = Vue; // 定義全局組件 Vue.options.components = {}; initExtend(Vue); Vue.mixin = function (mixin) { this.options = mergeOptions(this.options, mixin); }; // 通過Vue.components來註冊全局組件 Vue.components = function (id, definition) { const name = definition.name = definition.name || id; // 通過Vue.extend來創建Vue的子類 definition = this.options._base.extend(definition); // 將Vue子類添加到Vue.options.components對象中,key為name this.options.components[name] = definition; }; }
Vue.component
幫我們做瞭倆件事:
- 通過
Vue.extend
利用傳入的definition
生成Vue
子類 - 將
Vue
子類放到全局Vue.options.components
中
那麼Vue.extend
是如何創建出Vue
的子類呢?下面我們來實現Vue.extend
函數
Vue.extend
Vue.extend
利用JavaScript
原型鏈實現繼承,我們會將Vue.prototype
指向Sub.prototype.__proto__
,這樣就可以在Sub
的實例上調用Vue
原型上定義的方法瞭:
Vue.extend = function (extendOptions) { const Super = this; const Sub = function VueComponent () { // 會根據原型鏈進行查找,找到Super.prototype.init方法 this._init(); }; Sub.cid = cid++; // Object.create將Sub.prototype的原型指向瞭Super.prototype Sub.prototype = Object.create(Super.prototype); // 此時prototype為一個對象,會失去原來的值 Sub.prototype.constructor = Sub; Sub.options = mergeOptions(Super.options, extendOptions); Sub.component = Super.component; return Sub; };
如果有小夥伴對JavaScript
原型鏈不太瞭解的話,可以看筆者的這篇文章: 一文徹底理解JavaScript原型與原型鏈
核心的繼承代碼如下:
const Super = Vue const Sub = function VueComponent () { // some code ... }; // Object.create將Sub.prototype的原型指向瞭Super.prototype Sub.prototype = Object.create(Super.prototype); // 此時prototype為一個對象,會失去原來的值 Sub.prototype.constructor = Sub;
Object.create
會創建一個新對象,使用一個已經存在的對象作為新對象的原型。這裡將創建的新對象賦值給瞭Sub.prototype
,相當於做瞭如下倆件事:
Sub.prototype = {}
Sub.prototype.__proto__ = Super.prototype
為Sub.prototype
賦值後,其之前擁有的constructor
屬性便會被覆蓋,這裡需要再手動指定一下Sub.prototype.constructor = Sub
最終Vue.extend
會將生成的子類返回,當用戶實例化這個子類時,便會通過this._init
執行子類的初始化方法創建組件
組件渲染流程
在用戶執行new Vue
創建組件的時候,會執行this._init
方法。在該方法中,會將用戶傳入的配置項和Vue.options
中定義的配置項進行合並,最終放到vm.$options
中:
function initMixin (Vue) { Vue.prototype._init = function (options = {}) { const vm = this; // 組件選項和Vue.options或者 Sub.options進行合並 vm.$options = mergeOptions(vm.constructor.options, options); // ... }; // ... }
執行到這裡時,mergeOptoins
會將用戶傳入options
中的components
和Vue.options.components
中通過Vue.component
定義的組件進行合並。
在merge-options.js
中,我們為strategies
添加合並components
的策略:
strategies.components = function (parentVal, childVal) { const result = Object.create(parentVal); // 合並後的原型鏈為parentVal for (const key in childVal) { // childVal中的值都設置為自身私有屬性,會優先獲取 if (childVal.hasOwnProperty(key)) { result[key] = childVal[key]; } } return result; };
components
的合並利用瞭JavaScript
的原型鏈,將Vue.options.components
中的全局組件放到瞭合並後對象的原型上,而將options
中components
屬性定義的局部組件放到瞭自身的屬性上。這樣當取值時,首先會從自身屬性上查找,然後再到原型鏈上查找,也就是優先渲染局部組件,如果沒有局部組件就會去渲染全局組件。
合並完components
之後,接下來要創建組件對應的虛擬節點:
function createVComponent (vm, tag, props, key, children) { const baseCtor = vm.$options._base; // 在生成父虛擬節點的過程中,遇到瞭子組件的自定義標簽。它的定義放到瞭父組件的components中,所有通過父組件的$options來進行獲取 // 這裡包括全局組件和自定義組件,內部通過原型鏈進行瞭合並 let Ctor = vm.$options.components[tag]; // 全局組件:Vue子類構造函數,局部組件:對象,合並後的components中既有對象又有構造函數,這裡要利用Vue.extend統一處理為構造函數 if (typeof Ctor === 'object') { Ctor = baseCtor.extend(Ctor); } props.hook = { // 在渲染真實節點時會調用init鉤子函數 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的原生標簽 return vNode(tag, props, key, children); } else { // 創建組件虛擬節點 return createVComponent(vm, tag, props, key, children); } }
在創建虛擬節點時,如果tag
不是html
中定義的標簽,便需要創建組件對應的虛擬節點。
組件虛擬節點中做瞭下面幾件事:
- 通過
vm.$options
拿到合並後的components
- 用
Vue.extend
將components
中的對象轉換為Vue
子類構造函數 - 在虛擬節點上的
props
上添加鉤子函數,方便在之後調用 - 執行
vNode
函數創建組件虛擬節點,組件虛擬節點會新增componentOptions
屬性來存放組件的一些選項
在生成虛擬節點之後,便會通過虛擬節點來創建真實節點,如果是組件虛擬節點要單獨處理:
// 處理組件虛擬節點 function createComponent (vNode) { let init = vNode.props?.hook?.init; init?.(vNode); if (vNode.componentInstance) { return true; } } // 將虛擬節點處理為真實節點 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; }
在處理虛擬節點時,我們會獲取到在創建組件虛擬節點時為props
添加的init
鉤子函數,將vNode
傳入執行init
函數:
props.hook = { // 在渲染真實節點時會調用init鉤子函數 init (vNode) { const child = vNode.componentInstance = new Ctor(); child.$mount(); } };
此時便會通過new Ctor()
來進行子組件的一系列初始化工作:
this._init
initState
- …
Ctor
是通過Vue.extend
來生成的,而在執行Vue.extend
的時候,我們已經將組件對應的配置項傳入。但是由於配置項中缺少el
選項,所以要手動執行$mount
方法來掛載組件。
在執行$mount
之後,會將組件template
創建為真實DOM
並設置到vm.$el
選項上。執行props.hook.init
方法時,將組件實例放到瞭vNode
的componentInstance
屬性上,最終在createComponent
中會判斷如果有該屬性則為組件虛擬節點,並將其對應的DOM
(vNode.componentInstance.$el
)返回,最終掛載到父節點上,渲染到頁面中。
整個渲染流程畫圖總結一下:
總結
明白瞭組件渲染流程之後,最後我們來看一下父子組件的生命周期函數的執行過程:
<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
的組件渲染流程後,便可以很輕易的解釋這個打印結果瞭:
- 首先會初始化父組件,執行父組件的
beforeCreate,created
鉤子 - 接下來會掛載父組件,在掛載之前會先執行
beforeMount
鉤子 - 當父組件開始掛載時,首先會生成組件虛擬節點,之後在創建真實及節點時,要
new SubComponent
來創建子組件,得到子組件掛載後的真實DOM
:vm.$el
- 而在實例化子組件的過程中,會執行子組件的
beforeCreate,created,beforeMount,mounted
鉤子 - 在子組件掛載完畢後,繼續完成父組件的掛載,執行父組件的
mounted
鉤子
到此這篇關於Vue 組件渲染詳情的文章就介紹到這瞭,更多相關Vue 組件渲染內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!