Vue组件的渲染流程详细讲解
目录
- 引言与例子
- 举一个工作中能用到的例子:
- 实现
- extend
- 执行流程
- 1. 注册流程(以Vue.component()祖册为例子):
- 2. 执行流程
- 3. 渲染流程
- 总结
注: 本文目的是通过源码方向来讲component组件的渲染流程
引言与例子
在我们创建Vue实例时,是通过new Vue
、this._init(options)
方法来进行初始化,然后再执行$mount
等,那么在组件渲染时,可不可以让组件使用同一套逻辑去处理呢?
答:当然是可以的,需要使用到Vue.extend
方法来实现。
举一个工作中能用到的例子:
需求:我们在项目中实现一个像element-ui
的Message Box
弹窗,在全局注册(Vue.use
)后,能像alert
方法一样,调用函数就可以弹出
实现
(先简单说下vue
的use
方法基础使用,use注册时,如果是函数会执行函数,如果是对象,会执行对象中的install
方法进行注册)
根据需求,我们在调用use方法后,需要实现两个目的:将组件注册并直接挂载到dom上,将方法放在Vue.prototype
下;
- 首先实现弹窗样式和逻辑(不是本文主要目的,此处跳过),假设其中有一个简单的显示函数
show(){this.visible = true}
- 要通过
use
的方式注册组件,就要有一个install
方法,在方法中首先调用Vue.extend(messageBox组件)
,然后调用该对象的$mount()
方法进行渲染,最后将生成的DOM节点messageBox.$el
上树,然后上show
方法放到Vue.prototype
上,就完成了
function install(Vue) { // 生成messageBox 构造函数 var messageBox = Vue.extend(this); messageBox = new messageBox(); // 挂载组件,生成dom节点(这里没传参,所以只是生成dom并没有上树) messageBox.$mount(); // 节点上树 document.body.appendChild(messageBox.$el); // 上show方法挂载到全局 Vue.prototype.$showMessageBox = messageBox.show; }
根据例子,我们来看一下这个extend
方法:
extend
Vue
中,有一个extend
方法,组件的渲染就是通过调用extend
创建一个继承于Vue
的构造函数。extend
中的创建的主要过程是:
在内部创建一个最终要返回的构造函数
Sub
,Sub
函数内部与Vue
函数相同,都是调用this._init(options)
继承Vue
,合并Vue.options
和组件的options
在Sub
上赋值静态方法 缓存Sub
构造函数,并在extend
方法开始时判断缓存,避免重复渲染同一组件 返回Sub构造函数(要注意extend调用后返回的是个还未执行的构造函数 Sub)
// 注:mergeOptions方法是通过不同的策略,将options中的属性进行合并 Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this // 父级构造函数 // 拿到cid,并通过_Ctor属性缓存,判断是否已经创建过,避免重复渲染同一组件 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) } // 创建构造函数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 // 缓存父级构造函数 // 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。为创建的每个实例调用defineProperty。 if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // 将全局方法放在Sub上,允许进一步调用 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
函数后,思考下,生成组件时是一个怎样的执行流程呢?
执行流程
1. 注册流程(以Vue.component()祖册为例子):
用户在调用Vue.component
时,其实就只执行了三行代码
// 简化版component源码 Vue.component = function (id,definition) { definition.name = definition.name || id // _base指向的是new Vue()时的这个Vue实例,调用的是Vue实例上的extend方法 definition = this.options._base.extend(definition) this.options.components[id] = definition return definition }
获取并赋值组件的name
definition.name
调用根Vue上的extend方法
将组件放到options.components
上
返回definition
(如果是异步组件的话,只会走后边两步,不会执行extend
)
在下文中,我们会将extend
方法返回的Sub对象称为Ctor
在创建组件时,我们实际只是为组件执行了extend
方法,但在option.components
中传入的组件不会被执行extend
方法,在3.渲染流程中会执行
2. 执行流程
在createElement
函数执行时,根据tag
字段来判断是不是一个组件,如果是组件,执行组件初始化方法createComponent
createComponent
- 首先判断传入的Ctor是否已经执行了
extend
方法,没有执行的话执行一遍 - 然后判断是不是异步组件(如果是,调用
createAsyncPlaceholder
生成并返回) - 然后处理
data
,创建data.hook
中的钩子函数,比如init
- 最后调用
new VNode()
生成节点
先看下createElement
函数源码,然后在底下主要说下init
函数
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没有执行过,在这里执行 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 } } // 重点 创建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
等方法在源码中是创建在componentVNodeHooks
对象上,通过installComponentHooks
和installComponentHooks
方法判断data.hook
中是否有该值,然后进行合并处理等操作实现的,在这里,我们不考虑其他的直接看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上,方便取值 // 在这个函数中会new并返回extend生成的Ctor const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) // 重点 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } } // createComponentInstanceForVnode函数示例 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
方法中,执行createComponentInstanceForVnode
时会调用new Ctor(options)
- 在上边介绍
extend
方法中可以看到new Ctor
时会调用Vue
的_init
方法,执行Vue
实例的初始化逻辑 - 在
Vue.prototype._init
方法初始化完毕,执行$mount
是,会有下边代码这样一个判断,组件这时没有el
,所以不会执行$mount
函数
if (vm.$options.el) { vm.$mount(vm.$options.el); }
- 手动执行
$mount
函数
3. 渲染流程
在组件渲染流程createElm
函数中,有一段代码
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return }
所以,组件的生成和判断都是在createComponent
函数中发生的
createComponent
- 因为在执行流程中,生成的
vnode
就是该函数中传入的vnode
,并且在vnode
创建时把data
放在了vnode
上,那么vnode.data.hook.init
就可以获取到上边说的init
函数,我们可以判断,如果有该值,就可以认定本次vnode
为组件,并执行vnode.data.hook.init
,init
的内容详见上边 init
执行完毕后,Ctor
的实例会被挂载到vnode.componentInstance
上,并且已经生成了真实dom,可以在vnode.componentInstance.$el
上获取到- 最后执行
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函数中注册的Ctor的init方法) if (isDef(i = i.hook) && isDef(i = i.init)) { // 执行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. //调用init hook之后,如果vnode是子组件 //它应该创建一个子实例并挂载它。孩子 //组件还设置了占位符vnode的elm。 //在这种情况下,我们只需返回元素就可以了。 // componentInstance是组件的ctor实例,有了代表已经创建了vnode.elm(真实节点) if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
总结
到此这篇关于Vue组件渲染流程的文章就介绍到这了,更多相关Vue组件渲染流程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!