菜鸟初探Vue源码(四)-- 组件化

Vue.js 的另外一个核心思想是组件化。所谓组件化就是将页面拆分红多个组件,组件之间资源相互独立,组件能够复用,组件之间也能够嵌套。 接下来以Vue/CLI初始化的代码为例,分析一下Vue组件初始化的过程。vue

import Vue from 'vue'
import App from './App.vue'

new Vue({
  render: h => h(App),
}).$mount('#app')
复制代码

本篇要从_createElement方法提及(_createElement -> createElement -> $createElement -> render -> _render),此处是 render 函数生成 vnode 过程当中差别出现的地方。node

export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> {
  // 对children作normalization,最终统一形式[vnode, vnode, ...]
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // tag是字符串
  if (typeof tag === 'string') {
    // ...
  } else {
    // tag是组件
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
复制代码

此处的tag是一个组件对象,因此会进入else逻辑,vnode将由createComponent方法生成(实际是生成一个占位 vnode )react

export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
  // Vue
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  // args order: tag、data、children、text、elm、context、componentOptions、asyncFactory
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}
复制代码

生成占位 vnode 过程当中,利用Vue.extend()tag转化为子组件的构造器。并做为new Vnode()的其中一个参数的属性传入,等待调用。还调用了下面提到的installComponentHooksapp

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 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
    )
    // ...此处省略多行代码
    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
}
复制代码

以上为Vue.extend内部,实现了js的类的继承,并返回子类构造器。async

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {// ...},
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {// ...},
  insert (vnode: MountedComponentVNode) {// ...},
  destroy (vnode: MountedComponentVNode) {// ...}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
function installComponentHooks (data: VNodeData) {
    const hooks = data.hook || (data.hook = {})
    for (let i = 0; i < hooksToMerge.length; i++) {
        const key = hooksToMerge[i]
        const existing = hooks[key]
        const toMerge = componentVNodeHooks[key]
        if (existing !== toMerge && !(existing && existing._merged)) {
            hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
        }
    }
}
复制代码

以上为installComponentHooks内部,给data上挂了一些钩子函数,其中就包括随后要调用的init函数

生成占位 vnode 以后,render 函数执行完毕。进入 _update 函数,一样会调用 patch ,以后调用到 createElm,在createElm内部产生了区别。组件化

function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) {
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
}

复制代码

因为此处的 vnode 为组件 vnode ,所以在进入 if 判断的createComponent()方法内部会返回 true ,createElm到此结束。接下来进入createComponentui

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            //调用init hook
            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
        }
    }
}
复制代码

在遍历 data 过程当中发现有 init 钩子函数,执行 init 。接下来进入init,接下来的逻辑稍微有些复杂,走的稍微有些远。咱们先进入 init 一探究竟,等 init 执行结束以后再回到 createComponent 执行组件的插入逻辑。this

const componentVNodeHooks = {
    init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
        const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
        // 手动调用$mount方法
        child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    },
    // ...
}
复制代码

init内部,执行createComponentInstanceForVnode(vnode, activeInstance),将生成的组件实例赋值给 child 变量,手动调用$mount方法。spa

其中要说明两个参数。参数 vnode 为占位 vnode ,而 activeInstance 是在 initLifecycle 中定义,_update 中赋值,表示当前激活的组件实例(即当前 vm 实例)。

export function createComponentInstanceForVnode (vnode, parent) {
    const options: InternalComponentOptions = {
        _isComponent: true,
        _parentVnode: vnode, //占位vnode
        parent //当前vm实例
    }
    // ...
    return new vnode.componentOptions.Ctor(options)
}
复制代码

代码new vnode.componentOptions.Ctor(options)其实是调用了真实组件的构造器(上文在生成占位 vnode 时把子组件的构造器做为其中一个参数的属性传入),今后开始子组件的实例化。

Vue.prototype._init = function (options?: Object) {
    if (options && options._isComponent) {
        initInternalComponent(vm, options)
    }
    initLifecycle(vm)
    //...
}
复制代码

此处_isComponent为true,进入initInternalComponent方法。

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
    const opts = vm.$options = Object.create(vm.constructor.options)
    const parentVnode = options._parentVnode //占位vnode
    opts.parent = options.parent //当前vm实例(子组件的父级)
    // ...
}
复制代码

initInternalComponent方法中,此时已经进入了子组件的实例化,注意,在函数中定义了子组件实例的 parent 赋值为父组件实例, parentVnode 赋值为以前的占位 vnode 。

export let activeInstance: any = null
export function initLifecycle (vm: Component) {
    const options = vm.$options
    let parent = options.parent
    if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
            parent = parent.$parent
        } 
        //这里是子组件初始化,因此vm是子组件实例
        parent.$children.push(vm)
    }

    vm.$parent = parent
    vm.$root = parent ? parent.$root : vm
}
复制代码

initLifecycle中,定义了父子组件关系。vm.$parent = vm.$options.parentoptions.parent.$children.push(vm)

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevActiveInstance = activeInstance
    // activeInstance在此赋值
    activeInstance = vm 
    vm._vnode = vnode // 渲染vnode
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    // ...
}
复制代码

_update中,activeInstance赋值为当前子组件的实例。并把以前的activeInstance赋值给prevActiveInstance。给vm._vnode赋值为子组件的渲染 vnode 。接下来看子组件的 patch 过程。

function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ... 
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    }
      
    return vnode.elm
}
复制代码

调用vm.__patch__(vm.$el, vnode, hydrating, false)此处的 oldVnode 是 undefined,进入 if 判断。

function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) {
    if (createComponent(vnode,insertedVnodeQueue,parentElm,refElm)) { 
       return 
    }
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
        // 建立DOM
        vnode.elm = nodeOps.createElement(tag, vnode)
        // 建立children
        createChildren(vnode, children, insertedVnodeQueue)
        // 插入(parentElm为空时不作插入操做)
        insert(parentElm, vnode.elm, refElm)
    } else if (isTrue(vnode.isComment)) {
        vnode.elm = nodeOps.createComment(vnode.text)
        insert(parentElm, vnode.elm, refElm)
    } else {
        vnode.elm = nodeOps.createTextNode(vnode.text)
        insert(parentElm, vnode.elm, refElm)
    }
}
复制代码

以上函数中,执行了createChildren,其实就是若是有子节点,则递归地进行以前的流程建立子节点,并插入父节点。等待全部子节点都挂载完毕后,返回到上面的createComponent,此时componentInstance为 true,执行 initComponent,执行组件的插入。到此为止,整个组件的初始化结束。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            //调用init hook
            i(vnode, false /* hydrating */)
        }
        if (isDef(vnode.componentInstance)) {
            initComponent(vnode, insertedVnodeQueue)
            // 组件的插入在这儿
            insert(parentElm, vnode.elm, refElm)
            return true
        }
    }
}
复制代码
function initComponent (vnode, insertedVnodeQueue) {
    // ...
    vnode.elm = vnode.componentInstance.$el
    // ...
}
复制代码

综上发现,嵌套组件的挂载顺序为子组件先挂载,父组件后挂载。

相关文章
相关标签/搜索