跳至主要內容

Vue 组件化

Mr.LRH大约 42 分钟

Vue 组件化

createComponent : 创建组件 VNode

Vue.prototype._render 进行 Virtual DOM 渲染时,执行 vnode = render.call(vm._renderProxy, vm.$createElement),通过 vm.$createElement 最终调用 createElement (最终调用 _createElement) 创建 Virtual DOM 。

_createElement(context, tag, data, children, normalizationType) 方法中,会对参数 tag 进行判断,如果是一个普通的 html 标签,则会实例化一个普通的 VNode 节点,否则,会通过 createComponent 方法创建一个组件 VNode 。

【render】过程:调用的 _createElement 方法
// src\core\vdom\create-element.ts
export function _createElement(
  context: Component,
  tag?: string | Component | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data as any).__ob__)) {
    __DEV__ &&
      warn(
        `Avoid using observed data object as vnode data: ${JSON.stringify(
          data
        )}\n` + 'Always create fresh vnode data objects in each render!',
        context
      )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(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 (isArray(children) && isFunction(children[0])) {
    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 (
        __DEV__ &&
        isDef(data) &&
        isDef(data.nativeOn) &&
        data.tag !== 'component'
      ) {
        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)))
    ) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(tag, data, children, undefined, undefined, context)
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag as any, data, context, children)
  }
  if (isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
【render】过程:通过的 createComponent 方法创建 VNode
// src\core\vdom\create-component.ts
export function createComponent(
  Ctor: typeof Component | Function | ComponentOptions | void,
  data: VNodeData | undefined,
  context: Component,
  children?: Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor as typeof Component)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (__DEV__) {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  // @ts-expect-error
  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 as typeof Component)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    // @ts-expect-error
    transformModel(Ctor.options, data)
  }

  // extract props
  // @ts-expect-error
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  // @ts-expect-error
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(
      Ctor as typeof Component,
      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

  // @ts-expect-error
  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
  // @ts-expect-error
  const name = getComponentName(Ctor.options) || tag
  const vnode = new VNode(
    // @ts-expect-error
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    // @ts-expect-error
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

构造子类构造函数

在开发过程中,编写 Vue 组件通常会创建一个普通对象, export 是一个对象。示例如下:

// App.vue
import HelloWorld from './components/HelloWorld'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}

createComponent 方法中,执行 const baseCtor = context.$options._base 时,其中:

  • baseCtor : Vue 构造函数。通过 context.$options._base 获取 Vue 构造函数,在执行 initGlobalAPI 方法时,通过 Vue.options._base = Vue 定义。
  • context.$options :Vue 实例的初始化选项。在 Vue 构造函数中,执行 this._init 方法,进而调用 Vue.prototype._init,执行 mergeOptions 进行 options 合并和挂载。
// src\core\vdom\create-component.ts

// createComponent 方法
const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor as typeof Component)
}

createComponent 方法中,执行 Ctor = baseCtor.extend(Ctor as typeof Component) 时, baseCtor 指向 Vue, 则 baseCtor.extend 实际执行 Vue.extend

Vue.extend 的作用就是构造一个 Vue 的子类,使用原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回。实例化 Sub 时,会执行 this.init 进行 Vue 实例初始化逻辑。同时,会 Sub 子类对扩展、初始化等操作:

  • Sub 这个对象本身扩展了一些属性:扩展 options 、 添加全局 API 等。
  • 对配置中的 propscomputed 进行初始化工作
  • 对于 Sub 构造函数进行缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造
Vue.extend
// src\core\global-api\extend.ts
export function initExtend(Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: any): typeof Component {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name =
      getComponentName(extendOptions) || getComponentName(Super.options)
    if (__DEV__ && name) {
      validateComponentName(name)
    }

    const Sub = function VueComponent(this: any, options: any) {
      this._init(options)
    } as unknown as typeof Component
    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
  }
}

function initProps(Comp: typeof Component) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed(Comp: typeof Component) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

安装组件钩子函数

createComponent 方法中,执行 installComponentHooks 安装组件钩子函数。

installComponentHooks 的过程就是将 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数。在合并过程中,如果某个时机的钩子已存在 data.hook 中,那么通过执行 mergeHook 函数合并,在最终执行的时候,依次执行这两个钩子函数。

installComponentHooks
// src\core\vdom\create-component.ts

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ))
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = (vnode.componentInstance = oldVnode.componentInstance)
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  destroy(vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

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]
    // @ts-expect-error
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

function mergeHook(f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}

实例化 VNode

createComponent 中,安装组件钩子函数后,会通过 new VNode 实例化一个 vnode 返回。与普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的。

// return a placeholder vnode
// @ts-expect-error
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
  // @ts-expect-error
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  data,
  undefined,
  undefined,
  undefined,
  context,
  // @ts-expect-error
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
)

return vnode

patch : 将组件 VNode 渲染真实 DOM

_render 过程中,通过 vm.$createElement 调用 createElement ,使用 createComponent 创建了组件 VNode 后,会调用 vm._update 执行 vm.__patch__ 将 VNode 转换成真实 DOM 节点。

patch 过程中,会调用 createElm 创建元素节点,其中,会判断 patch 的辅助函数 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值。

