我们在前面的文章对vue的创建渲染等都或多或少有所了解,现在我们再继续对串联其中的 组件化实现
进行学习,从单个组件的创建渲染到组件的递归创建渲染。
1.根组件实例化
根组件的实现和子组件的实现其实有所区别,根组件是我们手动调用Vue构造函数实例化,而子组件实例化的构造函数是继承自vue的 VueComponent
,我们先来看看根组件实现。
1.1入口
我们知道组件实例化是在 new Vue
中进行的,所以对于根实例来说,其入口十分明确,就在开发者主动调用构造函数初始化中。
new Vue({ el: '#app', render: h => h(App) }) 复制代码
1.2实例化
结合之前的分析我们知道,实例化将调用 _init
函数进行组件的初始化
let uid = 0 export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // 每个组件都有唯一UID vm._uid = uid++ // 组件对象不可以监测 // 在observe部分可以看到具体判断 vm._isVue = true // 配置合并 // merge options if (options && options._isComponent) { initInternalComponent(vm, options) // 首次实例化_isComponent为定义走else逻辑 } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // 各种数据定义的初始化 // 生命周期的调用 vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) initState(vm) initProvide(vm) callHook(vm, 'created') // 挂载节点 if (vm.$options.el) { vm.$mount(vm.$options.el) } } } 复制代码
其中配置合并将执行 resolveConstructorOptions
函数,对于根实例来说 resolveConstructorOptions
就是将 Vue.options
的配置合并到组件实例中
export function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { // ... } return options } 复制代码
而 Vue.options
对象则是包含了各种全局注册的conponents
,filters
,directives
等。如此,通过 Vue.mixin
,Vue.use
,Vue.extend
等方法添加的资源即可在组件中调用了。
1.3创建虚拟节点
组件实例化的最后一步,是挂载节点,我们应该知道它其实是在组件实例化流程中进行的,我们这边将其单独拎出来作为下一步来分析。
在 vm.$mount
中实际先执行 render
函数,其调用在 watcher
实例的回调函数 updateComponent
updateComponent = () => { vm._update(vm._render(), hydrating) } 复制代码
在 vm._render
执行的是我们的 rneder
函数,render
函数来源为开发者定义或 template
转化。在 render
中调用 _createElement
进行虚拟节点的创建。
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { //... // 子节点扁平化 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) if (config.isReservedTag(tag)) { vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } // ... if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { return vnode } else { return createEmptyVNode() } } 复制代码
从简化后的 _createElement
来看,其通过 tag
来判断是否是创建普通虚拟节点,如果是的话就执行 new VNode
逻辑。
1.4渲染
执行了 render
函数之后,意味为虚拟节点已经准备完毕,接下来就是执行渲染逻辑了。其调用依然在 updateComponent
中
updateComponent = () => { vm._update(vm._render(), hydrating) } 复制代码
我们来看看 vm._update
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this // 此时$el已经由配置#app转化为真实节点 const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) // _vnode更新为最新的虚拟节点 vm._vnode = vnode if (!prevVnode) { // 首次渲染时将使用真实节点作为虚拟节点 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() } 复制代码
__patch__
的逻辑我们就不细说了,等后面分析子组件的时候再去分析
1.5根组件小结
至此我们就完成了根组件从创建到渲染的分析,过程比较简单,我们来梳理下流程
- 开发者调用
new Vue
实例化组件,resolveConstructorOptions
将合并全局资源 - 在
render
中执行_createElement
创建虚拟节点vnode
,注意是整个组件的虚拟节点 - 执行
__patch__
逻辑完成渲染
2.子组件实例化
我们今天的主要任务是弄清楚子组件的实例化,这样才能弄清楚vue组件化实现的机制
2.1创建组件入口
在根组件生成虚拟节点的时候,遇到自定义组件会调用 createComponent
创建相应的组件虚拟节点,组件标签 => 组件虚拟节点
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { // ... let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 创建组件节点 vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // 创建组件节点 vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { return vnode } else { return createEmptyVNode() } } 复制代码
2.2创建组件节点
我们来看看创建组件节点 createComponent
的逻辑
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // _base === Vue const baseCtor = context.$options._base // 调用Vue.extend生成组件构造函数 if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // ... data = data || {} // 合并配置 resolveConstructorOptions(Ctor) // 提取props值并校验 const propsData = extractPropsFromVNodeData(data, Ctor, tag) const listeners = data.on data.on = data.nativeOn // 安装组件管理相关钩子 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 } 复制代码
前面分析了 createComponent
的主要流程,我们将其分为以下几个部分进行进一步分析
Vue.extend
生成组件构造函数resolveConstructorOptions
合并配置installComponentHooks
安装组件管理相关钩子new VNode
生成组件虚拟节点
2.2.1 Vue.extend生成组件构造函数
Vue.extend = function (extendOptions: Object): Function { // extendOptions是我们的组件配置 // 包括created methods data components等 extendOptions = extendOptions || {} // this===Vue const Super = this const SuperId = Super.cid // 往extendOptions._Ctor扩展属性保存此构造函数 // 当遇到相同的配置时就不需要再生成构造函数 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) } // VueComponent定义 const Sub = function VueComponent (options) { // 调用_init执行组件初始化的流程 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 // prop/computed初始化暂不分析 // ... // 扩展extension/mixin/plugin等函数 Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use // 扩展ASSET_TYPES中定义的函数包括component/directive/filter ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // 注册自身以支持循环调用 if (name) { Sub.options.components[name] = Sub } // 缓存各种配置 // 在后面将用于检查配置是否更新 Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // 缓存子构造器 cachedCtors[SuperId] = Sub return Sub } 复制代码
通过分析可以得到 Vue.extend
的主要功能就是继承了 Vue
的原型,定义了新的构造函数 VueComponent
与 Vue
进行区分,在构造函数中也很简单,就是执行 _init
函数。我们还是来总结下吧
- 校验组件名
validateComponentName
- 生成新的构造函数
Sub = VueComponent
- 合并配置,包括组件配置与全局配置,并定义到
Sub.options
- 扩展子构造函数
- 缓存配置及构造函数
2.2.2 resolveConstructorOptions合并配置
resolveConstructorOptions
这里的功能其实和 Vue.extend
中的合并配置差不多,区别就是会在这边再次校验缓存的配置是否有更新
export function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { const superOptions = resolveConstructorOptions(Ctor.super) const cachedSuperOptions = Ctor.superOptions // 判断配置是否更新 if (superOptions !== cachedSuperOptions) { // super option changed, // need to resolve new options. Ctor.superOptions = superOptions // check if there are any late-modified/attached options (#4976) const modifiedOptions = resolveModifiedOptions(Ctor) // update base extend options if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions) } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options } 复制代码
2.2.3 安装组件管理相关钩子
function installComponentHooks (data: VNodeData) { const hooks = data.hook || (data.hook = {}) // 遍历hooksToMerge合并componentVNodeHooks 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 } } } 复制代码
我们再来看看 componentVNodeHooks
的定义
const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch // ... } 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') } }, destroy (vnode: MountedComponentVNode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { deactivateChildComponent(componentInstance, true /* direct */) } } } } 复制代码
通过 installComponentHooks
函数为组件数据 data.hook
添加 componentVNodeHooks
中定义的钩子函数,钩子函数的具体实现我们具体调用的时候再进行分析,目前只需要知道在这边进行定义添加就行。
2.2.4 new 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 ) 复制代码
值得注意的是,此处的 children
并不是组件的根节点或者其它子节点,因为其定义在外层组件的 render
函数中,这边的 children
其实是关于 slot
的实现。
我们继续看看组件 vnode
的实例化
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag // 组件名 this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions // 组件配置 this.componentInstance = undefined // 组件实例(实例化之前为undefined) this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } 复制代码
通过参数对比可以发现组件虚拟节点的参数主要是 tag data context componentOptions asyncFactory
,其中 asyncFactory
和异步组件相关,与正常 vnode
不同之处在于其 tag
为组件名及 componentOptions
中保存了组件相关配置
2.3组件实例化
我们在前面的 1 -> 2
中梳理了组件的入口,组件构造函数的生成及组件节点 vnode
的生成,主要逻辑在于 _createElement
及 createcomponent
中。至此我们创建了组件的 vnode
,但是组件的实例化及渲染并没有涉及,其实组件实例化的过程在 patch
函数中。
2.3.1组件实例化入口
我们回到最开始的 patch
中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) // _vnode更新为最新的虚拟节点 vm._vnode = vnode if (!prevVnode) { // 首次渲染时将使用真实节点作为虚拟节点 // 对于子组件来说此时vm.$el为undefined vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() } 复制代码
关于 patch
的具体流程可以参考vue2-patch流程分析及vueDiff 算法解读,现在我们来重点分析其中的组件相关实现。因为源码比较多,我们会省略一些代码。我们来看看 patch
最终执行的函数 createPatchFunction
export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend // ... return function patch (oldVnode, vnode, hydrating, removeOnly) { // ... 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) { // 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 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) ) // ... // destroy old node if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } 复制代码
我们没有看到与组件相关的代码,但是能发现其主要逻辑是执行节点创建函数 createElm
,我们接着分析
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { vnode.isRootInsert = !nested // for transition enter check // 组件实例化入口 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // 真实节点创建相关 // ... 复制代码
2.3.2组件实例化实现
在 createElement
的执行中能看到创建组件的判断,我们分析其实现
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { 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) insert(parentElm, vnode.elm, refElm) return true } } } 复制代码
此时会先判断 vnode.data.hook.init
是否存在,如果存在则调用,这步是组件实例化的关键入口。init
的定义其实在上面我们分析过,存在 componentVNodeHooks
中,我们现在来分析下其实现。
2.3.2.1组件实例化调用
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) } } 复制代码
init
的逻辑主要是实现两步
- 执行
createComponentInstanceForVnode
并赋值给 vnode.componentInstance
- 调用
child.$mount
完成渲染逻辑
我们先看看 createComponentInstanceForVnode
export function createComponentInstanceForVnode ( vnode: any, parent: any ): Component { // 定义了构造函数的入参options // 仅包含三个参数_isComponent===true _parentVnode为组件vnode parent则会取到当前activeInstance也就是父组件实例 // 关于activeInstance的实现也很简单,大家可以自己去看看lifecycle中的_update实现即可。 const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, parent } // ... // 最后的结果是返回componentOptions.Ctor实例化的结果 // vnode.componentOptions.Ctor的生成我们在前面分析过 // 这边注意options不是开发者的组件配置 // 开发者对组件的配置数据其实在生成Ctor时就已经定义在静态属性Ctor.options中了 return new vnode.componentOptions.Ctor(options) } 复制代码
2.3.2.2组件初始化
我们继续复习下 Ctor
的定义
const Sub = function VueComponent (options) { this._init(options) } 复制代码
其中 _init
函数就是我们熟悉的组件初始化的函数
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag vm._isVue = true // merge options if (options && options._isComponent) { // 此时会执行initInternalComponent initInternalComponent(vm, options) } else { // ... } // 组件初始化的其它逻辑 vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') // 我们在组件中一般是不会定义el值的 // 所以不会直接调用vm.$mount进行渲染 if (vm.$options.el) { vm.$mount(vm.$options.el) } } } 复制代码
我们接着分析下 initInternalComponent(vm, options)
的逻辑
export function initInternalComponent (vm: Component, options: InternalComponentOptions) { // 定义了$options继承自Ctor.options const opts = vm.$options = Object.create(vm.constructor.options) // 同时将_parentVnode与parent属性赋值到vm.$options const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode // 同时将组件vnode的数据赋值到vm.$options const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } } 复制代码
2.3.2.3组件挂载渲染
至此,子组件的实例化也完成了,我们再看看前面所说的两步走的第二步
child.$mount(undefined) 复制代码
$mount
函数我们前面也有分析过,就是再执行组件的挂载渲染了
3.组件相关实现
本篇文章的重点组件实例化其实已经分析完,我们再来看看与组件相关的其它方面的实现
3.1 组件节点的处理
我们知道在创建虚拟节点的时候会有个组件节点,而我们在页面上看到的真实渲染是没有组件节点这个东西的,我们来看看组件节点是如何被跳过的,组件节点下面的节点又是如何找到正确的挂载父节点的。
我们知道在渲染组件的时候,会执行其中的 render
函数,而 render
函数外面又包了一层,在 core/instance/render.js
中
Vue.prototype._render = function (): VNode { const vm: Component = this // _parentVnode就是我们的组件节点 const { render, _parentVnode } = vm.$options // ... vm.$vnode = _parentVnode // render self let vnode try { currentRenderingInstance = vm vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // ... } finally { currentRenderingInstance = null } // ... // set parent vnode.parent = _parentVnode return vnode } 复制代码
通过分析可以看到,在 _render
中,其实会将组件节点 _parentVnode
作为 parent
赋值给 vnode
,而 vnode
来自于组件的 render
函数,所以实际在组件 _render
中,组件节点是不会被渲染在组件中的,而组件 render
返回的 vnode
会作为根节点返回。
以上代码解释了如何跳过组件节点的编译,我们再看看组件根节点是如何被挂载在父节点上的
在 patch
函数下的 createComponent
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 */) } if (isDef(vnode.componentInstance)) { // 在这调用initComponent initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } } 复制代码
其实节点的正确挂载逻辑在 initComponent
中
function initComponent (vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) vnode.data.pendingInsert = null } // 在这将vndoe.elm赋值为vnode.componentInstance.$el // vnode.componentInstance指向组件实例 // 其$el实际在_update函数中生成,实际是__patch__函数的结果 vnode.elm = vnode.componentInstance.$el if (isPatchable(vnode)) { invokeCreateHooks(vnode, insertedVnodeQueue) setScope(vnode) } } 复制代码
所以其原理是将组件创建的真实节点先赋值给组件节点上,对于组件根节点的 vnode
来说,vnode.parent.elm === vnode.elm
3.2 其它组件管理钩子
我们在上面定义了组件的管理钩子,但是后面只分析了其中 init
的实现,我们现在来看看其它钩子的实现。
3.2.1 prepatch
prepatch
的调用其实在 diff
的 preVnode
中,当我们的组件节点有更新时就会先调用 prepatch
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } 复制代码
在 prepatch
中主要调用 updateChildComponent
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 ) } 复制代码
因为在更新节点的时候,我们的组件其实是已经完成创建的,所以组件实例已经存在。在更新中如果发现组件节点 vnode
的有更新,则需要更新组件节点对应的组件实例相关数据,这就是 updateChildComponent
的主要任务。
export function updateChildComponent ( vm: Component, propsData: ?Object, listeners: ?Object, parentVnode: MountedComponentVNode, renderChildren: ?Array<VNode> ) { if (process.env.NODE_ENV !== 'production') { isUpdatingChildComponent = true } // 更新组件节点 vm.$options._parentVnode = parentVnode vm.$vnode = parentVnode // update vm's placeholder node without re-render if (vm._vnode) { // update child tree's parent vm._vnode.parent = parentVnode } vm.$options._renderChildren = renderChildren // 更新属性/事件 vm.$attrs = parentVnode.data.attrs || emptyObject vm.$listeners = listeners || emptyObject // update props if (propsData && vm.$options.props) { toggleObserving(false) const props = vm._props const propKeys = vm.$options._propKeys || [] for (let i = 0; i < propKeys.length; i++) { const key = propKeys[i] const propOptions: any = vm.$options.props // wtf flow? props[key] = validateProp(key, propOptions, propsData, vm) } toggleObserving(true) // keep a copy of raw propsData vm.$options.propsData = propsData } // update listeners listeners = listeners || emptyObject const oldListeners = vm.$options._parentListeners vm.$options._parentListeners = listeners updateComponentListeners(vm, listeners, oldListeners) } 复制代码
3.2.2 insert
在我们组件 patch
的最后一步,会执行 invokeInsertHook
// insertedVnodeQueue是在patch过程中收集的子组件 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) 复制代码
function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } } 复制代码
invokeInsertHook
会遍历触发组件的 insert
钩子
insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true // 逻辑很简单 就是调用生命周期mounted callHook(componentInstance, 'mounted') } } 复制代码
这也解释了为什么子组件的 mounted
会比父组件早,因为在父组件的 patch
中,会调用子组件的 patch
,而子组件的 patch
是先完成的。当然 insertedVnodeQueue
其实会有个存储的设计,因为 mounted
必须在根节点挂载后才能,所以并不是组件 patch
完成就会立即调用 insert
钩子。具体实现大家感兴趣可以自己去看看。
3.2.3 destroy
当移除组件节点的时候会触发 destroy
function removeVnodes (vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx] if (isDef(ch)) { if (isDef(ch.tag)) { removeAndInvokeRemoveHook(ch) invokeDestroyHook(ch) } else { // Text node removeNode(ch.elm) } } } } 复制代码
function invokeDestroyHook (vnode) { let i, j const data = vnode.data if (isDef(data)) { // 在这触发destroy if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode) for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode) } if (isDef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]) } } } 复制代码
我们来看看 destroy
的逻辑
destroy (vnode: MountedComponentVNode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { deactivateChildComponent(componentInstance, true /* direct */) } } } 复制代码
逻辑也很简,就是调用组件实例的 $destroy
,我们就不继续分析了。
总结
本篇文章分析了vue组件化
的实现,包括Vue实例
及VueComponent实例
的创建及渲染过程,篇幅比较长,很多方面讲的也不行。后面如果有时间的话会继续分析组件生命周期及节点更新函数,没有时间的话就先算了。anyway good good staduy day day up