组件化是 Vue
, React
等这些框架的一个核心思想,通过把页面拆成一个个高内聚、低耦合的组件,能够极大水平进步咱们的代码复用度,同时也使得我的项目更加易于保护。所以,本文就来剖析下组件的渲染流程。咱们通过上面这个例子来进行剖析:
<div id="demo"> <comp></comp></div><script> Vue.component('comp', { template: '<div>I am comp</div>', }) const app = new Vue({ el: '#demo', })</script>
这里咱们分为两步来剖析:组件申明、组件创立及渲染
组件申明
首先,咱们看下 Vue.component
是什么货色,它的申明在 core/global-api/assets.js
:
export function initAssetRegisters(Vue: GlobalAPI) { // ASSET_TYPES是数组:['component','directive','filter'] ASSET_TYPES.forEach((type) => { Vue[type] = function ( id: string, definition: Function | Object ): Function | Object | void { if (!definition) { return this.options[type + 's'][id] } else { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && type === 'component') { validateComponentName(id) } // 组件申明相干代码 if (type === 'component' && isPlainObject(definition)) { definition.name = definition.name || id // _base是Vue // Vue.extend({})返回组件构造函数 definition = this.options._base.extend(definition) } if (type === 'directive' && typeof definition === 'function') { definition = {bind: definition, update: definition} } // 注册到components选项中去 // 在Vue原始选项上增加组件配置,未来其余组件继承,它们都有这些组件注册 this.options[type + 's'][id] = definition return definition } } })}
这里 this.options._base.extend(definition)
调用的其实就是 Vue.extend(definition)
:
Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this 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) } 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) 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 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}
这里咱们能够了解为返回了一个名叫 VueComponent
的构造函数且继承了 Vue
。所以,这里的组件定义实现后 Vue
就会变成这样:
{ ... options: { components: { comp: function VueComponent() {} } } ..}
组件创立及挂载
咱们晓得 Vue
中的模板最初会变编译成 render
函数,比方下面例子最终的 render
函数会如下所示:
render() { with (this) {return _c('div',{attrs:{"id":"demo"}},[_c('comp')],1)}}
这里 _c
的定义能够在 core/instance/render.js
中找到:
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
所以 _c('comp')
最终还是调用了 createElement
(core/vdom/create-element.js) 这个办法:
export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean): VNode | Array<VNode> { ... return _createElement(context, tag, data, children, normalizationType)}export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number): VNode | Array<VNode> { ... } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 自定义组件 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 ) } ...}
这里咱们只看自定义组件的相干逻辑,发现最初调用了 createComponent
(core/vdom/create-component.js):
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void { ... // install component management hooks onto the placeholder node // 装置组件治理钩子:将来会做组件初始化(实例创立、挂载) installComponentHooks(data) // return a placeholder vnode 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 ) return vnode}
这里咱们跳过其余的代码,先看看 installComponentHooks
:
function installComponentHooks(data: VNodeData) { const hooks = data.hook || (data.hook = {}) for (let i = 0; i < hooksToMerge.length; i++) { const key = hooksToMerge[i] const existing = hooks[key] const toMerge = componentVNodeHooks[key] if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } }}
这里会在 data.hook
上挂载一些 hooks
,如果用户也传了雷同的 hooks
则会进行合并。这个 hooks
又是啥呢:参考 前端vue面试题具体解答
const componentVNodeHooks = { // 实例化和挂载 init(vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && // 实例曾经存在 !vnode.componentInstance._isDestroyed && // 未被销毁 vnode.data.keepAlive // 被标记为keepAlive ) { // kept-alive components, treat as a patch // 对于缓存组件,只需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 */) } } },}
这里有四个 hooks
,看他们的名字就晓得他们会在对应的操作去执行。比方 init
会在组件初始化的时候执行,这个前面碰到了再说。咱们持续看 createComponent
:
// return a placeholder vnodeconst name = Ctor.options.name || tagconst vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, {Ctor, propsData, listeners, tag, children}, asyncFactory)return vnode
export default class VNode { ... constructor( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { ... this.componentOptions = componentOptions } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child(): Component | void { return this.componentInstance }}
这里初始化了一个 VNode
并进行了返回,到这里 _c('comp')
的工作就实现了。能够看到咱们的自定义组件的构造函数在这一步并没有执行,仅仅只是挂载到了 componentOptions
属性上。那他什么时候执行呢?别急,咱们接着往下走。
当根组件的 render
执行完后,会执行 vm._update
进行组件的更新,而后会调用 __patch__
,咱们顺藤摸瓜最终来到 core/vdom/patch.js
:
return function patch(oldVnode, vnode, hydrating, removeOnly) { ... // create new node 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) ) ... return vnode.elm}
而后会走到 createElm
:
function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { ... } else { createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { // 事件、属性等等初始化 invokeCreateHooks(vnode, insertedVnodeQueue); } // 插入节点 insert(parentElm, vnode.elm, refElm); } ...
留神到这里的 vnode
是 <div id="demo"></div>
这个元素的,所以会走到 createChildren
:
function createChildren(vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(children) } for (let i = 0; i < children.length; ++i) { createElm( children[i], insertedVnodeQueue, vnode.elm, null, true, children, i ) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) }}
这里最初又回到了 createElm
,不过此时的 vnode
就是自定义组件了,会走到这里:
function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { ... // 自定义组件创立 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return; }
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { // 缓存的状况 const isReactivated = isDef(vnode.componentInstance) && i.keepAlive // 后面装置的钩子在这里用到了,执行了 init,自定义组件实例化 if (isDef((i = i.hook)) && isDef((i = i.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. // 如果下面创立过程已实现,组件实例已存在 if (isDef(vnode.componentInstance)) { // 初始化组件:组件下面事件、属性等 initComponent(vnode, insertedVnodeQueue) // 插入dom insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } }}
留神到这里会执行 i.init
办法,该办法上文曾经说过,会实例化组件对象,而后进行 $mount
。而执行 $mount
最终又会走到 patch
办法,并最终执行 createElm
:
function patch(oldVnode, vnode, hydrating, removeOnly) { ... if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true; createElm(vnode, insertedVnodeQueue); } ...}
执行该办法又会递归的将自定义组件内的 vnode
渲染成实在的 dom
,最初通过 insert
办法将整颗 dom 树插入到父元素之中。到这里自定义组件的渲染过程就完结了