patch 的辅助函数 createComponent 函数中,对 vnode.data 进行了判断。如果 vnode 是一个组件 VNode,条件满足,并得到 iinit 钩子函数,对组件进行初始化。

【patch】 过程: 调用的 patch 辅助函数 createComponent
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef((i = i.hook)) && isDef((i = i.init))) {
      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
    }
  }
}

在执行 _render 过程中,通过 _createElement,调用 createComponent 方法,执行 installComponentHooks 安装组件钩子函数,包含 init 钩子函数。在不考虑 keep-alive 的情况下,通过 createComponentInstanceForVnode 创建一个 Vue 的实例,然后调用 $mount 方法挂载子组件。

createComponentInstanceForVnode 函数构造一个内部组件的参数,然后执行 new vnode.componentOptions.Ctor(options) 进行子组件实例化,并执行实例的 this._init 方法。

  • vnode.componentOptions.Ctor 对应的是子组件的构造函数,实际上是继承于 Vue 的一个构造器 Sub,相当于 new Sub(options)
  • options 参数
    • options._isComponent : 为 true 表示为一个组件
    • options.parent : 表示当前激活的组件实例
【_render】 过程:调用 installComponentHooks 安装组件的 init 钩子函数
// src\core\vdom\create-component.ts

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ))
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  // ... 省略其他钩子函数定义 : prepatch、insert、destroy
}

export function createComponentInstanceForVnode(
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent?: any
): 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)
}

子组件实例化的过程中,调用 this._init 执行相关逻辑会有些不同:

  • 合并 options : 在 Vue.prototype._init 方法中,判断 options._isComponenttrue 会执行 initInternalComponent(vm, options) 。其中,

    • opts.parent = options.parent
    • opts._parentVnode = parentVnode

    是通过 createComponentInstanceForVnode(vnode, parent) 函数传入的参数,合并到内部的选项 $options 中。

  • 子组件接管 $mount : 子组件初始化的时候不传 el 参数,则,回到组件 init 钩子函数,完成实例化的 _init 后,执行 child.$mount(hydrating ? vnode.elm : undefined, hydrating)

    • hydrating : 为 true 一般表示服务端渲染的情况
    • child.$mount(hydrating ? vnode.elm : undefined, hydrating) 对于客户端渲染,相当于 child.$mount(undefined, false) ,最终会调用 mountComponent 方法,进而执行 vm._render() 方法
【子组件实例化过程】:调用的 Vue.prototype._init 进行初始化
// src\core\instance\init.ts

export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to mark this as a Vue instance without having to do instanceof
    // check
    vm._isVue = true
    // avoid instances from being observed
    vm.__v_skip = true
    // effect scope
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    // merge options
    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 as any)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (__DEV__ && 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)
    }
  }
}




 




















 
 
 
 
 
 
 
 
 
 
 
 
























 
 
 


子组件进行 _render 渲染 Virtual DOM 过程的时候,其中

  • _parentVnode : 当前组件的父 VNode
  • vm.$vnode : 当前 vnode 的 parent
  • vnode : 通过 render 函数生成的当前组件的渲染 vnode
