Vue2源码解析(组件化)

482 阅读2分钟

Vue2源码解析(组件化)

Vue主要的一个思想就是组件化,正常开发中我们的一个完整Vue项目一般都是又一个一个Vue组件模块拼装组成,我们先看看Vue如何初始化一个组件:

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

var app = new Vue({
  el: '#app',
  // 这里的 h 是 createElement 方法
  render: h => h(App)
})

这里组件渲染调用render方法,将App参数传入createElement函数中:

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

createElement函数最终会调用_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)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // 获取component is属性获取对应的组件
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // 如果不纯在 tag 创建一个注释节点
    return createEmptyVNode()
  }
  // 处理组件slot插槽
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 组件children vnode数组扁平化,如果是模板生成的render 则根据参数选择扁平化方案,用户自建的render,则进行深度递归扁平化数组
  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)) {
      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
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 创建 component vnode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 创建 tag名的 Vnode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // 创建 component vnode
    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()
  }
}

_createElementtag参数就是我们传入的App对象,这个时候就会创建组件Vnode执行vnode = createComponent(tag, data, context, children)函数:

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子类构造函数
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  ....

  // 处理异步组件
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  ....
  // 安装组件钩子函数
  installComponentHooks(data)

  const name = Ctor.options.name || tag
  // 创建组件Vnode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  ....
  return vnode
}

createComponent方法主要做了三件事:

  1. 将组件对象通过Ctor = baseCtor.extend(Ctor)构建成Vue的子类构造函数
  2. 将组件的钩子函数通过installComponentHooks(data)方法将componentVNodeHooks的钩子函数合并到data.hook中。
  3. 创建组件Vnode,并返回Vnode。

创建完组件Vnode之后vm._update函数中会执行vm.__patch__(prevVnode, vnode)方法将Vnode转换为真实的DOM,这个方法定义在src/core/vdom/patch.js:

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {  // 删除旧节点
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {  // 如果旧节点不存在 创建新节点
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      ......
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

在这里因为我们是一个新的组件Vnode节点,所以执行createElm(vnode, insertedVnodeQueue)函数:

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

在这里因为我们是组件Vnode,所以createComponent(vnode, insertedVnodeQueue, parentElm, refElm)会返回true:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      // 执行组件Vnode 钩子函数init
      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
      }
    }
  }

因为是组件Vnode所以满足条件执行init钩子函数,这个钩子函数定义在src/core/vdom/create-component.js:

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    // 如果组件是keepAlive
    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)
  }
},

如果组件是keepAlive就执行componentVNodeHooks.prepatch(mountedNode, mountedNode)方法,如果不是则createComponentInstanceForVnode创建一个Vue实例,然后调用$mount方法挂载子组件:

export function createComponentInstanceForVnode (
  vnode: any,
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

函数中先构建组件的参数,然后执行new vnode.componentOptions.Ctor(options),这里的vnode.componentOptions.Ctor就是之前继承了Vue的子构造函数,参数_isComponenttrue表明是一个组件,parent表明当前的组件实例,就是指的父组件。所以子组件的实例化是在这时机执行的,然后会执行构造函数里的_init方法:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._uid = uid++

    let startTag, endTag
    vm._isVue = true
    if (options && options._isComponent) {
      
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    ...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

组件实例传入的_isComponenttrue,执行initInternalComponent(vm, options)方法:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  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
  }
}

这个函数主要是将我们传入的参数合并到选项$options中。 _init函数最后执行:

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

因为组件实例初始化不传入el,所以组件自己接管的$mount方法,实例化组件Vue之后在钩子函数componentVNodeHooksinit中会执行child.$mount(hydrating ? vnode.elm : undefined, hydrating),这里hydrating为false所以,它最终会调用mountComponent方法,最后执行vm._render()方法:

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  vm.$vnode = _parentVnode
  let vnode
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    // ...
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

这里的_parentVnode就是之前组件Vnode,它会被作为当前组件的父VNode,而render函数生成的渲染Vnodeparent指向_parentVnode,也就是vm.$vnode,他们是父子关系,在生成渲染vnode之后,执行vm._update去渲染对应vnode:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

首先将组件渲染vnode赋值给vm._vnode,这里的const restoreActiveInstance = setActiveInstance(vm)的作用是保存当前上下文的Vue实例:

export let activeInstance: any = null

export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () => {
    activeInstance = prevActiveInstance
  }
}

这里的activeInstance是全局变量,prevActiveInstance保存当前vm实例的父Vue实例,activeInstance保存当前的Vue实例然后返回一个函数用于子组件patch完成后将activeInstance指回当前实例的父Vue实例,因为Vue初始化是深度遍历的过程,在实例化子组件的时候需要知道当前Vue实例,把它作为子组件的父Vue实例,在子组件$mount挂载之前会调用initLifecycle(vm)方法:

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
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  ...
}

这里会将当前vm存储到父实例的$children中,这样就保证了vm实例和它所有子树的父子关系。 然后执行了vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */),会调用开头的createElm渲染成DOM:

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)) {
    // ...

    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)
    }
    
    // ...
  } 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)
  }
}

这里和开始不同的是我们这里传入的vnode是组件渲染vnode,因为我们的渲染vnode根元素是一个普通元素,所以是普通的vnode,而不是之前的组件vnode,所以不会走之前创建组件实例的逻辑,而是走下面普通vnode的逻辑,先判断是否vnode中是否包含tag参数,包含的话就先校验合法性,然后去调用平台DOM操作去创建一个占位符元素:

vnode.elm = vnode.ns
  ? nodeOps.createElementNS(vnode.ns, tag)
  : nodeOps.createElement(tag, vnode)
setScope(vnode)

然后调用createChildren方法创建子元素,然后通过invokeCreateHooks(vnode, insertedVnodeQueue)触发所有的create钩子函数并把vnodepush到insertedVnodeQueue中:

if (isDef(data)) {
  invokeCreateHooks(vnode, insertedVnodeQueue)
}

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

最后调用insert(parentElm, vnode.elm, refElm),因为我们传入的parentElm是空,所以在这不会做插入操作,而是在createComponent函数中完成组件初始化之后执行initComponent函数将组件渲染vnode$el赋值给组件vnodeelm

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // ....
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // ...
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

最后在执行insert(parentElm, vnode.elm, refElm)完成组件DOM的插入。

总结

Vue在根据vnode树渲染整个项目时,如果是普通vnode会直接创建元素插入,但如果是组件vnode会先初始化组件Vue实例并将其parent指向父实例,然后进行组件的patch将生成的真实DOM插入到组件所在位置中,这就是组件初始化渲染的大致过程。