Vue实例的源码解析 (实例二)

实例二

该实例的模板如下:

    <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')

获取render函数

当执行完这些后,根组件的数据初始化就基本完成了,当初始化完成之后就开始进行挂载部分。进行挂载调用的是: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函数

创建watcher实例

mountComponent函数中,主要做了这么几件事。首先是判断render函数,然后执行beforeMount钩子函数,接着定义updateComponent函数,然后执行new Watcher

Watcher构造函数中,首先创建一个与vm_0实例对象相对应的watcher实例对象,用这个watcher来代表vm_0这个组件,当watcher创建完成之后调用watcher.get函数。

该函数首先将watcher压入target栈中,用来注明现在是vm_0组件正在进行渲染的流程。然后执行updateComponent。该函数是用来进行挂载的,我们来具体看看该函数内部的操作。

生成根组件的vnode

进入到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的时候,oldVnodevm_0.$el。是一个真实的DOMvnode是我们自定义或者通过编译生成的虚拟节点。当我们首次执行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 
      )

其实这个函数的作用是创建一个组件实例。我们进入该函数中讲解具体过程。

创建mycom子组件实例

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

mycom子组件数据处理

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 = trueif (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上并没有挂载节点对象,所以也不会执行这个分支。所以会进行回退。

mycom获取render函数

一直回退到init函数当中,这个initdata.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 = falseif (vm.$vnode == null) {
     
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

因为代码很长,所以就删减了一部分。我们来看代码,因为我们的组件时子组件,所以不存在el。即vm_1.$el === undefined。然后对vm_1.$options.render做出判断,因为vm_1render函数是存在的,所以不会走这个分支。然后定义updateComponent函数接着就开始执行:new Watcher()构造函数了。

mycom创建watcher实例

  new Watcher(vm, updateComponent, noop, {
     
    before () {
     
      if (vm._isMounted && !vm._isDestroyed) {
     
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true)

和根组件一样,Vuevm_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_1watcher挂载到Dep.target上了。挂载到Dep.target的目的其实是很好理解的,那就是当前正在渲染的是vm_1组件,此时任何的属性被访问,那一定是vm_1实例访问的。为什么要将其推入栈中呢?其实是记录当前所执行的组件实例,因为我们可能会嵌套很多层组件,如何去分辨执行到哪个组件了,其实targetStack这个栈就有很大的辅助功能。

我们回到get函数中,接着执行:

 value = this.getter.call(vm, vm)

这个函数就是updateComponent。这个我们已经很熟悉了。在这个函数的内部执行:

 vm._update(vm._render(), hydrating)

生成mycom的vnode

首先是调用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)
    }
  }

创建aa子组件实例

在这个函数内部会调用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子组件实例对象。

aa子组件数据处理

当创建完成后执行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子组件实例的数据,事件等初始化。

获取render函数

当初始化完成之后回退到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_2render函数是存在的,所以不会走这个分支。然后定义updateComponent函数接着就开始执行:new Watcher()构造函数了。

aa创建watcher实例

Watcher函数的内部,此时会创建一个watcher实例对象,这个对象只属于aa组件实例对象,并且:

 vm._watcher = this

watcher挂载到vm_2_watcher属性上。其从这里我们就可以看到,每一个组件实例都会有自己的一个watcherwatcher实例就相当于组件实例的另一个唯一标识。当vm_2实例对象创建完自己的watcher实例对象之后调用watcher.get()函数,然后将vm_2watcher压入watcher栈中,然后将Dep.Target改为vm_2watcehr表示当前正在渲染的是vm_2组件实例,属性的任何访问都是它干的。然后就调用vm_2updateComponent函数。在该函数中执行了如下代码:

  updateComponent = () => {
     
      vm._update(vm._render(), hydrating)
    }

生成aa的vnode

首先调用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.$elundefined。这也是判别渲染的是根组件还是子组件的一种方式。当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组件的根节点,也就是divvnode。而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.elmmycom组件的根节点元素对象div。虽然vnodemycom组件节点的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。这里的vnodemycom组件的根节点元素的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^)*。

你可能感兴趣的:(vue,vue.js)