【_render】 过程:调用 Vue.prototype._render 渲染 Virtual DOM
// src\core\instance\render.ts

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

  if (_parentVnode && vm._isMounted) {
    vm.$scopedSlots = normalizeScopedSlots(
      vm.$parent!,
      _parentVnode.data!.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
    if (vm._slotsProxy) {
      syncSetupSlots(vm._slotsProxy, vm.$scopedSlots)
    }
  }

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode!
  // render self
  let vnode
  try {
    // There's no need to maintain a stack because all render fns are called
    // separately from one another. Nested component's render fns are called
    // when parent component is patched.
    setCurrentInstance(vm)
    currentRenderingInstance = vm
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e: any) {
    handleError(e, vm, `render`)
    // return error render result,
    // or previous vnode to prevent render error causing blank component
    /* istanbul ignore else */
    if (__DEV__ && vm.$options.renderError) {
      try {
        vnode = vm.$options.renderError.call(
          vm._renderProxy,
          vm.$createElement,
          e
        )
      } catch (e: any) {
        handleError(e, vm, `renderError`)
        vnode = vm._vnode
      }
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
    setCurrentInstance()
  }
  // if the returned array contains only a single node, allow it
  if (isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (__DEV__ && isArray(vnode)) {
      warn(
        'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
        vm
      )
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}




















 

 






 







































 
 

子组件通过 vm._render 生成 VNode 后,执行 vm._update 调用 __patch__ 进行 VNode 渲染。其中:

  • vm._vnode = vnode : vnode 是通过 vm._render() 返回的组件渲染 VNode
  • vm._vnodevm.$vnode 的关系就是一种父子关系,通过代码标识为 vm._vnode.parent === vm.$vnode
  • activeInstance : 作用是保存当前上下文的 Vue 实例,是 lifecycle 模块的全局变量。
    • 在调用 createComponentInstanceForVnode(vnode, parent) 方法创建组件 Vue 实例时,会从 lifecycle 模块中获取 activeInstance,并作为 parent 参数传入。实际上,JavaScript 是一个单线程,Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,需要知道当前上下文的 Vue 实例,并把它作为子组件的父 Vue 实例。
    • 子组件实例化的过程中,会调用 this._init 进行初始化。
      • 执行 initInternalComponent(vm, options) 。 合并 options,将 parent 存储在 vm.$options 中。
      • 执行 initLifecycle(vm) 。使用 vm.$parent 保留当前 vm 的父实例,并通过 parent.$children.push(vm) 把当前的 vm 存储到父实例的 $children 中。
    • 在执行 vm._update 过程中,会调用 setActiveInstance 将当前的 vm 赋值给 activeInstance,同时,通过 const prevActiveInstance = activeInstance 使用 prevActiveInstance 保留上一次的 activeInstance 。实际上,prevActiveInstance 和当前的 vm 是一个父子关系,当一个 vm 实例完成它的所有子数的 patch 或者 update 过程后,activeInstance 会回到它的父实例,这样就能保证在 createComponentInstanceForVnode(vnode, parent) 整个深度遍历过程中,在实例子组件的时候能传入当前子组件的父 Vue 实例,并在 this._init 过程中,通过 vm.$parent 将这个父子关系保留。
【patch】过程:通过 vm._update 进行 VNode 渲染
// src\core\instance\lifecycle.ts

export let activeInstance: any = null

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

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
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}


 



 










 









 








 





__patch__ 过程中,实际调用辅助函数 createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) 渲染 VNode 为真实 DOM 。

patch 函数中,初始化的时候,通过 createElm(vnode, insertedVnodeQueue) 调用。在 createElm 函数中,vnode : 组件渲染的 vnode ,即: vm._vnode

  • 如果组件的根节点是普通元素,则 vm._vnode 为普通 vnode 。在 createElmcreateComponent(vnode, insertedVnodeQueue, parentElm, refElm) 返回为 false 。先创建一个父节点占位符,然后再遍历所有的子 VNode 递归调用 createElm
  • 在遍历过程中,如果遇到子 VNode 是一个组件 VNode,则调用 createComponent 创建组件 VNode 重新执行相关逻辑。这样通过一个递归的方式就可以完整地构建了整个组件树。

完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么 DOM 的插入顺序是 先子后父

【patch】过程:调用的 patch 函数
export function createPatchFunction(backend) {
  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue: any[] = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (__DEV__) {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                  'server-rendered content. This is likely caused by incorrect ' +
                  'HTML markup, for example nesting block-level elements inside ' +
                  '<p>, or missing <tbody>. Bailing hydration and performing ' +
                  'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

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













 



























































































【patch】过程:调用的 patch 辅助函数 createElm
function createElm(
  vnode,
  insertedVnodeQueue,
  parentElm?: any,
  refElm?: any,
  nested?: any,
  ownerArray?: any,
  index?: any
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    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 (__DEV__) {
      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)

    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    insert(parentElm, vnode.elm, refElm)

    if (__DEV__ && data && data.pre) {
      creatingElmInVPre--
    }
  } 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)
  }
}

merge options : 合并配置

在进行 Vue 实例化的过程中,会执行 this._init (即:Vue.prototype._init)。该过程中会进行 merge options 合并配置。 new Vue 的过程通常有 2 种场景:

  • 外部主动调用 new Vue(options) 实例化 Vue 对象
  • 组件内部调用 new Vue(options) 实例化子组件
【Vue 实例初始化】过程:调用 Vue.prototype._init 合并配置
// src\core\instance\init.ts

export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to mark this as a Vue instance without having to do instanceof
    // check
    vm._isVue = true
    // avoid instances from being observed
    vm.__v_skip = true
    // effect scope
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    // merge options
    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 as any)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (__DEV__ && 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)
    }
  }
}

























 
 
 
 
 
 
 
 
 
 
 
 





























外部主动调用合并配置

外部主动调用 new Vue(options) 的时候,会执行 this._init (即:Vue.prototype._init)中如下逻辑合并 options 。实际上是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 合并。

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

resolveConstructorOptions(vm.constructor) 返回为 vm.constructor.options,相当于 Vue.options

Vue.options 是在初始化 Vue 过程中,执行 initGlobalAPI(Vue) 的时候定义,其中:

  • 通过 Vue.options = Object.create(null) 创建空对象

  • 遍历 ASSET_TYPESVue.options 上扩展 componentdirectivefilter,相当于

    Vue.options.components = {}
    Vue.options.directives = {}
    Vue.options.filters = {}
    
  • 通过 Vue.options._base = Vue 获取 Vue 的构造函数

  • 通过 extend(Vue.options.components, builtInComponents) 将一些内置组件扩展到 Vue.options.components 上。目前 Vue 的内置组件包括 <keep-alive><transition><transition-group> 组件,这也就是在其他组件中使用 <keep-alive> 不需要注册的原因。

initGlobalAPI 函数
// src\shared\constants.ts
export const ASSET_TYPES = ['component', 'directive', 'filter'] as const

