Vue組件的渲染流程詳細講解
注: 本文目的是通過源碼方向來講component組件的渲染流程
引言與例子
在我們創(chuàng)建Vue實例時,是通過new Vue
、this._init(options)
方法來進行初始化,然后再執(zhí)行$mount
等,那么在組件渲染時,可不可以讓組件使用同一套邏輯去處理呢?
答:當然是可以的,需要使用到Vue.extend
方法來實現(xiàn)。
舉一個工作中能用到的例子:
需求:我們在項目中實現(xiàn)一個像element-ui
的Message Box
彈窗,在全局注冊(Vue.use
)后,能像alert
方法一樣,調(diào)用函數(shù)就可以彈出
實現(xiàn)
(先簡單說下vue
的use
方法基礎使用,use注冊時,如果是函數(shù)會執(zhí)行函數(shù),如果是對象,會執(zhí)行對象中的install
方法進行注冊)
根據(jù)需求,我們在調(diào)用use方法后,需要實現(xiàn)兩個目的:將組件注冊并直接掛載到dom上,將方法放在Vue.prototype
下;
- 首先實現(xiàn)彈窗樣式和邏輯(不是本文主要目的,此處跳過),假設其中有一個簡單的顯示函數(shù)
show(){this.visible = true}
- 要通過
use
的方式注冊組件,就要有一個install
方法,在方法中首先調(diào)用Vue.extend(messageBox組件)
,然后調(diào)用該對象的$mount()
方法進行渲染,最后將生成的DOM節(jié)點messageBox.$el
上樹,然后上show
方法放到Vue.prototype
上,就完成了
function install(Vue) { // 生成messageBox 構造函數(shù) var messageBox = Vue.extend(this); messageBox = new messageBox(); // 掛載組件,生成dom節(jié)點(這里沒傳參,所以只是生成dom并沒有上樹) messageBox.$mount(); // 節(jié)點上樹 document.body.appendChild(messageBox.$el); // 上show方法掛載到全局 Vue.prototype.$showMessageBox = messageBox.show; }
根據(jù)例子,我們來看一下這個extend
方法:
extend
Vue
中,有一個extend
方法,組件的渲染就是通過調(diào)用extend
創(chuàng)建一個繼承于Vue
的構造函數(shù)。extend
中的創(chuàng)建的主要過程是:
在內(nèi)部創(chuàng)建一個最終要返回的構造函數(shù)
Sub
,Sub
函數(shù)內(nèi)部與Vue
函數(shù)相同,都是調(diào)用this._init(options)
繼承Vue
,合并Vue.options
和組件的options
在Sub
上賦值靜態(tài)方法 緩存Sub
構造函數(shù),并在extend
方法開始時判斷緩存,避免重復渲染同一組件 返回Sub構造函數(shù)(要注意extend調(diào)用后返回的是個還未執(zhí)行的構造函數(shù) Sub)
// 注:mergeOptions方法是通過不同的策略,將options中的屬性進行合并 Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this // 父級構造函數(shù) // 拿到cid,并通過_Ctor屬性緩存,判斷是否已經(jīng)創(chuàng)建過,避免重復渲染同一組件 const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } // name校驗+拋出錯誤 const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) } // 創(chuàng)建構造函數(shù)Sub const Sub = function VueComponent (options) { this._init(options) } // 繼承原型對象 Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ // cid自增 // 父級options與當前傳入的組件options合并 Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super // 緩存父級構造函數(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. // 對于props和computed屬性,我們在擴展時在擴展原型的Vue實例上定義代理getter。這避免了object。為創(chuàng)建的每個實例調(diào)用defineProperty。 if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // 將全局方法放在Sub上,允許進一步調(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) // 對應上邊的_Ctor屬性緩存 cachedCtors[SuperId] = Sub return Sub } }
看完export
函數(shù)后,思考下,生成組件時是一個怎樣的執(zhí)行流程呢?
執(zhí)行流程
1. 注冊流程(以Vue.component()祖冊為例子):
用戶在調(diào)用Vue.component
時,其實就只執(zhí)行了三行代碼
// 簡化版component源碼 Vue.component = function (id,definition) { definition.name = definition.name || id // _base指向的是new Vue()時的這個Vue實例,調(diào)用的是Vue實例上的extend方法 definition = this.options._base.extend(definition) this.options.components[id] = definition return definition }
獲取并賦值組件的name
definition.name
調(diào)用根Vue上的extend方法
將組件放到options.components
上
返回definition
(如果是異步組件的話,只會走后邊兩步,不會執(zhí)行extend
)
在下文中,我們會將extend
方法返回的Sub對象稱為Ctor
在創(chuàng)建組件時,我們實際只是為組件執(zhí)行了extend
方法,但在option.components
中傳入的組件不會被執(zhí)行extend
方法,在3.渲染流程中會執(zhí)行
2. 執(zhí)行流程
在createElement
函數(shù)執(zhí)行時,根據(jù)tag
字段來判斷是不是一個組件,如果是組件,執(zhí)行組件初始化方法createComponent
createComponent
- 首先判斷傳入的Ctor是否已經(jīng)執(zhí)行了
extend
方法,沒有執(zhí)行的話執(zhí)行一遍 - 然后判斷是不是異步組件(如果是,調(diào)用
createAsyncPlaceholder
生成并返回) - 然后處理
data
,創(chuàng)建data.hook
中的鉤子函數(shù),比如init
- 最后調(diào)用
new VNode()
生成節(jié)點
先看下createElement
函數(shù)源碼,然后在底下主要說下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()時的這個Vue實例 const baseCtor = context.$options._base // 如果extend沒有執(zhí)行過,在這里執(zhí)行 if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // 報錯處理 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 } } // 重點 創(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
對象上,通過installComponentHooks
和installComponentHooks
方法判斷data.hook
中是否有該值,然后進行合并處理等操作實現(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上,方便取值 // 在這個函數(shù)中會new并返回extend生成的Ctor const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) // 重點 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
時會調(diào)用new Ctor(options)
- 在上邊介紹
extend
方法中可以看到new Ctor
時會調(diào)用Vue
的_init
方法,執(zhí)行Vue
實例的初始化邏輯 - 在
Vue.prototype._init
方法初始化完畢,執(zhí)行$mount
是,會有下邊代碼這樣一個判斷,組件這時沒有el
,所以不會執(zhí)行$mount
函數(shù)
if (vm.$options.el) { vm.$mount(vm.$options.el); }
- 手動執(zhí)行
$mount
函數(shù)
3. 渲染流程
在組件渲染流程createElm
函數(shù)中,有一段代碼
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return }
所以,組件的生成和判斷都是在createComponent
函數(shù)中發(fā)生的
createComponent
- 因為在執(zhí)行流程中,生成的
vnode
就是該函數(shù)中傳入的vnode
,并且在vnode
創(chuàng)建時把data
放在了vnode
上,那么vnode.data.hook.init
就可以獲取到上邊說的init
函數(shù),我們可以判斷,如果有該值,就可以認定本次vnode
為組件,并執(zhí)行vnode.data.hook.init
,init
的內(nèi)容詳見上邊 init
執(zhí)行完畢后,Ctor
的實例會被掛載到vnode.componentInstance
上,并且已經(jīng)生成了真實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 // 在判斷是否定義的同時,把變量做了改變,最終拿到了i.hook.init(在extend函數(shù)中注冊的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是子組件 //它應該創(chuàng)建一個子實例并掛載它。孩子 //組件還設置了占位符vnode的elm。 //在這種情況下,我們只需返回元素就可以了。 // componentInstance是組件的ctor實例,有了代表已經(jīng)創(chuàng)建了vnode.elm(真實節(jié)點) if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
總結
到此這篇關于Vue組件渲染流程的文章就介紹到這了,更多相關Vue組件渲染流程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
lottie實現(xiàn)vue自定義loading指令及常用指令封裝詳解
這篇文章主要為大家介紹了lottie實現(xiàn)vue自定義loading指令及常用指令封裝,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09vue緩存的keepalive頁面刷新數(shù)據(jù)的方法
這篇文章主要介紹了vue緩存的keepalive頁面刷新數(shù)據(jù)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04vue-resource調(diào)用promise取數(shù)據(jù)方式詳解
這篇文章主要介紹了vue-resource調(diào)用promise取數(shù)據(jù)方式詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07詳解Vue+axios+Node+express實現(xiàn)文件上傳(用戶頭像上傳)
這篇文章主要介紹了詳解Vue+axios+Node+express實現(xiàn)文件上傳(用戶頭像上傳),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08