Vue组件渲染流程
不在五行中 人气:0注: 本文目的是通过源码方向来讲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 } } }
总结
加载全部内容