// src\core\global-api\index.ts
export function initGlobalAPI(Vue: GlobalAPI) {
  // config
  const configDef: Record<string, any> = {}
  configDef.get = () => config
  if (__DEV__) {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

 



































 
 
 
 



 

 






mergeOptions 函数主要功能是将 parentchild 这两个对象根据合并策略,合并成一个新对象并返回。其中,核心逻辑为:先递归将 extendmixins 合并到 parent 上,然后遍历 parent,调用 mergeField ,然后再遍历 child,如果 key 不在 parent 的自身属性上,则调用 mergeField 函数。

对于 mergeField 函数,不同的 Key 有着不同的合并策略。以生命周期为例:

  • 如果不存在 childVal ,就返回 parentVal
  • 否则,再判断是否存在 parentVal ,如果存在就把 childVal 添加到 parentVal 后返回新数组
  • 否则,返回 childVal 的数组

所以,一旦 parentchild 都定义了相同的钩子函数,会把 2 个钩子函数合并成一个数组

mergeOptions 函数
// src\core\util\options.ts
/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions(
  parent: Record<string, any>,
  child: Record<string, any>,
  vm?: Component | null
): ComponentOptions {
  if (__DEV__) {
    checkComponents(child)
  }

  if (isFunction(child)) {
    // @ts-expect-error
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options: ComponentOptions = {} as any
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField(key: any) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeLifecycleHook
})

/**
 * Hooks and props are merged as arrays.
 */
export function mergeLifecycleHook(
  parentVal: Array<Function> | null,
  childVal: Function | Array<Function> | null
): Array<Function> | null {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : isArray(childVal)
      ? childVal
      : [childVal]
    : parentVal
  return res ? dedupeHooks(res) : res
}

// src\shared\constants.ts
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch',
  'renderTracked',
  'renderTriggered'
] as const

组件内部调用合并配置

组件的构造函数是通过 Vue.extend(extendOptions) 继承 Vue 的。其中,extendOptions 为定义的组件对象,会和 Vue.options 合并到 Sub.options 中。

Vue.extend
export function initExtend(Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: any): typeof Component {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name =
      getComponentName(extendOptions) || getComponentName(Super.options)
    if (__DEV__ && name) {
      validateComponentName(name)
    }

    const Sub = function VueComponent(this: any, options: any) {
      this._init(options)
    } as unknown as typeof Component
    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
  }
}

































 












 
 
 
 























子组件通过 createComponentInstanceForVnode 函数进行组件实例化,并执行实例的 this._init 方法,判断 options._isComponenttrue 会执行 initInternalComponent(vm, options) 进行 options 合并。其中:

  • 执行 const opts = vm.$options = Object.create(vm.constructor.options)
    • vm.constructor 是子组件的构造函数 Sub,相当于 vm.$options = Object.create(Sub.options)
  • 然后,把实例化子组件传入的子组件父 VNode 实例 parentVnode 、 子组件的父 Vue 实例 parent 保存到 vm.$options 中。
  • 同时,还保留了 parentVnode 配置中的 propsData 等其他属性
initInternalComponent
// src\core\instance\init.ts
export function initInternalComponent(
  vm: Component,
  options: InternalComponentOptions
) {
  const opts = (vm.$options = Object.create((vm.constructor as any).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
  }
}

生命周期

Vue 实例在被创建之前会经过一系列的初始化过程。例如,需要设置数据监听、编译模板、挂载实例到 DOM 、在数据变化时更新 DOM 等。在执行的过程中,会运行生命周期钩子函数,供用户在一些特定的场景中执行相关代码。

执行生命周期函数都会调用 callHook 函数,在 lifecycle 模块中定义。

callHook 函数中,会根据传入的字符串 hook ,获取 vm.$options[hook] 对应的回调函数数组,然后遍历执行,执行的时候会将 vm 作为函数执行的上下文。

lifecycle 模块 callHook 函数
// src\core\instance\lifecycle.ts
export function callHook(
  vm: Component,
  hook: string,
  args?: any[],
  setContext = true
) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const prev = currentInstance
  setContext && setCurrentInstance(vm)
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, args || null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  setContext && setCurrentInstance(prev)
  popTarget()
}

beforeCreate & created

beforeCreatecreated 函数在进行 Vue 实例化的过程中,在调用 this._init 方法(即:Vue.prototype._init 方法)进行初始化的时候执行的。

beforeCreatecreated 生命周期钩子函数分别在 initState 前后执行。initState 函数会初始化 propsdatamethodswatchcomputed 等属性。beforeCreate 的生命钩子函数中,不能获取到 propsdata 中定义的值,也不能调用 methods 中定义的函数。

beforeCreatecreated 在执行的时候,并没有渲染 DOM ,也不能访问 DOM 。

一般来说,如果组件在加载的时候,需要与后端进行交互,将交互逻辑放在这两个钩子函数之后即可。

Vue.prototype._init(options) : 初始化 Vue
// src\core\instance\init.ts
export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to mark this as a Vue instance without having to do instanceof
    // check
    vm._isVue = true
    // avoid instances from being observed
    vm.__v_skip = true
    // effect scope
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    // merge options
    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 as any)
    } else {
      // 合并配置
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件中心
    initRender(vm) // 初始化渲染
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // 初始化 inject - resolve injections before data/props
    initState(vm) // 初始化 props 、 methods 、 data 、 computed 、 watch
    initProvide(vm) // 初始化 provide - resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // 如果有 el 属性, 则调用 vm.$mount 方法挂载 vm
    // 目的是将模板渲染程最终的 DOM
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}













































 
 
 
 
 
 
 
 















beforeMount & mounted

beforeMount 函数发生在 Vue 实例挂载 DOM 之前,在执行公共 $mount 方法中,最终调用 mountComponent 函数中执行。

在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mounted 钩子函数。

