傳統的頁面開發主張將內容、樣式和行爲分開,便於開發和維護。等到React、Vue等MVVM前端框架大行其道時,人們更傾向於使用html、css、js聚合在一塊兒建立組件,經過編寫小型、獨立和一般可複用的組件來構建大型應用。
組件是現代開發框架的基石,下面詳細介紹Vue組件的實現原理。
css
在Vue中組件註冊分爲兩種:局部註冊、全局註冊。全局註冊是經過 Vue.component 方法進行的,局部註冊是經過在實例化組件時添加 components 選項完成的。
下面詳細介紹組件註冊以及相關內容。
html
Vue.options 的 components 屬性是在 /src/core/global-api/index.js 文件中調用 initGlobalAPI 函數來定義的。
前端
initGlobalAPI(Vue)
// initGlobalAPI 代碼
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
// ASSET_TYPES
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
// builtInComponents
import KeepAlive from './keep-alive'
export default { KeepAlive }
複製代碼
在 /src/platforms/web/runtime/index.js 文件中會對 Vue.options.components 進一步賦值。
vue
import platformComponents from './components/index'
extend(Vue.options.components, platformComponents)
// platformComponents
import Transition from './transition'
import TransitionGroup from './transition-group'
export default {
Transition,
TransitionGroup
}
複製代碼
最終 Vue.options.components 中會包含三個內置組件:
node
Vue.options.components = {
KeepAlive: {/* ... */},
Transition: {/* ... */},
TransitionGroup: {/* ... */}
}
複製代碼
在《選項合併》一文中講過,資源選項的合併是經過 mergeAssets 函數進行的。合併策略是以父選項對象爲原型,所以:
react
// vm 爲Vue實例,即Vue組件
vm.$options.components.prototype = Vue.options.components = {
KeepAlive: {/* ... */},
Transition: {/* ... */},
TransitionGroup: {/* ... */}
}
複製代碼
Vue.extends 用來根據傳入的配置選項建立一個Vue構造函數的「子類」,精簡代碼以下:
web
Vue.extend = function (extendOptions) {
/* ... */
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
)
/* ... */
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
if (name) {
Sub.options.components[name] = Sub
}
/* ... */
return Sub
}
複製代碼
能夠看到 Vue.extend 返回的函數 VueComponent 跟Vue構造函數同樣,都是調用 _init 方法進行初始化。VueComponent 函數自身也會添加跟Vue相同的靜態屬性和方法。
VueComponent 與 Vue 的主要區別是靜態屬性 options 不一樣。VueComponent.options 是將 Vue.extend 參數和原有構造函數的 options 參數經過 mergeOptions 函數進行合併而獲得的。
另外,會將構造函數添加到自身的 options.components 對象屬性上,也就是說經過 VueComponent 實例化的對象上的屬性 $options.components.prototype 上除了內置組件還會有自定義組件的構造函數。
api
Vue關於資源的靜態方法(Vue.component、Vue.directive、Vue.filter)定義以下:
數組
initAssetRegisters(Vue)
function initAssetRegisters (Vue) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (id,definition ){
if (!definition) {
return this.options[type + 's'][id]
} else {
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
複製代碼
單看 Vue.component 方法其定義以下所示:
前端框架
Vue.component = function (id, definition){
if (!definition) {
return this.options.components[id]
} else {
// 組件名合法性檢測
validateComponentName(id)
if (isPlainObject(definition)) {
definition.name = definition.name || id
definition = Vue.extend(definition)
}
Vue.options.components[id] = definition
return definition
}
}
複製代碼
由以上代碼可知,全局註冊的實質是根據全局註冊組件選項生成Vue子構造函數,而後將該子構造函數添加到Vue.options.components對象上。
使用 components 選項來註冊組件,會將要註冊組件信息存儲在當前組件實例的 $options.components 對象上。
在局部組件根據渲染函數生成對應VNode時,是由 createComponent 函數來最終生成VNode的。
function createComponent (Ctor,data,context,children,tag) {
/*...*/
var baseCtor = context.$options._base;
// Ctor 爲組件配置對象
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
/*...*/
}
複製代碼
由上述代碼可知,在生成VNode的過程當中會調用所在組件實例的 extend 方法根據註冊信息生成對應的子構造函數。
組件的解析過程和普通標籤同樣:
一、根據模板生成渲染函數。
二、根據渲染函數生成虛擬DOM。
三、根據虛擬DOM生成真實DOM。
下面以一個簡單的例子來講明組件的解析過程:
<body>
<div id="app"></div>
</body>
<script> var ComponentA = { template: '<div>組件A</div>' } var vue = new Vue({ el: '#app', template: `<div id="app" class="home"><component-a></component-a></div>`, components: { "component-a": ComponentA } }) </script>
複製代碼
模板中組件生成的渲染函數比較簡單,跟標籤同樣由 _c() 函數包裹。_c() 的第一個參數爲組件名,第二個參數爲組件屬性對象,第三個參數爲使用 <slot> 接收的內容。
function anonymous() {
with(this){
return _c(
'div',
{staticClass:"home",attrs:{"id":"app"}},
[_c('component-a')],
1
)
}
}
複製代碼
組件的具體配置參數信息存儲在 vm.$options.components 中:
vm.$options.components = {
"component-a" : {
template: "<div>組件A</div>"
}
}
複製代碼
組件生成VNode是調用渲染函數中的 _c() 完成的,_c() 最終會調用 _createElement 來生成VNode。_createElement 中關於組件處理的代碼以下所示:
// context 爲當前組件實例
if ((!data || !data.pre) &&
isDef(Ctor = resolveAsset(context.$options, 'components', tag){
vnode = createComponent(Ctor, data, context, children, tag);
}
複製代碼
resolveAsset 對組件類型資源的處理代碼以下:
function resolveAsset (options,type,id,warnMissing) {
if (typeof id !== 'string') { return }
var assets = options[type];
if (hasOwn(assets, id)) { return assets[id] }
var camelizedId = camelize(id);
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
var PascalCaseId = capitalize(camelizedId);
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (warnMissing && !res) {
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id,options);
}
return res
}
複製代碼
該函數對組件的處理比較有意思,首先是關於組件名稱的問題:在模板中使用的組件名稱,在組件註冊時能夠有三種形式。註冊時能夠跟使用時保持一致,也可使用駝峯命名或者首字母大寫的駝峯命名。
其次是關於組件局部註冊以及全局註冊的問題:局部註冊的組件會保存在 vm.$options.components 中,全局註冊的組件保存在 Vue.options.components 中,而 Vue.options.components 在 vm.$options.components 的原型鏈上。
resolveAsset 函數查詢組件註冊信息會先查註冊的局部變量,若是找不到再沿着原型鏈查詢。這就是局部組件只能自身使用,全局註冊的組件可以全局使用的緣由。
組件VNode生成函數 createComponent 的精簡代碼以下所示:
function createComponent (Ctor,data,context,children,tag){
if (isUndef(Ctor)) { return }
const baseCtor = context.$options._base
if (isObject(Ctor)) {Ctor = baseCtor.extend(Ctor)}
/* 省略異步組件相關處理代碼 */
data = data || {}
resolveConstructorOptions(Ctor)
/* 省略v-model相關處理代碼 */
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
/* 省略函數式組件相關處理代碼 */
const listeners = data.on
data.on = data.nativeOn
/* 省略抽象組件相關處理代碼 */
installComponentHooks(data)
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相關代碼 */
return vnode
}
複製代碼
首先是根據局部組件註冊信息調用 extend 方法生成子構造函數,而後調用 resolveConstructorOptions 函數來更新子構造函數的 options 屬性。這裏會有一個疑問:在 extend 方法中已經使用 mergeOptions 方法完成對子構造函數 options 屬性合併更新,爲何還要調用 resolveConstructorOptions 函數處理 options?
function resolveConstructorOptions (Ctor) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
Ctor.superOptions = superOptions
const modifiedOptions = resolveModifiedOptions(Ctor)
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
複製代碼
這是爲了防止在組件構造函數建立之後使用全局 mixins 更改父構造函數的選項,resolveConstructorOptions 函數的做用就是根據原型鏈上對象的 options 值來更新子構造函數的 options。
接着調用 extractPropsFromVNodeData 函數來從當前實例中提取局部組件 props 的值,調用 installComponentHooks 來在 data 屬性上安裝組件的鉤子函數。
最後使用 new VNode() 來生成組件類型VNode,傳入的第一個參數是根據組件名拼接處理的;第三個參數不傳,也就是說組件VNode沒有 children 屬性;與生成其餘類型VNode不一樣,第七個參數會傳入組件選項對象 componentOptions;第八個參數會根據是否爲異步組件而傳入不一樣的值。
組件鉤子安裝函數 installComponentHooks 以及相關代碼以下所示:
const componentVNodeHooks = {
init (vnode, hydrating) {/* 省略具體實現 */},
prepatch (oldVnode, vnode) {/* 省略具體實現 */},
insert (vnode) {/* 省略具體實現 */},
destroy (vnode) {/* 省略具體實現 */}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
function installComponentHooks (data) {
var hooks = data.hook || (data.hook = {});
for (var i = 0; i < hooksToMerge.length; i++) {
var key = hooksToMerge[i];
var existing = hooks[key];
var toMerge = componentVNodeHooks[key];
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
}
}
}
function mergeHook (f1, f2) {
const merged = (a, b) => {
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
複製代碼
組件鉤子函數生成邏輯比較簡單:將 data.hook 與 componentVNodeHooks 中的函數加以合併,合併策略爲將同名函數合併到同一函數中。
若是本來 data.hook 中沒有鉤子函數,則最終 data.hook 的值以下所示:
data.hook = componentVNodeHooks = {
init (vnode, hydrating) {/* 省略具體實現 */},
prepatch (oldVnode, vnode) {/* 省略具體實現 */},
insert (vnode) {/* 省略具體實現 */},
destroy (vnode) {/* 省略具體實現 */}
}
複製代碼
最終有四個鉤子函數:init、prepatch、insert、destroy。鉤子函數的具體功能在後面用到時再詳細講解。
構造函數 VNode() 中關於生成組件實例的代碼以下所示:
export default class VNode {
constructor (tag,data,children,text,elm,
context,componentOptions,asyncFactory) {
this.tag = tag
this.data = data
this.context = context
this.componentOptions = componentOptions
this.asyncFactory = asyncFactory
/*省略...*/
}
get child (){
return this.componentInstance
}
}
複製代碼
例子中的組件VNode最終以下所示:
vnode = {
tag: 'vue-component-1-component-a',
data:{
on: undefined,
hook:{
init (vnode, hydrating) {/* 省略具體實現 */},
prepatch (oldVnode, vnode) {/* 省略具體實現 */},
insert (vnode) {/* 省略具體實現 */},
destroy (vnode) {/* 省略具體實現 */}
}
},
componentOptions:{
Ctor: function VueComponent(options){/*組件構造函數*/}
tag: "component-a"
children: undefined
listeners: undefined
propsData: undefined
},
asyncFactory: undefined,
componentInstance: undefined
/*省略...*/
}
複製代碼
在 patch 的過程當中,組件類型VNode生成真實DOM是調用函數 createPatchFunction 中的內部函數 createComponent 來完成的。
函數 createComponent 代碼以下所示:
function createComponent(vnode,insertedVnodeQueue,parentElm,refElm){
var i = vnode.data
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
複製代碼
在不考慮 keepAlive 的狀況下,組件類型VNode生成DOM的過程爲:
一、調用 data.hook.init 方法生成組件實例 componentInstance 屬性,並完成組件的掛載。
二、調用 initComponent 函數使用鉤子函數完成組件初始化。
三、調用 insert 方法將生成的DOM插入。
鉤子函數 init 函數代碼以下所示:
init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child =
vnode.componentInstance =
createComponentInstanceForVnode(vnode,activeInstance)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
複製代碼
keepAlive 的狀況在後續講解內置組件時闡述。通常狀況下會走 else 分支,使用 createComponentInstanceForVnode 函數建立 VNode 的組件實例屬性。最後調用組件實例的 $mount 方法掛載實例。
function createComponentInstanceForVnode (vnode,parent) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options)
}
複製代碼
createComponentInstanceForVnode 函數主要做用是調取組件的構造函數生成組件構造實例。
initComponent 函數在組件 tag 存在的狀況下,主要做用是調用局部變量 cbs.create 中的各類鉤子函數來完成初始化,cbs.create 在Virtual DOM一文中有詳細介紹。 以後使用 setScope 設置 style 做用域。
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
vnode.data.pendingInsert = null;
}
vnode.elm = vnode.componentInstance.$el;
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
setScope(vnode);
} else {
registerRef(vnode);
insertedVnodeQueue.push(vnode);
}
}
複製代碼
函數式組件跟 react 裏面的無狀態組件很類似,函數式組件無狀態 (沒有響應式數據),也沒有實例 (沒有 this 上下文)。由於只是函數,沒有實例,因此函數式組件相對於普通組件來講渲染開銷較低。
下面先簡單介紹函數式組件的使用,再闡述其源碼實現。
函數式組件的使用方式通常有兩種:
一、使用 Vue.component 聲明組件時,在選項中將 functional 屬性置爲 true,且手動實現 render 函數。
二、在單文件組件中,使用 <template functional> 代替 <template> 聲明模板。
函數式組件的中的 render 函數除了第一個 createElement 參數以外,還添加了第二個參數 context 對象。組件須要的一切都是經過 context 參數傳遞。
context 對象包含屬性以下:
context = {
props:{ /*提供全部 prop 的對象 */ },
children: [ /*VNode 子節點的數組*/ ],
slots: () => {},/*一個返回了包含全部插槽的對象的函數*/
scopedSlots: { /*暴露傳入的做用域插槽的對象*/ },
data: { /*不是數據對象,是組件屬性,createElement第二個參數*/ },
parent: { /*對父組件的引用*/ },
listeners: { /*事件監聽器的對象,data.on 的一個別名*/ },
injections: { /*被 inject 選項注入的屬性。*/ },
}
複製代碼
下面是一個簡單的函數式組件的例子,後續以此爲例闡述函數式組件原理。
<body>
<div id="app"></div>
</body>
<script> var ComponentA = { functional: true, render: function(createElement,context) { return createElement('div',context.props.name) } } var vue = new Vue({ el: '#app', template: `<div id="app" class="home"> <component-a name='組件A'></component-a> </div>`, components: { "component-a": ComponentA } }) </script>
複製代碼
依舊按照組件的編譯順序來探究其實現原理。
上述示例由模板生成的渲染函數以下所示,能夠看到,函數式組件生成渲染函數與普通組件並沒有不一樣之處。
with(this){
return _c(
'div',
{staticClass:"home",attrs:{"id":"app"}},
[
_c('component-a',{attrs:{"name":"組件A"}})
],
1
)
}
複製代碼
在由渲染函數生成VNode的過程當中,會調用生成組件VNode的函數 createComponent,在該函數中有對函數式組件的特殊處理。
function createComponent(Ctor,data,context,children,tag){
/* 省略... */
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor,propsData,data,context,children)
}
/* 省略... */
}
複製代碼
從以上代碼中能夠看出,函數式組件的VNode是 createFunctionalComponent 函數的返回值。
function createFunctionalComponent(Ctor,propsData,data,contextVm,children){
var options = Ctor.options;
var props = {};
var propOptions = options.props;
if (isDef(propOptions)) {
for (var key in propOptions) {
props[key] = validateProp(key, propOptions, propsData || emptyObject);
}
} else {
if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
if (isDef(data.props)) { mergeProps(props, data.props); }
}
var renderContext = new FunctionalRenderContext(
data,
props,
children,
contextVm,
Ctor
);
var vnode = options.render.call(null, renderContext._c, renderContext);
if (vnode instanceof VNode) {
return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
} else if (Array.isArray(vnode)) {
var vnodes = normalizeChildren(vnode) || [];
var res = new Array(vnodes.length);
for (var i = 0; i < vnodes.length; i++) {
res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext);
}
return res
}
}
複製代碼
createFunctionalComponent 函數主要有四個功能:
一、將 attrs、props 上的值都合併到 props 中。
二、根據傳入的 context 值,合併生成上下文參數對象 renderContext。
三、由手寫的 render 函數生成VNode。
四、克隆VNode,而後添加fnContext、fnOptions等屬性。
這裏能夠看出函數式組件與普通組件最大的區別:普通組件生成組件VNode,VNode對上有指向組件實例的componentInstance屬性。函數式組件根據render函數生成VNode,自己並無相應的組件實例。。
根據函數式組件生成的VNode以下所示:
VNode = {
/* 省略... */
tag: "div",
children: [{/*子節點VNode*/}],
devtoolsMeta: {renderContext: {/*createElement第二個參數對象*/}},
fnContext: {/*上下文信息*/},
fnOptions: {/*函數式組件選項*/},
isCloned: true,
isRootInsert: true,
componentInstance: undefined,
componentOptions: undefined,
data: undefined
/* 省略... */
}
複製代碼
在 patch 階段,由於根據函數式組件生成的VNode上並無組件選項 componentOptions 屬性,根據VNode生成真實DOM的過程與普通組件同樣。
實際上,函數式組件僅僅是生成包裹內容對應的VNode,在生成真實DOM的時候,函數式組件徹底透明,生成的DOM由根據包裹內容而定的。
組件註冊的方式有兩種:局部註冊、全局註冊。組件註冊的實質是根據傳入的選項生成Vue子構造函數,在使用組件時使用子構造函數生成組件實例。全局註冊組件的信息在局部組件註冊對象的原型上,所以全局註冊的組件能夠不重複註冊而被全局使用。
根據組件生成的渲染函數除了slot以外跟普通的標籤同樣,組件渲染函數生成的VNode上有組件選項信息屬性 componentOptions。在 patch 的過程當中,首先生成組件實例,而後根據組件實例生成真實DOM並掛載。
普通組件都會生成對應的組件實例對象,相對而言開銷比較大。函數式組件不會生成專門的VNode以及實例對象,函數式組件至關於一個容器,在組件生成時直接渲染包裹的內容。
歡迎關注公衆號:前端桃花源,互相交流學習!