该实例的模板如下:
<div id="app">
<mycom>mycom>
<div>div>
div>
<script>
var vm = new Vue({
el:'#app',
data:{
message:123
},components:{
"mycom":{
template:' ',
data(){
return {
}
},
components:{
"aa":{
template:'',
data(){
return {
}
}
}
}
}
}
})
script>
主要是组件实例的创建流程。
对于我们的实例二,你可能因为实例一的组件渲染过程而对实例二心存忌惮,因为实例一只是一个根组件,但是却写了那么多的流程。其实你这样想是正常的,但是我们需要明白一点,那就是实例一的过程是创建一个组件,根组件是组件,那么子组件同样也是组件,也就是说,子组件也会走根组件创建时一样的流程。也就是说,在接下来的子组件嵌套的过程中,有绝大部分的代码是相同的,所以不用担心。为了流程的简洁,所以相同的部分我们会略讲,重点不同的地方我们会详细讲解。好了,开始我们的实例二的讲解吧。
首先我们来梳理一下我们将要创建的组件的结构:
<div id="app">
<mycom>mycom>
<div>div>
div>
<div>
<aa>aa>
div>
<span>span>
以上是我们组件的基本结构。了解了基本结构之后我们正式的进入源码中去解析。
首先通过new Vue
构造函数创建根组件的实例对象,我们称为vm_0
。
然后执行vm_0._init()
函数,我们前面讲了,该函数的主要作用是对组件实例的数据初始化。我们进入该函数,主要调用:
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')
当执行完这些后,根组件的数据初始化就基本完成了,当初始化完成之后就开始进行挂载部分。进行挂载调用的是:vm.$mount(vm.$options.el)
函数,该函数主要判断vm_0
实例对象是否有render
函数,很明显是没有,然后就会通过解析模板来生成render
函数,然后将其挂载到vm_0.render
上。
在这里重新回顾一下对于render
函数的获取流程。首先判断我们是否自己写的有render
函数,如果没有开始解析我们是否定义了template
配置,如果定义了那么就使用该配置项通过编译生成render
函数,如果我们没有定义template
,那么只能使用我们的根组件的id
所对应的模板当作template
。然后通过编译来生成render
函数。然后将生成的render
函数赋值给vm_0.render
。
当挂载完渲染函数之后,接着调用mount.call(this, el, hydrating)
函数,进行下一部分的功能模块。mount
函数内部调用mountComponent
函数。我们来看mountComponent
函数
在mountComponent
函数中,主要做了这么几件事。首先是判断render
函数,然后执行beforeMount
钩子函数,接着定义updateComponent
函数,然后执行new Watcher
。
在Watcher
构造函数中,首先创建一个与vm_0
实例对象相对应的watcher
实例对象,用这个watcher
来代表vm_0
这个组件,当watcher
创建完成之后调用watcher.get
函数。
该函数首先将watcher
压入target
栈中,用来注明现在是vm_0
组件正在进行渲染的流程。然后执行updateComponent
。该函数是用来进行挂载的,我们来具体看看该函数内部的操作。
进入到updateComponent
函数的内部:
updateComponent = () => {
//vm._update是在lifecycleMixin(Vue)中定义的
//vm._render是在renderMixin中定义的。
//hydrating:false
//该函数的执行其实是在new Watcher()中执行的,我们暂时只关注它的执行,不去关注在什么地方触发。
vm._update(vm._render(), hydrating)
}
首先是调用vm_0._render
函数,在实例一中我们讲过,该函数是用来生成组件的vnode
。现在我们就进入到该函数中来粗略的过一下它的具体流程。进入到_render
函数中,首先解析出render
函数,然后对vm_0
实例又挂载一些属性。然后就调用render.call
函数。render
函数的内部是这样的:
_c('div',{
attrs:{
"id":"app"}},[_c('mycom'),_v(" "),_c('div')],1)
关于_c
函数的内部处理机制,我们在实例一中已经讲解过了。但是这里我还是要讲一下,因为这里涉及到一个特殊的节点,那就是组件节点。当调用_c
函数的时候,它的内部调用的是createElement
函数,而这个函数内部调用的是_createElement
函数。我们来看这个函数内部的代码逻辑:
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)}
'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)
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
)
}
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)
console.log(vnode)
} 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)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
如果是正常的元素节点,那么毫无疑问的它会走:
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
这个分支,然后创建元素节点的vnode
。
但是有趣的是该节点的子节点中,有一个_c('mycom')
。也就是说它传入的tag
并不是一个原生的元素标签,而是一个我们自定义的一个标签。我们来看它会走哪个分支。很显然,他会走:
else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
debugger
vnode = createComponent(Ctor, data, context, children, tag)
console.log(vnode)
}
也就是说会调用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
}
const baseCtor = context.$options._base// Vue
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
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
const vnode = new VNode(
`vue-component-${
Ctor.cid}${
name ? `-${ name}` : ''}`,
data, undefined, undefined, undefined, context,
{
Ctor, propsData, listeners, tag, children },
asyncFactory
)
//......
return vnode
}
首先我们来解释一些参数:
Ctor:{
template: " ", components: {
…}, _Ctor: {
…}, data: ƒ}//其实就是我们传入的配置型。
data:undefined
context:vm_0
children:undefined
tag:"mycom"
先获取到Vue
构造函数,然后判断Ctor
是否是一个对象,很显然是的,然后将Ctor
传入:
Ctor = baseCtor.extend(Ctor)//将对象变成函数 baseCtor === Vue
因为调用了Vue.extend
函数,我们来看一下该函数内部的具体代码实现:
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
}
其实也很简单,该函数的内部主要做了两件事:第一,声明一个构造函数,然后向该构造函数上挂载各种属性,然后然会该函数。我们回到createComponent
函数中去,继续执行代码,后续的代码是异步组件和一些事件的处理,我们暂时忽略,我们先看主要的部分。当执行完上面的代码后会执行:
installComponentHooks(data)
installComponentHook
函数的主要作用是用来向data
对象中添加一些钩子函数的。主要是下面四个:
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
//.....
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
//....
},
insert (vnode: MountedComponentVNode) {
//....
},
destroy (vnode: MountedComponentVNode) {
//.......
}
}
当添加完之后就执行:
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
此时就会生成组件vnode
,这就是Vue
对于组件节点的处理。我们来看一下完整的vnode
生成结果。
//简易版本
{
tag:'div',
data:{
attrs: {
…}},
children:[
{
tag:"vue-component-1-mycom",
data:{
hooks: {
init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
on:undefined
},
children:undefined
},
{
tag: undefined,
data: undefined,
children: undefined,
},
{
tag:'div',
data:undefined,
children:undefined
}
]
}
节点来就是通过调用vm_0._updata
函数来创建真实节点了。
我们进入到vm_0._update
函数当中,在该函数中,因为是第一次调用,所以会调用:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
我们进入到vm_0.__patch__
函数中。当我们第一次渲染DOM
的时候,oldVnode
是vm_0.$el
。是一个真实的DOM
。vnode
是我们自定义或者通过编译生成的虚拟节点
。当我们首次执行patch
函数的时候,它会执行如下代码:
oldVnode = emptyNodeAt(oldVnode)
emptyNodeAt
函数的作用是将真实的DOM
转化为虚拟DOM
。其实为什么要这样做,理由是可以想明白的。如果我们修改了数据,然后重新进行渲染。在渲染前Vue
会将每一个节点通过diff
算法进行对比,目的是为了减少操作DOM
的次数,但是在对比节点的时候不是通过真实DOM
来进行比较的,而是通过原有的虚拟DOM
和修改后的数据的虚拟DOM
进行比较,此时我们就需要将我们的真实的vm.$el
转化为我们的虚拟DOM。
当我们上述地数据准备好之后,开始执行接下来的代码:
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
从这个函数开始,开始创建真正的节点对象。我们来看看,当组件实例对象的vnode
中有组件节点,看Vue
是怎么处理的。我们再来看一下vnode
。
{
tag:'div',
data:{
attrs:{
"id":"app"}},
children:[
{
tag:"vue-component-1-mycom",
data:undefined,
children:undefined
},
{
text:" "
},
{
tag:'div',
data:undefined,
children:undefined
}
]
}
当进入到createElm
函数的时候,首先判断我们的这个vnode
是否是一个组件,很显然不是,然后就创建div
元素节点,然后遍历children
逐个调用createElm
函数。
首先是children[0]
即我们组件vnode
。首先调用:
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
因为是组件vnode
,所以满足这个条件,所以我们来看看该函数的内部代码:
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)) {
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)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
第一步就是获取到vnode.data
对象,这里存有四个函数,我们上面说过,然后判断init()
函数是否存在,很显然是存在的,所以会调用init(vnode)
函数。我们来看看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 {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
if
分支是用来处理keep-alive
的,所以我们暂时忽略。我们执行else
分支。这执行else
分支的时候,会执行:
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
其实这个函数的作用是创建一个组件实例。我们进入该函数中讲解具体过程。
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)
}
createComponentInstanceForVnode
函数内部比较简单,我们来看一下它到底做了什么。首先是定义一个options
配置对象,然后判断if (isDef(inlineTemplate))
。这个条件是不成立的,所以会执行:
new vnode.componentOptions.Ctor(options)
vnode.componentOptions.Ctor
函数是什么,你是否还记得在组件vnode
创建的时候,Vue
通过Vue.extends
函数将我们的Ctor
配置对象变成了Ctor
子组件的构造函数。也就是说该函数就是Sub
构造函数。
const Sub = function VueComponent (options) {
this._init(options)
}
当我们执行new Sub()
的时候,其实相当于执行new Vue
。它们的结果都是一样的,目的是创建一个组件实例。只不过new Vue
函数创建的是根组件实例,而new Sub
构造函数创建的是子组件实例。为什么说是相当于执行new Vue
函数呢,原因是因为我们在内部定义Sub
构造函数的时候,把Vue
构造函数的大部分属性也都挂载到了Sub
构造函数上,也就相当于一个小的Vue
。但是却不完全是,因为Vue
构造函数的主要任务是创建根组件实例,而Sub
函数的主要任务是创建子组件实例。其目的性不同,但是因为都同属组件实例,在很多地方两者都有相似的地方。我们就进入到Sub
构造函数当中,来看它是怎么创建及处理子组件实例的。
当我们new Sub
构造函数的时候,就已经创建了一个子组件实例对象,其原型上也挂载了很多东西。首先就是执行vm_1._init
方法来初始化我们子组件实例vm_1
。
vm_1._init
函数是继承过来的,该函数是继承自Vue.prototype._init
函数中的。也就是子组件实例调用的_init
函数本质上就是_init()
函数,该函数具体代码如下:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${
vm._uid}`
endTag = `vue-perf-end:${
vm._uid}`
mark(startTag)
}
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {
},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
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')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${
vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
首先就是将我们新创建的子组件实例对象添加一个唯一的标识vm_1._uid = 1
。然后将vm_1._isVue
置为true
。
在这里会遇到一个分支:
if (options && options._isComponent) {
initInternalComponent(vm, options)
}
很显然,options
是存在的,那么options._isComponent
又是什么呢?当我们执行createComponentInstanceForVnode
函数的时候,会有一个这样的操作:
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
然后将options
传入到Sub
函数当中,_isComponent
属性的作用是标记该vnode
是一个组件节点。带调用_init
函数的时候,该函数就会通过options._isComponent
来判断进行初始化的实例对象是根组件实例对象还是子组件实例对象。在这里,vm_1
是一个子组件实例对象,所以会走initInternalComponent
函数。我们进入该函数内部,看它是如何对vm_1
子组件实例进行初始化的。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
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
}
}
首先给vm_1
实例对象添加$options
属性,该属性的原型是Vue.options
。接着就是向vm_1.$options
上添加一些必要的属性。我们回退到_init
函数当中,接着就是执行一个数据和事件的处理:
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')
最后进行判断:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
因为vm_1.$options.el
上并没有挂载节点对象,所以也不会执行这个分支。所以会进行回退。
一直回退到init
函数当中,这个init
是data.hooks
中的init
函数。回到到该函数当中后会执行下面代码:
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
这里的child
就是vm_1
子组件实例对象。因为继承了Vue.prototype
。所以会有$mount
函数。这个函数我们前面也讲过,但是为了更好的分析代码,所以不得不把它放出来:
Vue.prototype.$mount = function (
el?: string | Element,//我们传入的el类型有两种,一种是字符串'#app',另一种是一个元素对象,例如document.getElementById('app')
hydrating?: boolean
): Component {
el = el && query(el)
if (el === document.body || el === document.documentElement) {
}
const options = this.$options//这里的this指向的是vm实例对象
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
//如果配置项的类型为字符串。
if (template.charAt(0) === '#') {
//这里我们只处理template为#xxx的格式的模板,也就是类似于template:'#app'这种
template = idToTemplate(template)//该函数返回的是template模板内部的节点的字符串形式。
}
} else if (template.nodeType) {
//如果我们传入的template是一个节点对象,那么获取该节点对象中的innerHTML,然会的也是字符串形式
template = template.innerHTML
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
const {
render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
因为代码逻辑太长了,所以我把走不到或者说是不需要的代码全都删除了。因为是子组件实例,所以没有el
元素。
通过分析上面的代码,其实我们可以发现Vue
对于子组件的处理和对根组件的处理在这方面是出奇的一致,只不过这里没有el
元素,所以当我们不定义模板和render
函数的情况下是不能进行渲染的,相当于一个空节点,虽然会有子组件实例,但是并没有什么作用。
其实这个函数就是判断vm_1
实例对象有没有render
函数,如果没有就通过tempalte
模板编译来生成render
函数。因为我们在子组件中没有写render
函数,所以这里就通过template
模板编译来生成render
函数:
template:' '
在vm_1
子组件中我们发现了一个奇怪的现象,那就是这个模板好像混入了什么东西。没错又是一个子组件,也就是说子组件vm_1
的内部又嵌套了一个子组件。那么接下来我们的任务就重了,那就是再分析一次从编译到vnode
生成的过程。我们继续来看代码,当执行compileToFunctions
会将模板编译为render
函数,然后将render
函数挂载到vm_1.options.render
上。为我们后面的vnode
生成做准备。
然后调用mount.call
函数,mount.call
函数调用本质是调用:
mountComponent(this, el, hydrating)
所以我们进入到该函数中。该该函数的代码如下:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
//.......
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
//这和运行性能有关,暂时可以忽视
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
//......
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
因为代码很长,所以就删减了一部分。我们来看代码,因为我们的组件时子组件,所以不存在el
。即vm_1.$el === undefined
。然后对vm_1.$options.render
做出判断,因为vm_1
的render
函数是存在的,所以不会走这个分支。然后定义updateComponent
函数接着就开始执行:new Watcher()
构造函数了。
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true)
和根组件一样,Vue
为vm_1
子组件创建一个只属于它watcher
实例对象。我们来进入到该函数来具体聊聊里面的细节。
//constructor函数
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.before = options.before
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${
expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
当创建并初始化完之后调用watcher.get()
函数:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${
this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()//
this.cleanupDeps()
}
return value
}
这个函数就有点意思了。首先会执行pushTatget(this)
。这里的this
指向的是vm_1.watcher
对象。我们进入到pushTarget
函数当中:
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
看到没?它把vm_1
组件的watcher
推进到了targetStack
栈当中,然后把vm_1
的watcher
挂载到Dep.target
上了。挂载到Dep.target
的目的其实是很好理解的,那就是当前正在渲染的是vm_1
组件,此时任何的属性被访问,那一定是vm_1
实例访问的。为什么要将其推入栈中呢?其实是记录当前所执行的组件实例,因为我们可能会嵌套很多层组件,如何去分辨执行到哪个组件了,其实targetStack
这个栈就有很大的辅助功能。
我们回到get
函数中,接着执行:
value = this.getter.call(vm, vm)
这个函数就是updateComponent
。这个我们已经很熟悉了。在这个函数的内部执行:
vm._update(vm._render(), hydrating)
首先是调用vm._render()
,在vm._render
函数的内部会执行:
render.call(vm._renderProxy, vm.$createElement)
render
函数最终的样子是:
with(this){
return _c('div',[_c('aa')],1)}
该函数内部会调用_c
函数,因为_c
函数内部我们讲过,所以这里我们就直接跳过内部,直接看其生成的vnode
。
{
tag:'div',
data:undefined,
children:[
{
tag:"vue-component-2-aa",
data:{
on: undefined,
hook: {
init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
},
child:undefined
}
]
}
其实和根实例生成的vnode
很像。当生成完vnode
之后调用vm_1._update()
函数。
我们进入到vm_1._update
函数当中,在该函数中,因为是第一次调用,所以会调用:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
我们进入到vm_1.__patch__
函数中。这是第一次渲染子组件,所以传入到patch
函数中的oldVnode
是不存在的。当oldVnode
不存在。Vue
就知道了此时需要渲染的是子组件。所以会调用:
isInitialPatch = true
createElm(vnode, insertedVnodeQueue/* true*/)
我们进入createElm
函数当中:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
//.......
} else {
//
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
//......
} else {
//.....
}
}
我们来回顾一下vm_1
组件实例的vnode
:
{
tag:'div',
data:undefined,
children:[
{
tag:"vue-component-2-aa",
data:{
on: undefined,
hook: {
init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
},
child:undefined
}
]
}
因为该组件实例的vnode
是一个元素节点,所以if
条件是不成立的。然后通过调用:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
创建处真正的div
元素节点,当创建完节点之后开始创建子节点对象,在createChildren
函数中通过for
循环来逐一遍历vnode.children
。将每一个子节点vnode
都执行一遍createElm
函数。
当执行第一个子节点vnode
的时候就发现这个子节点是一个组件vnode
。此时就会调用:
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
将组件节点的vnode
传入。我们来看这个函数:
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)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
和以前一样,同样是判断该节点上是否有data.hook
。如果有那么就判定是组件节点,很显然是有的,所以会执行:
i(vnode, false /* hydrating */)
<===>
init(vnode,false)
我们进入到该函数内部:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
//..........
else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
在这个函数内部会调用createComponentInstanceForVnode
函数来创建一个新的子组件实例,我们进入到该函数内部来看一看:
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)
该函数内部很简单,就是调用vnode.componentOptions.Ctor
构造函数,即Sub
构造函数。通过new Sub()
来创建vm_2
子组件实例对象。
当创建完成后执行vm_2._init()
函数,对vm_2
子组件实例对象进行初始化。
Vue.prototype._init = function (options?: Object) {
debugger
//定义一个vm并指向this
const vm: Component = this
// a uid
//为这个vm实例添加一个唯一的uid,每一个实例都有一个唯一的标识符
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${
vm._uid}`
endTag = `vue-perf-end:${
vm._uid}`
mark(startTag)
}
//监听对象变化时用于过滤vm
// a flag to avoid this being observed
vm._isVue = true
// merge options
// _isComponent是内部创建子组件时才会添加为true的属性
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
//合并options
vm.$options = mergeOptions(
//resolveConstructorOptions函数在后面有定义
//向该函数传入的是vm.constructor也就是Vue
resolveConstructorOptions(vm.constructor),
options || {
},
vm
)
//vm.$options合并两项
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
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')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${
vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
初始化也就是那几个步骤,第一像vm_2
实例对象挂载唯一标识vm_2._uid = 2
。然后判断初始化的这个组件是不是一个子组件,很显然是,那就执行:
initInternalComponent(vm, options)
该函数就是向vm_2.$options
上挂载这种属性。然后执行:
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')
进行vm_2
子组件实例的数据,事件等初始化。
当初始化完成之后回退到init
函数当中调用vm_2.$mount
函数。
Vue.prototype.$mount = function (
el?: string | Element,//我们传入的el类型有两种,一种是字符串'#app',另一种是一个元素对象,例如document.getElementById('app')
hydrating?: boolean
): Component {
el = el && query(el)
if (el === document.body || el === document.documentElement) {
}
const options = this.$options//这里的this指向的是vm实例对象
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
//如果配置项的类型为字符串。
if (template.charAt(0) === '#') {
//这里我们只处理template为#xxx的格式的模板,也就是类似于template:'#app'这种
template = idToTemplate(template)//该函数返回的是template模板内部的节点的字符串形式。
}
} else if (template.nodeType) {
//如果我们传入的template是一个节点对象,那么获取该节点对象中的innerHTML,然会的也是字符串形式
template = template.innerHTML
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
const {
render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
该函数的主要作用是通过模板编译生成render
函数,然后将其挂载到vm_2.options.render
属性上。然后调用mount.call
函数mount.call
函数调用本质是调用:
mountComponent(this, el, hydrating)
所以我们进入到该函数中。该该函数的代码如下:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
//.......
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
//这和运行性能有关,暂时可以忽视
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
//......
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
因为代码很长,所以就删减了一部分。我们来看代码,因为我们的组件是子组件,所以不存在el
。即vm_2.$el === undefined
。然后对vm_2.$options.render
做出判断,因为vm_2
的render
函数是存在的,所以不会走这个分支。然后定义updateComponent
函数接着就开始执行:new Watcher()
构造函数了。
在Watcher
函数的内部,此时会创建一个watcher
实例对象,这个对象只属于aa
组件实例对象,并且:
vm._watcher = this
将watcher
挂载到vm_2
的_watcher
属性上。其从这里我们就可以看到,每一个组件实例都会有自己的一个watcher
,watcher
实例就相当于组件实例的另一个唯一标识。当vm_2
实例对象创建完自己的watcher
实例对象之后调用watcher.get()
函数,然后将vm_2
的watcher
压入watcher
栈中,然后将Dep.Target
改为vm_2
的watcehr
表示当前正在渲染的是vm_2
组件实例,属性的任何访问都是它干的。然后就调用vm_2
的updateComponent
函数。在该函数中执行了如下代码:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
首先调用vm._render()
函数来创建aa
组件所对应的vnode
。我们进入到_render()
函数中,该函数主要是调用了:
vnode = render.call(vm._renderProxy, vm.$createElement)
我们来看render
函数的内部。
with(this){
return _c('span')}
所以它通过调用_c
函数生成的vnode
应该是这样:
{
tag:'sapn',
data:undefined,
children:undefined
........
}
当获取到vnode
之后开始进行回退。一直回退到updateComponent
函数当中。
当执行完vm._render()
函数之后会返回vm_2
组件所对应的vnode
。然后执行vm_update()
函数,这个函数是用来创建真实的节点对象的。因为该子组件渲染时第一次,所以会执行下面的代码:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
因为__patch__
就是patch
。所以我们进入patch
函数来看内部的执行过程。因为现在创建的时子组件实例,所以vm.$el
为undefined
。这也是判别渲染的是根组件还是子组件的一种方式。当isUndef(oldVnode)
为真的时候,此时Vue
就知道现在渲染的是子组件,所以会执行:
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
在调用createElm
函数的时候,将aa
组件的vnode
传入。我们来看该函数的内部实现:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
//......
} else {
//
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
}
} else if (isTrue(vnode.isComment)) {
} else {
}
}
其实代码逻辑很简单,首先判断这个vnode
是否是一个组件vnode
。很显然不是,然后继续执行:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
创建该vnode
所对应的span
真实节点对象。创建之后开始执行createChildren
函数,因为这个span
节点并没有子元素,所以执行这个函数并没有什么作用,然后调用insert(parentElm, vnode.elm, refElm)
。
在执行insert()
函数的时候我们需要搞清楚我们传入的参数:
parentElm:父组件根元素节点
vnode.elm:待插入的真实节点
refElm:替代节点
我们来看具体的插入操作:
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
当我们真正的去观察我们传入的参数的时候,我们会发现传入的参数值对应如下:
parent:undefined
elm:[Object]
ref:undefined
我们发现,parent
竟然是undefined
。也就是说没有办法进行插入操作????其实不是的,这个insert
函数进行的操作针对的是普通的节点,什么意思呢?我们现在是不是想把子组件的节点出插入到父组件的容器中,这样就进行了跨组件插入。这个函数处理的是在同一组件内,例如:
<div>
<span>1111span>
div>
假如有一个组件模板是这样的,insert
函数处理的是把span
节点插入到div
中或者111
文本节点插入到span
节点当中。也就是在组件内部的插入。但是要把div
节点插入到父组件的容器中就没有那么容易了,至少不使用这个函数。
那么将子组件的根节点插入到父组件的指定位置应该用什么呢?我们先继续执行代码。当执行完insert
函数后(只是走过场,没有真正执行什么)。进行函数回退,回退到patch
函数中去,然后就执行:
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
注意,vnode.elm
包含了子组件aa
的根组件的真实节点对象。我们来看invokInsertHook
函数:
function invokeInsertHook (vnode, queue, initial) {
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])
}
}
}
该函数的逻辑比较简单,我们先看传入的参数的值:
vnode:[Object]
queue:[] //空
initial:true
我们进入到函数当中,首先判断if
分支的真假性,因为initial === true
第一个条件成立,然后判断第二个条件:isDeff(vnode.parent)
就是判断vnode.parnet
是否存在。那么这个vnode
是否存在呢?要想知道答案我们需要知道vnode.parent
属性是在哪里定义并挂载的,很显然是在创建vnode
的时候定义的。我们来看它定义的过程。要想生成vnode
。就必须要执行render.call
。而该函数的内部调用了_c
函数,在执行_c
函数的内部的时候,会执行createElement
函数会传入一个至关重要的东西,那就是context
。也就是vm_2
。我们的aa
子组件实例。然后将vm_2
传递给_createElement
函数。在该函数中执行new VNode
构造函数,在构造函数中执行一些初始化,此时的vnode.parent === undefined
。然后代码回退,一直回退到vm_2._render()
函数中,会执行这样一行代码:
vnode.parent = _parentVnode;
_parentVnode
就是vm_2
实例对象。
现在我们已经知道了vnode.parent
是谁了,所以就知道它下一步该执行什么了:
vnode.parent.data.pendingInsert = queue
这样代码将vm_2
实例对象的data
属性中的pendingInsert
置为[]
。然后退出函数进入到patch
函数当中,patch
直接返回vnode.elm
。也就是我们aa
子组件的根节点
。然后继续回退函数,一直回退到Vue._update()
函数当中,然后:
if (vm.$el) {
vm.$el.__vue__ = vm;
}
子组件是存在vm.$el
的。每一个子组件都有自己的vm.$el
。该值指向的是该组件实例的根节点。但是在首次渲染组件的时候是存在的,只有执行完这段代码才有:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
该函数会返回vnode.elm
。也就是根节点元素。
回到正题。然后继续执行后续的代码,然后进行回退,一直回退到watcher.get()
函数当中因为在这个函数执行的updateComponent
函数。
接着就会执行popTarget()
函数,该函数函数将vm_2
实例对象的watcher
从我们的watcher
栈中弹出,表示该组件的渲染已经结束,属性的访问和它无关。然后执行watcher.cleacDeps
。然后一直回退,回退到createComponent
函数,这个函数中我们调用了:
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
所以才会回退到这里的。然后执行:
initComponent(vnode, insertedVnodeQueue);
我们来看看这个函数内部做了什么:
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
registerRef(vnode)
insertedVnodeQueue.push(vnode)
}
}
首先我们来看initComponent
函数传入的参数。首先是vnode
。这个vnode
已经不是我们之前的那个span
根元素节点,而是aa
组件节点,这个节点是相对于mycom
组件模板中的节点而言的。该节点的vnode.data.pendingInsert
属性是不存在的,然后获取vnode.componentInstance.$el
。这个节点才是我们上面说的span
节点,即:
vnode.componentInstance.$el === span
然后将span
节点来替代aa
组件节点。节从此刻起,vnode.elm
就是span
节点。
虽然上面只是寥寥几笔,但是完成了一个很重要的事,那就是对组件节点的替换。因为当我们在模板中出现我们自定义的组件的时候,不能说把我们自定义的组件给插入到元素节点中去吧,这是不合适,也是不合理的,因为浏览器并不认识我们写的自定义组件,而我们真正想插入的其实是组件的模板。所以将组件模板的根节点来代替我们的组件节点,这就是:
vnode.elm = vnode.componentInstance.$el
这段代码的作用。
然后向下执行代码,接着就会调用invokeCreateHooks
函数,在这个函数中执行了一些操作,然后执行:
if (isDef(i.insert)) {
insertedVnodeQueue.push(vnode); }
将vnode
。也就是aa
组件实例的vnode
添加到vnode
插入队列当中。然后就一直回退到createComponent
函数当中,执行:
insert(parentElm, vnode.elm, refElm);
该函数才是真正的插入操作。我们来看看它传入的参数:
parentElm:div
vnode.elm:span
refElm:undefined
这里的insert
函数要特别的讲解一次。首先我们来看insert
的执行环境,其实这个insert
执行的是mycom
组件中的元素的插入操作。但是此时mycom
组件中的aa
节点已经被替换成了aa
组件实例的根节点(span
)。所以aa
组件节点就等同于span
。懂我意思吧。也就是说以前是:
<div>
<aa>aa>
div>
现在进行了替换,所以就相当于是:
<div>
<span>span>
div>
将span
节点插入到mycom
组件的指定位置,而这个指定的位置就是
组件节点所在的位置。
以上就是我们组件节点的插入工作,其实回过头来看,其实麻烦但是好理解。
现在我们把aa
组件节点进行了替换并挂载到了我们的mycom
组件的根节点中,接着就是继续向下遍历剩余的子节点。我们回到createElm
函数当中,这个函数是mycom
组件在遍历子组件的时候调用的。
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
上面的代码就是我们对aa
组件的处理,当处理完成后我们来看后续代码,接着就是退回到createChildren
函数中:
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
接下来就是继续遍历mycom
组件实例的子节点。其实通过代码我们可以发现,这里只有一个子节点,那就是我们的aa
组件实例,其实如果后面还有元素节点的话,依旧会调用createElm
函数,然后创建并执行insert
函数将其挂载到mycom
组件的根节点上。如果子节点是一个组件节点,那么就会像aa
组件节点一样去处理,什么创建实例啊,render
函数获取啊,然后是watcher
之类的,然后就是进行节点的替代并挂载。
当没有子节点之后就会回退,回退到createElm
函数,这个函数是用来创建我们的mycom
组件节点,因为在我们的根组件实例中,我们引入了改组件:
<div id="app">
<mycom>mycom>
<div>div>
div>
而改组件的挂载不能和普通的元素节点一样,所以后面的insert
函数是不起作用的。即:
insert(parentElm, vnode.elm, refElm);
那生成好的mycom
真实DOM
节点该如何挂载呢,其实很简单,有同学可能已经想到了,那就是和aa
组件一样的做法。为了巩固组件节点的挂载流程,我们再来进行一次具体流程的讲解。
当执行完insert
函数后(本质上没有其任何作用,只是走一个过场)。然后就进行回退。回退到patch
函数之后开始执行invokeInsertHook
函数,该函数的作用是执行下面的代码:
vnode.parent.data.pendingInsert = queue;
这里的vnode
指的是aa
组件的根节点,也就是div
的vnode
。而vnode.parent
就指的是aa
组件节点的vnode
。然后就返回vnode.elm
。这里的vnode
指向的是aa
组件的根节点的vnode
。那么理所当然的vnode.elm
就指向div
节点。
也就是说我们以上的操作还是在mycom
组件的根节点内部的,还没有跳出mycom
组件来到根组件实例模板内部。也就是说我们现在操作的是mycom
组件而不是vm_0
根组件。接着继续回退,一直回退到watcher.get()
函数当中,这里的watcher
是根组件实例的watcher
。因为在这个函数中执行了根组件的updateComponent
函数。
接着就是执行watcher.cleanDeos
函数,然后回退代码,一直回退到createComponent
函数当中。接着执行:
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
我们来回顾一下我们什么时候执行的`createComponent`函数的?其实是在当组件去循环遍历内部模板的`vnode`生成真实节点的时候,当遇到一个组件节点的时候执行该函数。
我们回到该函数,来看一下后续的操作,所有就是if
判断,这个判断是成立的。然后执行initComponent()
函数。这个函数做了一件极为重要的事,那就是执行了:
vnode.elm = vnode.componentInstance.$el;
即将我们的组件节点替换为了改组件内部的根节点,为什么这样做,我们前面有讲过。然后就是调用了:
invokeCreateHooks(vnode, insertedVnodeQueue);
setScope(vnode);
该函数将我们需要插入的vnode
放到一个队列中。然后退出函数,然后回退到createComponent
函数当中,接下来执行insert
操作。此时我们现在做的操作是在vm_0
即根组件实例环境中的。而传入到insert
函数中的parentElm
就是根组件的根节点,vnode.elm
是mycom
组件的根节点元素对象div
。虽然vnode
是mycom
组件节点的vnode
。按理来说vnode.elm
是子组件所对应的节点元素,由于各种因素我们在前面做了替换。当执行完insert
函数之后,我们的mycom
组件的DOM
就被插入到了根组件实例的根节点上的指定位置上了。然后就是一直回退,一直回退到createChildren
函数当中。该函数的作用是逐个遍历根组件内部的子节点,然后逐个调用createElm
函数,createChildren
函数是在创建根组件实例的根节点的时候调用的。
当createComponent
函数执行完之后就说明这个组件节点已经被插入到mycom
组件的对应节点上了。然后回到createElm
函数中,我们发现当我们处理完组件节点后,我们回到createElm
函数执行的是return
操作,也就是说我们处理完组件节点,那么当前的任务就完成了,然后回到createChildren
函数当中,然后继续执行:
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
继续去遍历mycom
组件中的其他节点,因为这里mycom
组件中只有aa
一个子组件,所以createhildren
函数执行完毕,然后返回。当这个函数返回,那么就代表一件事,那就是mycom
组件根节点内部的子节点被创建完毕,然后判断mycom
根节点是否有data
。这里mycom
组件的根节点是没有的。所以跳过分支。然后执行:
insert(parentElm, vnode.elm, refElm)
因为传入到insert()
函数中的parentElm
的值为undefined
。因为我们要插入的值是mycom
组件节点的根节点,所以执行insert()
函数并不会有任何作用,只是走一个过场。因为这是对子组件的根节点的插入操作,所以此时的insert
就没有任何作用了。然后回退到patch
函数当中,然后执行:
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
当该函数执行完毕之后就返回vnode.elm
。这里的vnode
是mycom
组件的根节点元素的vnode
。那么vnode.elm
就是真实的根节点,即div
元素节点。
接着就一直回退,回退到watcher.get
函数当中,这里的watcher
指的是mycom
组件的watcher
。为什么呢,因为我们在这个函数中调用的updateComponent
函数,也就是说mycom
回退到了执行new Watcher
函数的时候了。然后我们向后看它做了什么,接下来就是执行:
popTarget();
this.cleanupDeps();
第一,将vm_1.watcher
函数从我们的watcher
栈中移除,表示现在任何属性的访问都和vm_1
实例没有任何的关系。第二就是将watcher
上的旧依赖全部清除。
当执行完这些后就开始一直回退,一直回退到createComponent
函数当中,现在执行这个函数的其实是我们根组件实例,即vm_0
。执行这个函数的原因是因为在根组件中我们引入了mycom
组件,所以在进行节点创建的时候会执行这个函数,好了我们来看该函数接下来执行了什么:
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
首先是执行了initComponent
函数,传入的参数是vnode
和一个空数组,在这个函数中做了一个非常重要的事情,那就是:
vnode.elm = vnode.componentInstance.$el;
vnode
指向的是mycom
组件实例,理所当然的vnode.elm
也应该是改组件节点,但是我们前面说了,浏览器不认识我们当以的组件节点,所以就把该组件节点的根节点元素赋值给了vnode.elm
。然后返回,此时的vnode
节点的elm
上就是mycom
组件的根节点元素了。这样就完成了一次组件节点的迁移。然后执行后续代码后就返回。然后返回到createComponent
函数当中。接着就执行insert()
函数,此时的parentElm
就是执行位置的父节点元素,vnode.elm
就是mycom
组件的根节点元素,执行完这个函数之后,将相当于把mycom
组件内部的DOM
节点插入到了根组件的指定位置了,为什么说是指定的位置呢,因为有时候是这样的一个结构:
<div>
<span>
<mycom>mycom>
span>
div>
该组件并不是直接存在于根节点之下的,所以parentElm
指的就是指定位置的父节点,按照 我们刚才局的例子就是span
元素节点。
当执行完insert
函数之后,那么此时就把mycom
内部的元素插入到了我们的根组件的根节点的内部了。然后开始回退。
一直回退到createChildren
函数当中。
当回退到createChildren
函数之后,就代表着Vue
的工作即将完成。因为这个函数是vm_0
也就是根组件实例调用的,调用它用来执行创建子节点元素的。当回退到该函数的时候会执行下面的代码:
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
因为这个函数执行的时候中途遇到了组件节点,然后完美的解决了,然后继续向下执行。在执行之前我们来看一下根组件的节点:
<div id="app">
<mycom>mycom>
<div>div>
div>
mycom
组件节点已经处理完了,然后就是对文本节点进行处理,对于文本几点的处理也很简单,依旧实调用createElm
函数,因为该节点是一个文本节点,所以会执行:
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
来创建一个文本节点,然后将该文本节点插入到根组件的根节点中。当执行完后就回到createChildren
函数中。
然后就是对div
执行createElm
函数了。其实对于div
的创建很简单,首先是调用createElm
,在该函数中会执行:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
因为此时的vnode
指向的是span
节点,所以这里创建的是span
节点,即vnode.elm
是真实的span
节点,因为span
节点没有子节点,所以调用createChildren
并没有什么作用。接着就执行insert
函数,该函数的作用就是将span
节点插入到根组件的根节点中的指定位置,然后返回。代表此时的节点已经创建并挂载完成,然后进行回退。回退到createChildren
函数当中。至此根组件中的所有子节点都已经处理完毕了。然后进行函数的回退,一直回退到createElm
函数当中,这个函数是vm_0
也就是根组件实例进行调用的,此时这里的vnode
即使根组件的根实例。
接下来执行:
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
因为是根组件的根节点,所以是有data
选项的,所以会调用invokeCreateHook
函数。
当处理完成后就执行:
insert(parentElm, vnode.elm, refElm);
可能有同学会产生疑惑,为什么这里还有一个insert
操作,这个操作是将我们的根组件的根实例插入到我们的页面的DOM
当中。其实去哦们追求其本质,我们上面做的各种插入操作其实就是在内存中存在的DOM
节点,但是还没有呈现到页面上,通过上面的insert
函数将我们的根组件插入到body
中,这样就完成了把我们的组件渲染到页面上的任务了。传入到insert
函数中的parentElm
参数其实是页面中的body
元素。接着就开始函数回退,一直回退到patch
函数当中,然后判断parentElm
是否存在,该函数的目的是移除节点。移除节点?移除什么节点?其实当我们把组件挂载到页面上去的时候,页面会出现两个根组件。这样的情况当然是不被允许的,所以就会执行removeVnodes
函数,将旧的根组件从页面上移除掉。
当移除后接着调用invokeInsertHook
函数,执行完该函数之后就开始进行函数回退,一直回退到Watcher
函数当中,然后执行:
popTarget();
this.cleanupDeps();
第一,将根组件的watcher
从栈中移除,表示现在有任何的属性的访问,哪都不管根组件的事情。第二,将根组件的旧deps
移除。接着就一直回退,一直回退到Vue
构造函数中。
此时,我们的Vue
流程就此结束。
完结撒花*(^0^)*。