在执行 mounted 钩子函数的时候,会进行判断。如果 vm.$vnode == null ,则表示不是组件的初始化过程,是通过外部 new Vue 的初始化过程。

对于组件 VNode patch 到 DOM 的过程中,会执行 invokeInsertHook 函数,把 insertedVnodeQueue 里保存的钩子函数依次执行一遍。

invokeInsertHook 函数会执行 insert 钩子函数,每个子组件都是在这个钩子函数中执行 mounted 钩子函数。insertedVnodeQueue 的添加顺序是先子后父,对于同步渲染的子组件而言,mounted 钩子函数的执行顺序也是先子后父

【$mount 挂载】过程:调用的 mountComponent 方法
// src\core\instance\lifecycle.ts
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    // @ts-expect-error invalid type
    vm.$options.render = createEmptyVNode
    if (__DEV__) {
      /* istanbul ignore if */
      if (
        (vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el ||
        el
      ) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
            'compiler is not available. Either pre-compile the templates into ' +
            'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (__DEV__ && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  const watcherOptions: WatcherOptions = {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }

  if (__DEV__) {
    watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
    watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  hydrating = false

  // flush buffer for flush: "pre" watchers queued in setup()
  const preWatchers = vm._preWatchers
  if (preWatchers) {
    for (let i = 0; i < preWatchers.length; i++) {
      preWatchers[i].run()
    }
  }

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}































 





























































 
 
 
 


【patch】过程:调用 invokeInsertHook 函数
function invokeInsertHook(vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  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])
    }
  }
}

beforeUpdate & updated

beforeUpdateupdated 的钩子函数执行时机都应该是在数据更新的时候。

beforeUpdate 的执行时机是在调用 mountComponent 函数中,渲染 Watcherbefore 函数中调用。在组件 mounted 之后,才会调用这个钩子函数。

update 的执行时机是在 flushSchedulerQueue 函数调用的时候。其中,updatedQueue 是更新了的 watcher 数组,在调用 callUpdatedHooks 函数中,对 updatedQueue 中的数组进行遍历,只有满足当前 watchervm._watcher 以及组件已完成 mounted,才会调用 updated 钩子函数。

  • 在组件 mount 的过程中,会在 mountComponent 函数执行时,会实例化一个渲染的 Watcher 去监听 vm 上的数据变化重新渲染。
  • 在实例化 Watcher 的过程中,在它的构造函数里会判断 isRenderWatcher,把当前 watcher 的实例赋值给 vm._watcher,同时,把当前 wathcer 实例 push 到 vm._watchers 中。
  • vm._watcher 是专门用来监听 vm 上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher,因此在 callUpdatedHooks 函数中,只有 vm._watcher 的回调执行完毕后,才会执行 updated 钩子函数。
