vue组件初始化过程
之前文章有写到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() } }
如上图所示,主流程与实例化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,然后再讲老旧节点移除。
上一篇: 寒假集训第一天---并查集题解
下一篇: 爬虫(六):XPath、lxml模块