之前文章有寫到vue構造函數的實例化過程,只是對vue實例做了個粗略的描述,並沒有說明vue組件實例化的過程。本文主要對vue組件的實例化過程做一些簡要的描述。 組件的實例化與vue構造函數的實例化,大部分是類似的,vue的實例可以當做一個根組件,普通組件的實例化可以當做子組件。真實的DOM是一個樹 ...
之前文章有寫到vue構造函數的實例化過程,只是對vue實例做了個粗略的描述,並沒有說明vue組件實例化的過程。本文主要對vue組件的實例化過程做一些簡要的描述。
組件的實例化與vue構造函數的實例化,大部分是類似的,vue的實例可以當做一個根組件,普通組件的實例化可以當做子組件。真實的DOM是一個樹形結構,虛擬DOM本質只是真實DOM的抽象,也是一個樹形結構。簡單來說,整個vue工程的實例化過程如下:
如上圖所示,在調用render函數時,會依次調用createElement方法,createElement方法的代碼如下,主要作用就是生成vnode。
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { if (isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } // object syntax in v-bind if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // warn against non-primitive key if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.key) && !isPrimitive(data.key) ) { if (!__WEEX__ || !('@binding' in data.key)) { warn( 'Avoid using non-primitive value as key, ' + 'use string/number value instead.', context ) } } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } // 組件格式化 if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) // 普通的HTML標簽 if (config.isReservedTag(tag)) { // platform built-in elements if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) { warn( `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`, context ) } // 創建一個普通的DOM節點 vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component // 創建組件 vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } }從上述代碼中可以看出,如果存在tag且tag的類型為string,會走一些判斷邏輯,主要就是判斷兩類,一類是HTML標簽,通過config.isReservedTag判斷是否是HTML標簽,另外一類就是在當前實例作用域options中的component中查找,是否存在對該類標簽的聲明,存在,即使組件,詳細流程圖如下圖所示:
如上圖所示,主流程與實例化Vue類似,只是在實例化Vue的過程中,額外走了一個創建組件的分支,其中createComponent方法實現如下:
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 } // 獲取Vue基礎構造函數,在initGlobal中,將vue基礎構造方法賦值給_base屬性 const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { // 將組件的配置,合併到構造方法中,extend是定義在Vue構造方法中的 Ctor = baseCtor.extend(Ctor) } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component 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 || {} // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } // install component management hooks onto the placeholder node // 初始化組件的鉤子函數 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 ) // Weex specific: invoke recycle-list optimized @render function for // extracting cell-slot template. // https://github.com/Hanks10100/weex-native-directive/tree/master/component /* istanbul ignore if */ if (__WEEX__ && isRecyclableComponent(vnode)) { return renderRecyclableComponentTemplate(vnode) } return vnode }
從上述代碼中可以看出,createComponent主要作用就是返回一個vnode,中間的流程主要作用有兩點,一是組裝組件的構造方法,用於實例化組件,另外一點就是調用installComponentHooks,初始化組件的生命周期入口。組件的聲明周期鉤子雖然與vue根實例一致,但是調用的位置還是有一定的差別,具體有以下幾點:
1. Vue構造方法是在src\core\instance\index.js中,而組件的構造方法是基於Vue根構造方法,在上述createComponet中調用Vue.extend方法進行組裝而成,本質上都是調用Vue實例上的_init方法,但是組件的構造方法VueComponent聲明瞭一些屬於自己的自定義屬性,具體實現代碼如下:
Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this // 父級實例cid const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) } // 定義vue初始化方法,和實例化Vue走同一個路線 const Sub = function VueComponent (options) { this._init(options) } // super -> this -> Vue 繼承Vue構造方法中的屬性 Sub.prototype = Object.create(Super.prototype) // 指定子組件的構造方法為Sub -> VueComponent Sub.prototype.constructor = Sub Sub.cid = cid++ // 合併組件屬性 Sub.options = mergeOptions( Super.options, extendOptions ) // 定義父級作用域 Sub['super'] = Super // 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. if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // allow further extension/mixin/plugin usage // 子組件的實例,保持對vue構造方法的引用 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) // cache constructor cachedCtors[SuperId] = Sub return Sub } }
2. Vue根實例的模板解析與DOM掛載入口不一致,在_init方法中,提供了對根實例的模板解析與DOM掛載,而組件沒有。在創建組件時,調用了installComponentHooks,componet hooks主要包含init、prepatch、insert、destory,init在實例化組件時調用,insert是插入DOM時調用,destory是在銷毀組件時調用,而prepatch是在更新組件時調用,具體如下:
const componentVNodeHooks = { // 組件初始化方法 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 { // 實例化組件 const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) //掛載組件 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { const options = vnode.componentOptions const child = vnode.componentInstance = oldVnode.componentInstance updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ) }, insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { // vue-router#1212 // During updates, a kept-alive component's child components may // change, so directly walking the tree here may call activated hooks // on incorrect children. Instead we push them into a queue which will // be processed after the whole patch process ended. queueActivatedComponent(componentInstance) } else { activateChildComponent(componentInstance, true /* direct */) } } }, destroy (vnode: MountedComponentVNode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { deactivateChildComponent(componentInstance, true /* direct */) } } } }
如上述代碼所示,實例化組件調用的是createComponentInstanceForVnode,createComponentInstanceForVnode代碼如下,調用在Vue.extend中組裝的組件構造方法VueComponent,初始化調用的還是Vue原型上的_init方法,大致流程與Vue初始化類似,只是解析模板有所區別,組件解析模板調用的是child.$mount。
// 創建組件的作用域,執行組件的_init方法,同vue實例化過程 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) }
在installComponentHooks中,在vnode的data屬性中初始化了hooks,後面在_patch__中,會調用patch.js中聲明的createComponent -> init -> 實例化組件。組件實例化完成後,會將真實DOM元素,插入到上一級元素。patch.js中的createComponent方法如下:
// 創建組件,如果節點類型是組件,則直接走創建組件的方法 function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data // 判斷是否存在組件的生命周期,存在,即需要走創建組件的流程 if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { // 執行component的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. // 組件的vnode對象中存在當前組件的作用域 if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) // 將子組件插入到父節點中 insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
在實例化完成後,會將生成的真實DOM元素插入到上級元素中,vue在獲取真實DOM時,是從低往上,一級級添加,最終將渲染的元素添加到DOM body中,__patch__主流程如下:
function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. // nodeType 1 元素 3 文字 if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== 'production') { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + '<p>, or missing <tbody>. Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } // replacing existing element // 獲取老舊節點 const oldElm = oldVnode.elm // 獲取老舊節點的父節點 const parentElm = nodeOps.parentNode(oldElm) // create new node // 將虛擬DOM轉換成真實DOM // 傳入父級節點,一級級添加 createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node // 移除老舊節點 if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
模板的解析,是先把模板解析成HTML,然後再講老舊節點移除。