Vue組件的渲染流程詳細(xì)講解
注: 本文目的是通過(guò)源碼方向來(lái)講component組件的渲染流程
引言與例子
在我們創(chuàng)建Vue實(shí)例時(shí),是通過(guò)new Vue、this._init(options)方法來(lái)進(jìn)行初始化,然后再執(zhí)行$mount等,那么在組件渲染時(shí),可不可以讓組件使用同一套邏輯去處理呢?
答:當(dāng)然是可以的,需要使用到Vue.extend方法來(lái)實(shí)現(xiàn)。
舉一個(gè)工作中能用到的例子:
需求:我們?cè)陧?xiàng)目中實(shí)現(xiàn)一個(gè)像element-ui的Message Box彈窗,在全局注冊(cè)(Vue.use)后,能像alert方法一樣,調(diào)用函數(shù)就可以彈出
實(shí)現(xiàn)
(先簡(jiǎn)單說(shuō)下vue的use方法基礎(chǔ)使用,use注冊(cè)時(shí),如果是函數(shù)會(huì)執(zhí)行函數(shù),如果是對(duì)象,會(huì)執(zhí)行對(duì)象中的install方法進(jìn)行注冊(cè))
根據(jù)需求,我們?cè)谡{(diào)用use方法后,需要實(shí)現(xiàn)兩個(gè)目的:將組件注冊(cè)并直接掛載到dom上,將方法放在Vue.prototype下;
- 首先實(shí)現(xiàn)彈窗樣式和邏輯(不是本文主要目的,此處跳過(guò)),假設(shè)其中有一個(gè)簡(jiǎn)單的顯示函數(shù)
show(){this.visible = true} - 要通過(guò)
use的方式注冊(cè)組件,就要有一個(gè)install方法,在方法中首先調(diào)用Vue.extend(messageBox組件),然后調(diào)用該對(duì)象的$mount()方法進(jìn)行渲染,最后將生成的DOM節(jié)點(diǎn)messageBox.$el上樹(shù),然后上show方法放到Vue.prototype上,就完成了
function install(Vue) {
// 生成messageBox 構(gòu)造函數(shù)
var messageBox = Vue.extend(this);
messageBox = new messageBox();
// 掛載組件,生成dom節(jié)點(diǎn)(這里沒(méi)傳參,所以只是生成dom并沒(méi)有上樹(shù))
messageBox.$mount();
// 節(jié)點(diǎn)上樹(shù)
document.body.appendChild(messageBox.$el);
// 上show方法掛載到全局
Vue.prototype.$showMessageBox = messageBox.show;
}
根據(jù)例子,我們來(lái)看一下這個(gè)extend方法:
extend
Vue中,有一個(gè)extend方法,組件的渲染就是通過(guò)調(diào)用extend創(chuàng)建一個(gè)繼承于Vue的構(gòu)造函數(shù)。extend中的創(chuàng)建的主要過(guò)程是:
在內(nèi)部創(chuàng)建一個(gè)最終要返回的構(gòu)造函數(shù)
Sub,Sub函數(shù)內(nèi)部與Vue函數(shù)相同,都是調(diào)用this._init(options)繼承Vue,合并Vue.options和組件的options在Sub上賦值靜態(tài)方法 緩存Sub構(gòu)造函數(shù),并在extend方法開(kāi)始時(shí)判斷緩存,避免重復(fù)渲染同一組件 返回Sub構(gòu)造函數(shù)(要注意extend調(diào)用后返回的是個(gè)還未執(zhí)行的構(gòu)造函數(shù) Sub)
// 注:mergeOptions方法是通過(guò)不同的策略,將options中的屬性進(jìn)行合并
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this // 父級(jí)構(gòu)造函數(shù)
// 拿到cid,并通過(guò)_Ctor屬性緩存,判斷是否已經(jīng)創(chuàng)建過(guò),避免重復(fù)渲染同一組件
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// name校驗(yàn)+拋出錯(cuò)誤
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
// 創(chuàng)建構(gòu)造函數(shù)Sub
const Sub = function VueComponent (options) {
this._init(options)
}
// 繼承原型對(duì)象
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++ // cid自增
// 父級(jí)options與當(dāng)前傳入的組件options合并
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super // 緩存父級(jí)構(gòu)造函數(shù)
// For props and computed properties, we define the proxy getters on the Vue instances at extension time, on the extended prototype. This avoids Object.defineProperty calls for each instance created.
// 對(duì)于props和computed屬性,我們?cè)跀U(kuò)展時(shí)在擴(kuò)展原型的Vue實(shí)例上定義代理getter。這避免了object。為創(chuàng)建的每個(gè)實(shí)例調(diào)用defineProperty。
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// 將全局方法放在Sub上,允許進(jìn)一步調(diào)用
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// 對(duì)應(yīng)上邊的_Ctor屬性緩存
cachedCtors[SuperId] = Sub
return Sub
}
}
看完export函數(shù)后,思考下,生成組件時(shí)是一個(gè)怎樣的執(zhí)行流程呢?
執(zhí)行流程
1. 注冊(cè)流程(以Vue.component()祖冊(cè)為例子):
用戶(hù)在調(diào)用Vue.component時(shí),其實(shí)就只執(zhí)行了三行代碼
// 簡(jiǎn)化版component源碼
Vue.component = function (id,definition) {
definition.name = definition.name || id
// _base指向的是new Vue()時(shí)的這個(gè)Vue實(shí)例,調(diào)用的是Vue實(shí)例上的extend方法
definition = this.options._base.extend(definition)
this.options.components[id] = definition
return definition
}
獲取并賦值組件的name
definition.name調(diào)用根Vue上的extend方法
將組件放到options.components上
返回definition
(如果是異步組件的話,只會(huì)走后邊兩步,不會(huì)執(zhí)行extend)
在下文中,我們會(huì)將extend方法返回的Sub對(duì)象稱(chēng)為Ctor
在創(chuàng)建組件時(shí),我們實(shí)際只是為組件執(zhí)行了extend方法,但在option.components中傳入的組件不會(huì)被執(zhí)行extend方法,在3.渲染流程中會(huì)執(zhí)行
2. 執(zhí)行流程
在createElement函數(shù)執(zhí)行時(shí),根據(jù)tag字段來(lái)判斷是不是一個(gè)組件,如果是組件,執(zhí)行組件初始化方法createComponent
createComponent
- 首先判斷傳入的Ctor是否已經(jīng)執(zhí)行了
extend方法,沒(méi)有執(zhí)行的話執(zhí)行一遍 - 然后判斷是不是異步組件(如果是,調(diào)用
createAsyncPlaceholder生成并返回) - 然后處理
data,創(chuàng)建data.hook中的鉤子函數(shù),比如init - 最后調(diào)用
new VNode()生成節(jié)點(diǎn)
先看下createElement函數(shù)源碼,然后在底下主要說(shuō)下init函數(shù)
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// _base指向的是new Vue()時(shí)的這個(gè)Vue實(shí)例
const baseCtor = context.$options._base
// 如果extend沒(méi)有執(zhí)行過(guò),在這里執(zhí)行
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// 報(bào)錯(cuò)處理
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// 異步處理
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
// 處理data
data = data || {}
resolveConstructorOptions(Ctor)
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
const listeners = data.on
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 重點(diǎn) 創(chuàng)建init方法
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
// 得到vnode
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
讓我們看下init方法
init,prepatch,insert,destroy等方法在源碼中是創(chuàng)建在componentVNodeHooks對(duì)象上,通過(guò)installComponentHooks和installComponentHooks方法判斷data.hook中是否有該值,然后進(jìn)行合并處理等操作實(shí)現(xiàn)的,在這里,我們不考慮其他的直接看init方法
先放上完整代碼:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 掛載到vnode上,方便取值
// 在這個(gè)函數(shù)中會(huì)new并返回extend生成的Ctor
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 重點(diǎn)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
// createComponentInstanceForVnode函數(shù)示例
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
- 在
init方法中,執(zhí)行createComponentInstanceForVnode時(shí)會(huì)調(diào)用new Ctor(options) - 在上邊介紹
extend方法中可以看到new Ctor時(shí)會(huì)調(diào)用Vue的_init方法,執(zhí)行Vue實(shí)例的初始化邏輯 - 在
Vue.prototype._init方法初始化完畢,執(zhí)行$mount是,會(huì)有下邊代碼這樣一個(gè)判斷,組件這時(shí)沒(méi)有el,所以不會(huì)執(zhí)行$mount函數(shù)
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
- 手動(dòng)執(zhí)行
$mount函數(shù)
3. 渲染流程
在組件渲染流程createElm函數(shù)中,有一段代碼
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
所以,組件的生成和判斷都是在createComponent函數(shù)中發(fā)生的
createComponent
- 因?yàn)樵趫?zhí)行流程中,生成的
vnode就是該函數(shù)中傳入的vnode,并且在vnode創(chuàng)建時(shí)把data放在了vnode上,那么vnode.data.hook.init就可以獲取到上邊說(shuō)的init函數(shù),我們可以判斷,如果有該值,就可以認(rèn)定本次vnode為組件,并執(zhí)行vnode.data.hook.init,init的內(nèi)容詳見(jiàn)上邊 init執(zhí)行完畢后,Ctor的實(shí)例會(huì)被掛載到vnode.componentInstance上,并且已經(jīng)生成了真實(shí)dom,可以在vnode.componentInstance.$el上獲取到- 最后執(zhí)行
initComponent和insert,將組件掛載
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 在判斷是否定義的同時(shí),把變量做了改變,最終拿到了i.hook.init(在extend函數(shù)中注冊(cè)的Ctor的init方法)
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 執(zhí)行init
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
//調(diào)用init hook之后,如果vnode是子組件
//它應(yīng)該創(chuàng)建一個(gè)子實(shí)例并掛載它。孩子
//組件還設(shè)置了占位符vnode的elm。
//在這種情況下,我們只需返回元素就可以了。
// componentInstance是組件的ctor實(shí)例,有了代表已經(jīng)創(chuàng)建了vnode.elm(真實(shí)節(jié)點(diǎn))
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}總結(jié)
到此這篇關(guān)于Vue組件渲染流程的文章就介紹到這了,更多相關(guān)Vue組件渲染流程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
lottie實(shí)現(xiàn)vue自定義loading指令及常用指令封裝詳解
這篇文章主要為大家介紹了lottie實(shí)現(xiàn)vue自定義loading指令及常用指令封裝,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
vue3項(xiàng)目目錄結(jié)構(gòu)示例詳解
更好的了解項(xiàng)目的目錄結(jié)構(gòu),能更好的去開(kāi)發(fā)項(xiàng)目,下面這篇文章主要給大家介紹了關(guān)于vue3項(xiàng)目目錄結(jié)構(gòu)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02
vue實(shí)現(xiàn)動(dòng)態(tài)控制表格列的顯示隱藏
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)動(dòng)態(tài)控制表格列的顯示隱藏,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
vue-element的select下拉框賦值實(shí)例
這篇文章主要介紹了vue-element的select下拉框賦值實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
vue緩存的keepalive頁(yè)面刷新數(shù)據(jù)的方法
這篇文章主要介紹了vue緩存的keepalive頁(yè)面刷新數(shù)據(jù)的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-04-04
vue-resource調(diào)用promise取數(shù)據(jù)方式詳解
這篇文章主要介紹了vue-resource調(diào)用promise取數(shù)據(jù)方式詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07
vue中添加語(yǔ)音播報(bào)功能的實(shí)現(xiàn)
本文主要介紹了vue中添加語(yǔ)音播報(bào)功能的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
詳解Vue+axios+Node+express實(shí)現(xiàn)文件上傳(用戶(hù)頭像上傳)
這篇文章主要介紹了詳解Vue+axios+Node+express實(shí)現(xiàn)文件上傳(用戶(hù)頭像上傳),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
vue項(xiàng)目中openlayers繪制行政區(qū)劃
這篇文章主要為大家詳細(xì)介紹了vue項(xiàng)目中openlayers繪制行政區(qū)劃,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12