flushSchedulerQueue 函数
// src\core\observer\scheduler.ts

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort(sortCompareFn)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (__DEV__ && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' +
            (watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

function callUpdatedHooks(queue: Watcher[]) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm && vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}






















































 













 
 
 


beforeDestroy & destroyed

beforeDestroydestroyed 钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy 方法。

beforeDestroy 钩子函数的执行时机是在 $destroy 函数执行最开始的地方,接着执行了一系列的销毁动作:从 parent$children 中删掉自身、删除 watcher、删除当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy 钩子函函数

$destroy 的执行过程中,它又会执行 vm.__patch__(vm._vnode, null) 触发它子组件的销毁钩子函数,一层层的递归调用,所以 destroy 钩子函数执行顺序是先子后父,和 mounted 过程一样。

Vue.prototype.$destroy
Vue.prototype.$destroy = function () {
  const vm: Component = this
  if (vm._isBeingDestroyed) {
    return
  }
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  // remove self from parent
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  // teardown scope. this includes both the render watcher and other
  // watchers created
  vm._scope.stop()
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // invoke destroy hooks on current rendered tree
  vm.__patch__(vm._vnode, null)
  // fire destroyed hook
  callHook(vm, 'destroyed')
  // turn off all instance listeners.
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

组件注册

全局注册

注册全局属性,可通过 Vue.component(tagName, options) 进行注册。

Vue.component 是在初始化 Vue 过程中,调用 initGlobalAPI(Vue) 函数,执行 initAssetRegisters 的时候定义。在 initAssetRegisters 函数中:

  • 如果 typecomponentdefinition 是一个对象的话,通过 this.opitons._base.extend,相当于 Vue.extend 把这个对象转换成一个继承于 Vue 的构造函数。
  • 最后通过 this.options[type + 's'][id] = definition 把它挂载到 Vue.options.components 上。
初始化 Vue 过程中,调用的 initGlobalAPI 函数
// src\core\global-api\index.ts
export function initGlobalAPI(Vue: GlobalAPI) {
  // config
  const configDef: Record<string, any> = {}
  configDef.get = () => config
  if (__DEV__) {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}
















































 

在调用 initGlobalAPI 函数中,执行的 initAssetRegisters 函数
// src\shared\constants.ts
export const ASSET_TYPES = ['component', 'directive', 'filter'] as const

// src\core\global-api\assets.ts
export function initAssetRegisters(Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    // @ts-expect-error function is not exact same type
    Vue[type] = function (
      id: string,
      definition?: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (__DEV__ && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          // @ts-expect-error
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && isFunction(definition)) {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

每个组件的创建都是通过 Vue.extend 基础而来的。在 Vue.extend 方法中,会执行 Sub.options = mergeOptions(Super.options, extendOptions),把 Vue.options 合并到 Super.options (即:组件的 options)。然后,在组件的实例化阶段,执行 merge options 逻辑,把 Sub.options.components 合并到 vm.$options.components 上。

在创建 vnode 过程中,会通过 createElement 方法执行 _createElement 方法。在 _createElement 方法中,会执行判断逻辑 isDef(Ctor = resolveAsset(context.$options, 'components', tag))

resolveAsset 方法中 :

  • 先通过 const assets = options[type] 获取 assets

  • 再获取 assets[id] ,获取规则如下:

    • 直接使用 id ,获取 assets[id]
    • 如果不存在,则将 id 变成驼峰的形式,获取 assets[id]
    • 如果仍然不存在,则在驼峰的基础上,将首字母再变成大写的形式,虎丘 assets[id]
    • 如果仍然不存在,则报错

    该获取规则,标识在使用 Vue.component(id, definition) 全局注册组件时,id 可以是连字符、驼峰或首字母大写的形式

通过调用 resolveAsset(context.$options, 'components', tag) (即获取 vm.$options.components[tag])获取组件的构造函数,并作为 createComponent 的钩子的参数。

【创建 vnode】过程:通过 createElement 方法执行的 _createElement 方法
export function _createElement(
  context: Component,
  tag?: string | Component | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data as any).__ob__)) {
    __DEV__ &&
      warn(
        `Avoid using observed data object as vnode data: ${JSON.stringify(
          data
        )}\n` + 'Always create fresh vnode data objects in each render!',
        context
      )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(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 (isArray(children) && isFunction(children[0])) {
    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 (
        __DEV__ &&
        isDef(data) &&
        isDef(data.nativeOn) &&
        data.tag !== 'component'
      ) {
        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)))
    ) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(tag, data, children, undefined, undefined, context)
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag as any, data, context, children)
  }
  if (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】过程:在 _createElement 方法中,调用的 resolveAsset 方法
// src\core\util\options.ts
/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 */
export function resolveAsset(
  options: Record<string, any>,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  // check local registration variations first
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // fallback to prototype chain
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  if (__DEV__ && warnMissing && !res) {
    warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id)
  }
  return res
}

局部注册

每个组件的创建都是通过 Vue.extend 基础而来的。在 Vue.extend 方法中,会执行 Sub.options = mergeOptions(Super.options, extendOptions),把 Vue.options 合并到 Super.options (即:组件的 options)。然后,在组件的实例化阶段,执行 merge options 逻辑,把 Sub.options.components 合并到 vm.$options.components 上。这样可以在执行 resolveAsset 函数的时候,获取到这个组件的构造函数,并作为 createComponent 的参数。

局部注册和全局注册不同的,全局注册是扩展到 Vue.options 下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components 扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。

异步组件

在大型应用中,为了减少首屏代码体积,会把一些非首屏的组件设计成异步组件,按需加载。主要分为以下三种异步组件。

// 普通函数异步组件
Vue.component('async-example', function (resolve, reject) {
   // 这个特殊的 require 语法告诉 webpack,自动将编译后的代码分割成不同的块
   // 这些块将通过 Ajax 请求自动下载。
   require(['./my-async-component'], resolve)
})

// Promise 异步组件
Vue.component(
  'async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

// 高级异步组件
const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

render 过程中,通过 createComponent 创建组件 VNode 。

在异步组件场景下,createComponent(Ctor, data, context, children, tag) 中,Ctor 传入的是一个函数,并不会执行 Vue.extend 逻辑,因此它的 cidundefined,进入了异步组件创建的处理逻辑,执行 Ctor = resolveAsyncComponent(asyncFactory, baseCtor)

【render】过程:通过 createComponent 创建 VNode
export function createComponent(
  Ctor: typeof Component | Function | ComponentOptions | void,
  data: VNodeData | undefined,
  context: Component,
  children?: Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor as typeof Component)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (__DEV__) {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  // @ts-expect-error
  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 as typeof Component)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    // @ts-expect-error
    transformModel(Ctor.options, data)
  }

  // extract props
  // @ts-expect-error
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  // @ts-expect-error
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(
      Ctor as typeof Component,
      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

  // @ts-expect-error
  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
  // @ts-expect-error
  const name = getComponentName(Ctor.options) || tag
  const vnode = new VNode(
    // @ts-expect-error
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    // @ts-expect-error
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}














 
 
 













 
 
 
 
 
 
 
 
 
 






































































创建异步组件: resolveAsyncComponent 函数
export function resolveAsyncComponent(
  factory: { (...args: any[]): any; [keye: string]: any },
  baseCtor: typeof Component
): typeof Component | void {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner)
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (owner && !isDef(factory.owners)) {
    const owners = (factory.owners = [owner])
    let sync = true
    let timerLoading: number | null = null
    let timerTimeout: number | null = null

    owner.$on('hook:destroyed', () => remove(owners, owner))

    const forceRender = (renderCompleted: boolean) => {
      for (let i = 0, l = owners.length; i < l; i++) {
        owners[i].$forceUpdate()
      }

      if (renderCompleted) {
        owners.length = 0
        if (timerLoading !== null) {
          clearTimeout(timerLoading)
          timerLoading = null
        }
        if (timerTimeout !== null) {
          clearTimeout(timerTimeout)
          timerTimeout = null
        }
      }
    }

    const resolve = once((res: Object | Component) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })

    const reject = once(reason => {
      __DEV__ &&
        warn(
          `Failed to resolve async component: ${String(factory)}` +
            (reason ? `\nReason: ${reason}` : '')
        )
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })

    const res = factory(resolve, reject)

    if (isObject(res)) {
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isPromise(res.component)) {
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
            // @ts-expect-error NodeJS timeout type
            timerLoading = setTimeout(() => {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)
          }
        }

        if (isDef(res.timeout)) {
          // @ts-expect-error NodeJS timeout type
          timerTimeout = setTimeout(() => {
            timerTimeout = null
            if (isUndef(factory.resolved)) {
              reject(__DEV__ ? `timeout (${res.timeout}ms)` : null)
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    // return in case resolved synchronously
    return factory.loading ? factory.loadingComp : factory.resolved
  }
}

普通函数异步组件

// 普通函数异步组件
Vue.component('async-example', function (resolve, reject) {
   // 这个特殊的 require 语法告诉 webpack,自动将编译后的代码分割成不同的块
   // 这些块将通过 Ajax 请求自动下载。
   require(['./my-async-component'], resolve)
})

针对普通函数的情况,在 resolveAsyncComponent 创建异步组件函数中:

  • 对当前渲染的实例(currentRenderingInstance,从 render 模块中获取)进行判断,是考虑到多个地方同时初始化一个异步组件,实际应该只加载一次。

  • 进入实际加载逻辑,定义了 forceRenderresolvereject 函数,其中 resolvereject 函数使用 once 函数进行了包装,确保只执行一次。

    • forceRender 函数

      const forceRender = (renderCompleted: boolean) => {
        for (let i = 0, l = owners.length; i < l; i++) {
          owners[i].$forceUpdate()
        }
      
        if (renderCompleted) {
          owners.length = 0
          if (timerLoading !== null) {
            clearTimeout(timerLoading)
            timerLoading = null
          }
          if (timerTimeout !== null) {
            clearTimeout(timerTimeout)
            timerTimeout = null
          }
        }
      }
      
    • resolve 函数

      const resolve = once((res: Object | Component) => {
        // cache resolved
        factory.resolved = ensureCtor(res, baseCtor)
        // invoke callbacks only if this is not a synchronous resolve
        // (async resolves are shimmed as synchronous during SSR)
        if (!sync) {
          forceRender(true)
        } else {
          owners.length = 0
        }
      })
      
    • reject 函数

      const reject = once(reason => {
        __DEV__ &&
          warn(
            `Failed to resolve async component: ${String(factory)}` +
              (reason ? `\nReason: ${reason}` : '')
          )
        if (isDef(factory.errorComp)) {
          factory.error = true
          forceRender(true)
        }
      })
      
    • once 函数接收一个函数参数,并返回一个新的函数。利用闭包和标志位保证通过 once 包装的函数只执行一次。

      /**
       * Ensure a function is called only once.
       */
      export function once<T extends (...args: any[]) => any>(fn: T): T {
        let called = false
        return function () {
          if (!called) {
            called = true
            fn.apply(this, arguments as any)
          }
        } as any
      }
      
  • 通过执行 const res = factory(resolve, reject) 逻辑,执行定义普通函数异步组件的工厂函数,同时将 resolvereject 函数作为参数传入。异步组件的工厂函数通常会发送请求加载异步组件的 JavaScript 文件,获取到定义的对象 res 后,执行 resolve(res) 逻辑。

  • 执行定义的 resolve(res) 函数,其中:

    • 执行 factory.resolved = ensureCtor(res, baseCtor) 逻辑。ensureCtor 函数保证能找到异步组件 JavaScript 定义的组件对象,并且如果是一个普通对象,则调用 Vue.extend 将其转换成一个组件的构造函数。

      function ensureCtor(comp: any, base) {
        if (comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
          comp = comp.default
        }
        return isObject(comp) ? base.extend(comp) : comp
      }
      
    • sync 标识进行判断,在该场景下 syncfalse ,则会执行 forceRender 函数

  • 执行 forceRender 函数,遍历 factory.contexts 获取每一个调用异步组件的实例 vm ,执行 vm.$forceUpdate() 方法,其定义在 lifecycle 模块中。

    $forceUpdate() 方法中,调用渲染 watcherupdate 方法,让渲染 watcher 对应的回调函数执行,触发组件的重新渲染。因为 Vue 通常是数据驱动视图重新渲染,在整个异步组件加载过程中是没有数据变化的,所以通过执行 $forceUpdate() 强制组件重新渲染一次。

    // src\core\instance\lifecycle.ts
    Vue.prototype.$forceUpdate = function () {
      const vm: Component = this
      if (vm._watcher) {
        vm._watcher.update()
      }
    }
    

Promise 异步组件

// Promise 异步组件
Vue.component(
  'async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

webpack 2+ 支持了异步加载的语法糖:() => import('./my-async-component')

当在 resolveAsyncComponent 创建异步组件函数中,执行 const res = factory(resolve, reject) ,得到的返回值就是 import('./my-async-component') 的返回值,是一个 Promise 对象。进入后续判断逻辑,满足 isPromise(res) ,执行如下逻辑:

if (isUndef(factory.resolved)) {
  res.then(resolve, reject)
}
  • 组件异步加载成功,则执行在 resolveAsyncComponent 函数中定义的 resolve(res) 函数
  • 组件异步加载失败,则执行在 resolveAsyncComponent 函数中定义的 reject(res) 函数

高级异步组件

高级异步组件支持设置 loading 组件与 error 组件,用于处理组件异步加载过程中与加载失败的场景。

// 高级异步组件
const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

当在 resolveAsyncComponent 创建异步组件函数中,执行 const res = factory(resolve, reject) ,得到的返回值为定义的组件对象。进入后续判断逻辑,满足 isPromise(res.component),执行 res.component.then(resolve, reject)

  • 组件异步加载成功,则执行在 resolveAsyncComponent 函数中定义的 resolve(res) 函数
  • 组件异步加载失败,则执行在 resolveAsyncComponent 函数中定义的 reject(res) 函数

因为异步组件加载是一个异步过程,同时,又会同步执行在 resolveAsyncComponent 中的如下逻辑:

  • 判断 res.error 是否定义了 error 组件。如果有的话,则赋值给 factory.errorComp
  • 判断 res.loading 是否定义了 loading 组件,如果有的话,则赋值给 factory.loadingComp
  • 如果设置了 res.delay 且为 0,则设置 factory.loading = true,否则,延时 delay 的时间执行
  • 最后,判断 res.timeout,如果配置了该项,则在 res.timout 时间后,如果组件没有成功加载,执行 reject

resolveAsyncComponent 函数的最后,设置 sync = false ,同时执行 factory.loading ? factory.loadingComp : factory.resolved

  • 如果 delay 配置为 0,则这次直接渲染 loading 组件
  • 否则,则延时 delay 执行 forceRender,会再一次执行到 resolveAsyncComponent

此时,会存在几种情况,按照逻辑的执行顺序,对不同情况进行判断:

  • 异步组件加载失败,会执行 reject 函数。

    const reject = once(reason => {
      __DEV__ &&
        warn(
          `Failed to resolve async component: ${String(factory)}` +
            (reason ? `\nReason: ${reason}` : '')
        )
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })
    

    此时,会把 factory.error 设置为 true ,同时执行 forceRender() 再次执行到 resolveAsyncComponent ,会在该方法中执行到以下逻辑,返回 factory.errorComp,直接渲染 error 组件

    if (isTrue(factory.error) && isDef(factory.errorComp)) {
      return factory.errorComp
    }
    
  • 异步组件加载成功,会执行 resolve 函数

    const resolve = once((res: Object | Component) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })
    

    此时,会把加载结果缓存到 factory.resolved 中,这个时候因为 sync 已经为 false,则执行 forceRender() 再次执行到 resolveAsyncComponen,会在该方法中执行到以下逻辑,返回 factory.resolved,直接渲染成功加载的组件

    if (isDef(factory.resolved)) {
      return factory.resolved
    }
    
  • 异步组件加载中,未成功返回,会执行到以下逻辑,返回 factory.loadingComp ,渲染 loading 组件

    if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
      return factory.loadingComp
    }
    
  • 异步组件加载超时,会进入 reject 逻辑,之后逻辑和加载失败一样,渲染 error 组件。

异步组件 patch

createComponent 创建组件 VNode 方法中,通过 resolveAsyncComponen 函数完成异步组件创建后,会对返回值进行逻辑处理。

【render】过程:通过的 createComponent 方法创建 VNode
export function createComponent(
  Ctor: typeof Component | Function | ComponentOptions | void,
  data: VNodeData | undefined,
  context: Component,
  children?: Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor as typeof Component)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (__DEV__) {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  // @ts-expect-error
  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 as typeof Component)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    // @ts-expect-error
    transformModel(Ctor.options, data)
  }

  // extract props
  // @ts-expect-error
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  // @ts-expect-error
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(
      Ctor as typeof Component,
      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

  // @ts-expect-error
  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
  // @ts-expect-error
  const name = getComponentName(Ctor.options) || tag
  const vnode = new VNode(
    // @ts-expect-error
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    // @ts-expect-error
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}






























 
 
 
 
 
 
 
 
 
 






































































如果是第一次执行 resolveAsyncComponent,除非使用高级异步组件 0 delay 去创建了一个 loading 组件,否则返回是 undefiend,接着通过 createAsyncPlaceholder 创建一个注释节点作为占位符,同时把 asyncFactoryasyncMeta 赋值给当前 vnode

createAsyncPlaceholder 方法
export function createAsyncPlaceholder(
  factory: Function,
  data: VNodeData | undefined,
  context: Component,
  children: Array<VNode> | undefined,
  tag?: string
): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}

当执行 forceRender 的时候,会触发组件的重新渲染,会再一次执行 resolveAsyncComponent。此时,会根据不同的情况,可能返回 loadingerror 或成功加载的异步组件,返回值不为 undefined,因此会走正常的组件 renderpatch 过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode 的,会进行组件更新的 patch 过程。

上次编辑于:
贡献者: lingronghai