Vue 2.x 源码阅读记录(二):组件化

吐槽君 分类:javascript

写在前面

文章为阅读笔记向,需 clone 下来 Vue 源码并加以调试服用~~

一个最基本的组件例子,下面围绕这个例子,对源码进行分析:

// 创建一个名为 App 的组件
const App = Vue.component('App', {
  name: 'App',
  template: `<h1>My App</h1>`,
  props: {},
  data() {
    return {};
  },
  methods: {},
});

new Vue({
  el: '#app',
  render: (h) => h(App)
});
 

createComponent

在渲染的部分,我们分析了 _createElement 方法的具体实现,其中有一段代码是分析了tag,如果传入的是 html 标签则生成标签的 vnode 对象,否则调用 createComponent方法创建一个组件的 vnode:

  // 开始创建 vnode
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      // tag 如果是一个内置节点(div, span 等),直接创建一个 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
      // tag 如果是一个已注册的组件名,则创建一个组件类型的 vnode
      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
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    // tag 直接传递了一个组件,创建组件类型的 vnode
    vnode = createComponent(tag, data, context, children)
  }
 

createComponent定义在src\core\vdom\create-component.js,它在我们例子中主要做的三件事是:构造子类构造函数、安装组件函数钩子、实例化 vNode。先大致看看它的流程:

// vnode = createComponent(tag, data, context, children)
// 创建组件 vNode
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // debugger
  if (isUndef(Ctor)) {
    return
  }

  /* 
    context = 当前 Vue 实例
    Vue.option 在 src\core\global-api\index.js 定义并将 Vue 的构造函数赋值给 _base:Vue.options._base = Vue
    在初始化 _init 时有一个合并 option 的操作
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    baseCtor 就是当前上下文的构造函数
  */
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    // 如果 Ctor 是一个普通对象,则调用 Vue.extend 返回的
    // 在例子中,使用的是 Vue.component() 创建组件,返回的就是 extend 返回的一个组件构造函数
    Ctor = baseCtor.extend(Ctor)
  }

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

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

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

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // 向组件上挂载 init、prepatch、insert、destroy 钩子函数,后面介绍
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  // 创建组件的 vnode
  // 组件名由 Vue 根据组件的 cid 和 name/tag 定义
  // 将 props、事件、tag、children 等属性存到 VNode 的 context 对象下
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  // 最后返回组件的 vnode
  return vnode
}
 

构造子类构造函数

  const baseCtor = context.$options._base

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

在我们 import Vue from 'vue' 的时候,会执行一个 initGlobalAPI方法,它定义在src\core\global-api\index.js,主要做的是将一些静态的属性和方法挂载到 Vue 构造函数上:

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    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)
}
 

在这里面,看到了 _base的定义,它指向的是 Vue 的构造函数:

Vue.options._base = Vue
 

而在createComponent中,如果传入的ctor是一个对象,则调用baseCtorextend方法,这个方法返回一个继承了 Vue 构造函数的子类,通常来说,就是一个组件的构造函数。它在上面的initExtend方法中被定义:

/**
   * Class inheritance
   * Vue.extend 静态方法,返回一个继承了 Vue 实例的组件构造函数
   * 在 initGlobalAPI 的运行中被 initExtend 定义
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      // 如果调用同一个组件构造方法多次则返回缓存的
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 校验组件名是否合法或使用了原生标签,报出警告
      validateComponentName(name)
    }

    // 定义一个子类继承 Vue 构造函数
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // 原型继承
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    // 缓存父类的一些属性
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // 缓存构造函数
    cachedCtors[SuperId] = Sub
    return Sub
  }
}
 

安装组件钩子函数

// install component management hooks onto the placeholder node
installComponentHooks(data)
 

它的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中 ,这些 hook 在后面 patch 中会在不同阶段调用:

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

  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]
    if (existing !== toMerge && !(existing && existing._merged)) {
      // 在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行 mergeHook 函数做合并
      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

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  // 创建组件的 vnode
  // 组件名由 Vue 根据组件的 cid 和 name/tag 定义
  // 将 props、事件、tag、children 等属性存到 VNode 的 context 对象下
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
 

createComponent返回组件的 vnode 后,一样会走到_update方法,执行patch函数。


patch

通过前面我们知道,_updatecreateElm方法用于 patch vnode,将 vnode 生成真实 DOM 并插入。这里大致记录一下createElm流程:

vnode为普通元素:创建一个对应的tag的元素,调用createChildren循环调用createElm完成 DOM 的渲染:

vnode.elm = vnode.ns
	? nodeOps.createElementNS(vnode.ns, tag)
	: nodeOps.createElement(tag, vnode)
// ...
createChildren(vnode, children, insertedVnodeQueue)
 

vnode为组件时,调用组件的init钩子函数进行组件的创建与插入 DOM 过程,并终止:

// createElm
// 如果 createComponent 返回 true,则说明已经完成组件 DOM 的 patch
// 随后中止 createElm 后面的 DOM 逻辑
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
}
 

这里分步骤来看createComponent内部执行的流程:

​ 1、调用init钩子函数,相当于组件流程的入口:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      // 是一个组件 vnode
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 在 src\core\vdom\create-component.js 中的 createComponent 挂载了组件钩子函数
        // 满足条件则 i 就变成了 init 钩子函数
        i(vnode, false /* hydrating */)
      }
    }
    // ...
  }
 

2、init函数内部调用了createComponentInstanceForVnode,拿到组件实例对象并执行组件的挂载方法:

// 组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    // activeInstance 在 _update 的调用中被赋值,是当前实例的父级构造函数
    activeInstance
)
// 调用组件的 $mount 方法
// 由于组件是不用传入 el 的,所以是 $mount(undefined, false)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
 

3、createComponentInstanceForVnode执行组件的_init_init 中执行initLifecycle将当前组件实例插入到父实例的$children中。接着执行了initInternalComponent,合并了一些组件的 options。最后返回组件的实例:

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
  }
  // 调用 Vue 的子类构造函数,返回组件实例
  // 也就是组件构造函数调用了 this._init(options)
  // 组件也会走一遍 new Vue(options) 的初始化过程
  return new vnode.componentOptions.Ctor(options)
}
 

4、init函数调用了组件实例的$mount(vnode.elm),由于组件的 vnode 只是一个占位符,所以elmundefined。执行了$mount中拿到用户传入的template生成render方法,调用mountComponent。调用 mountComponent,就相当于进入了普通元素的_update(_render())流程。由于elmundefined,所以在这个 patch 过程中不会被插入到 DOM 中,但是生成的 DOM 元素会被赋值给占位 vnode 的componentInstance 属性下的$el属性。:

// 开始调用 $mount
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
 

5、createComponent中的i执行完成之后,执行initComponent方法,将vnode.componentInstance.$el赋值给vnode.elm

// createComponent
if (isDef(vnode.componentInstance)) {
    initComponent(vnode, insertedVnodeQueue)
    insert(parentElm, vnode.elm, refElm)
    // 在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入
    // init 钩子里会不断地去递归子组件,所以 DOM 会按先子后父的顺序插入
    // 如果组件 patch 过程中又创建了子组件,那么DOM 的插入顺序是先子后父。
    if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
    }
    return true
}

// initComponent
vnode.elm = vnode.componentInstance.$el
 

6、上面执行insert了方法,将vnode.elm插入 DOM 树,删除旧 vnode,返回组件vnode.elm赋值给vm.$el。至此,createComponent完成了对组件的 patch,然后将主要流程还给createElm,此时createComponent返回true,则当前createElm流程结束。这是 Vue 一种深度遍历的手段:

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    // 如果 createComponent 始终返回 true,表示是当前还是一个组件,继续进入组件流程
    return
}
 

7、最后,再由patch函数删除 DOM 中的旧节点,完成整个 patch 过程:

if (isDef(parentElm)) {
    removeVnodes([oldVnode], 0, 0)
}
 

PS:记录一些在整个 patch 过程中的重要遍历以及定位:

// 在 initLifecycle 中定义,initLifecycle 把当前实例 vm 插入到其父实例中 parent(如果父实例存在)
$parent, $root, $children 

// 在 mountComponent,第一次 patch 时为 #app 根元素
vm.$el = el

// createComponent 中将挂载组件钩子函数,定义组件占位符 vnode(tag = vue-component-cid..)

// _render() 返回的 vnode 对象
vm._vnode

// 在 createComponentInstanceForVnode 中定义,表示父 vnode 和当前组件 vnode
options.parent, _parentVnode

/* 
	小结:在 patch 的 createElm 中的 createComponent 中只要传入的 vnode 属于组件 vnode,就会返回 true,从而结束 createElm(patch) 的执行,createElm 的 vnode 一旦为组件,则组件从头开始执行 _init(),当组件 patch(insert) 完之后,才会继续执行父级的 patch ,从而形成先子后父的插入顺序。
*/
 

合并配置

普通实例合并

一个基本的实例配置例子,合并之后会将所有配置存储到实例的$options属性上:

let app = new Vue({
  // mixins: [mixin],
  el: '#app',
  data: {
    msg: 'Hello'
  },
  created() {}
})

/*
	app.$options: {
        components: {}
        created: [ƒ]
        data: ƒ mergedInstanceDataFn()
        directives: {}
        el: "#app"
        filters: {}
        render: ƒ anonymous( )
        staticRenderFns: []
        _base: ƒ Vue(options)
	}
*/
 

initGlobalAPI的调用中,首先定义了一些默认的配置,包括componentsdirectivesfilters,随后将 Vue 的一些内置组件keep-alivetransitiontransitionGroup合并到components配置中,这就是为什么 Vue 可以不用注册就使用这些组件:

{/* 默认的 options 定义 */}
Vue.options = Object.create(null)
{/* 添加 components, directives, filters 属性 */}
ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
})

{/* 向 components 里赋值 builtInComponents 中的属性 */}
{/* 这里实际上就是将 Vue 的内置组件率先合并到默认的 components 中 */}
extend(Vue.options.components, builtInComponents)
 

在创建实例时执行_init方法,对这个例子,它调用mergeOptions合并默认和传入的配置:

// 合并 new Vue 的配置参数
vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),  // 默认 options
    options || {},  // 用户传入的 options
    vm
)
 

在例子中,普通实例的resolveConstructorOptions直接返回默认的options

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  
  if (Ctor.super) {
      //...
  }

  // 当前是 Vue 根构造函数时直接返回默认的 options
  return options
}
 

mergeOptions将默认生成的配置与传入的配置合并,然后返回赋值给$options属性。它遍历默认和传入的配置,在内部为每个配置的项都设置了独立的合并策略函数,调用它们进行合并,使用options存储并返回:

const options = {}
let key
for (key in parent) {
    // console.log('parentKey :>> ', key);
    mergeField(key)
}
for (key in child) {
    if (!hasOwn(parent, key)) {
        // child 不存在 parent 中
        mergeField(key)
    }
}
// console.log('strats :>> ', strats);
function mergeField (key) {
    const strat = strats[key] || defaultStrat
    // strats 将所有选项都定义一个自己的合并策略函数
    // 合并策略函数将 parent 和 child 合并后的属性赋值给 options[key]
    options[key] = strat(parent[key], child[key], vm, key)
}

return options
 

这里看一下生命周期函数的合并策略,生命周期函数最终会被合并为一个数组,便于 mixin 等属性的生命周期函数顺序执行:

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

function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}

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

mixins的场景

接下来向配置中添加一个mixin,看看是如何合并的:

const mixin = Vue.mixin({
  created() {
    console.log('mixin created')
  }
})

let app = new Vue({
  mixins: [mixin],
  el: '#app',
  data: {
    msg: 'Hello'
  },
  created() {}
})

/*
	app.$options: {
        components: {}
        created: (2) [ƒ, ƒ]
        data: ƒ mergedInstanceDataFn()
        directives: {}
        el: "#app"
        filters: {}
        mixins: [ƒ]
        render: ƒ anonymous( )
        staticRenderFns: []
        _base: ƒ Vue(options)
	}
*/
 

mergeOptions中,如果传入的配置中带有mixins则将其遍历,递归调用mergeOptions将各个mixin的配置合并到parent默认配置中,最后再将传入的其它配置合并:

if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
    }
}
 

需要注意的是,在initGlobalAPI调用的initMixin中,定义了Vue.mixin静态方法。在调用它时会将当前mixinVue的默认options合并,再到后面实例化时将这个合并后的mixin与实例的options合并:

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
 

组件合并配置

再将例子改造成组件的使用场景:

const childComponent = {
  template: '<div>{{msg}}</div>',
  data() {
    return {
      msg: 'Hello Vue'
    };
  },
  created() {
    console.log('component created');
  },
  mounted() {
    console.log('component mounted');
  },
};

const mixin = Vue.mixin({
  created() {
    console.log('mixin created');
  }
})

let app = new Vue({
  mixins: [mixin],
  el: '#app',
  data: {
    msg: 'Hello'
  },
  created() {},
  render: (h) => h(childComponent)
})
 

在渲染组件环节,会调用createComponentInstanceForVnode来生成组件特有的options并调用组件构造函数。在initGlobalAPI生成子组件构造函数的initExtend方法中,将父构造函数的 options 与当前组件传入的 options 合并:

// 将父构造函数的 options 与当前组件传入的 options 合并
Sub.options = mergeOptions(
    Super.options,
    extendOptions
)
 

createComponentInstanceForVnode实例化组件构造函数,执行_init方法,此时合并配置调用的是initInternalComponent

if (options && options._isComponent) {
    // 组件构造函数的 _init() 走这里
    initInternalComponent(vm, options)
}

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // vm.constructor.options 拿到之前父构造函数与当前组件传入的配置合并后的配置
  // 相当于 vm.$options = Object.create(Sub.options)
  // Object.create 创建一个指定原型的对象,也就是将 vm.constructor.options 放到了 opts.__proto__ 属性中
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  // _parentVnode 在 createComponentInstanceForVnode 被赋值
  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, 子组件初始化过程通过 initInternalComponent 方式要比外部初始化 Vue 通过 mergeOptions 的过程要快 ,因为没有递归与合并策略等,最终组件的$options是这样的:

/*
	vm.$options: {
        parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
        propsData: undefined
        render: ƒ anonymous( )
        staticRenderFns: []
        _componentTag: undefined
        _parentListeners: undefined
        _parentVnode: VNode {tag: "vue-component-1", data: {…}, children: undefined, text: undefined, elm: div, …}
        _renderChildren: undefined
        __proto__:
            components: {}
            created: (2) [ƒ, ƒ]
            data: ƒ data()
            directives: {}
            filters: {}
            mounted: [ƒ]
            template: "<div>{{msg}}</div>"
            _Ctor: {0: ƒ}
            _base: ƒ Vue(options)
            __proto__: Object
	}
*/
 

生命周期

在组件渲染的各个阶段会调用不同的生命周期,它使用一个总线callHook函数来调用生命周期钩子。它在内部调用了传入的hook,并带有错误处理:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  // hook 函数
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      // 执行 hook 函数,内部进行了错误处理
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
 

beforeCreate & created

在初始化调用_init方法时,会调用这两个钩子:

// 初始化生命周期,渲染等
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 初始化 props, data, methods, watch, computed 等
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
 

由于beforeCreate调用在initState前面,所以beforeCreate时无法访问实例内的datamethods等属性。created在调用实例初始化完成调用$mount时还没有调用$mount,所以created无法访问refs、DOM 等元素属性。

beforeMount & mounted

在开始调用mountComponent方法时,beforeMount 会调用,此时可以访问datamethods等属性。在vm._update(vm._render(), hydrating)将 DOM 挂载完成之后,mounted会被调用,此时可以访问 DOM:

xport function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...

  // 执行 beforeMount 生命周期
  callHook(vm, 'beforeMount')

  let updateComponent
  // ...
  updateComponent = () => {
      vm._update(vm._render(), hydrating /* false */)
  }
    
  // ...
    
  new Watcher(vm, updateComponent, noop /* 空函数 */, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        // 组件已挂载并未销毁,执行 beforeUpdate 生命周期
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true) 
  
  // ...
  
  if (vm.$vnode == null) {
    // 渲染完成
    vm._isMounted = true
    // 执行 mounted 生命周期
    callHook(vm, 'mounted')
  } 
  // ...
}
 

vm.$vnode = _parentVnode_render方法中定义,如果vm.$vnodenull,表示这不是一次组件的 mount,而是new Vue()的 mount。那么再看看组件的 mount。首先了解一下,在patch方法中,定义了一个insertedVnodeQueue = []队列,当vnode是组件时,在组件插入 DOM 之前的initComponent方法中,会将组件vnodepush 到队列中:

// patch 
const insertedVnodeQueue = []

// initComponent
function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
        insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
        vnode.data.pendingInsert = null
    }
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
        setScope(vnode)
    } else {
        // empty component root.
        // skip all element-related modules except for ref (#3455)
        registerRef(vnode)
        // make sure to invoke the insert hook
        // 组件 vnode push 到队列中
        insertedVnodeQueue.push(vnode)
    }
}
 

由于在组件渲染过程中执行_render的时候,$vnode已经赋值了_parentVnode。所以组件的init钩子最后调用child.$mount不会执行mounted钩子。在patch方法最后,组件渲染完成之后,会调用 invokeInsertHook方法,它调用了队列中所有组件的insert钩子函数:

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) {
            // 执行组件的 insert 钩子
            queue[i].data.hook.insert(queue[i])
        }
    }
}
 

insert钩子内调用了组件的mounted生命周期,insertedVnodeQueue的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted钩子函数的执行顺序也是先子后父 :

insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
        componentInstance._isMounted = true
        // 组件的 mounted 调用
        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 */)
        }
    }
},
 

beforeUpdate & updated

beforeUpdate的执行时机是在mountComponent渲染 Watcher 的before函数中,它在组件mounted之后才会执行:

// 在挂载时添加一个`渲染 watcher`
// 用于元素的第一次渲染和后续的更新
new Watcher(vm, updateComponent, noop /* 空函数 */, {
    before () {
        if (vm._isMounted && !vm._isDestroyed) {
            // 组件已挂载并未销毁,执行 beforeUpdate 生命周期
            callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */)
 

updated后续更新。。。

beforeDestroy & destroyed

组件销毁的流程暂时还没有了解,但最后都会调用$destroy全局方法来销毁组件。beforeDestroy$destroy开始调用时执行。经过一系列销毁操作之后执行destroyed,包括从父实例的$children删除自身、卸载watcher、移除 DOM 等:

  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) {
      // 从父的 $children 树中删除自身
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      // 卸载 watcher
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // 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
    }
  }
}
 

在销毁过程中会递归销毁该组件的子组件,所以destroyed也是先子后父顺序执行。


组件注册

全局注册

先看一个全局注册组件的例子:

const MyComp = Vue.component('MyComp', {
  template: `
    <div class="my-comp">
      <span>{{msg}}</span>
    </div>
  `,
  data() {
    return {
      msg: 'Hello MyComp'
    };
  },
});

new Vue({
  el: '#app',
  render: (h) => h(MyComp)
});
 

在执行initGlobalAPI时,最后执行了initAssetRegisters方法,这个方法为Vue构造函数添加了componentdirectivefilter三个全局方法,其中component就是注册一个全局组件:

  // 定义了 Vue.component, Vue.directive, Vue.filter 三个静态方法
  // 这三个常量值也成为组件内部的选项
  ASSET_TYPES.forEach(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 (process.env.NODE_ENV !== 'production' && type === 'component') {
          // 检查组件名规范
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // 调用实例上的 exntend 方法创建组件
          // 相当于 Vue.extend(definition /* options */)
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        // 向 options 的 components 选项中存入该组件
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
 

当调用Vue.component时,实际上是调用了this.options._base.extend,相当于Vue.extend,返回一个组件的构造函数:

definition = this.options._base.extend(definition)
 

此时MyComp是一个组件的构造函数。后来执行到这个组件的initInternalComponent时,会将这个组件的options合并到vm.$options的原型上:

var opts = vm.$options = Object.create(vm.constructor.options);
 

要注意的是,组件在初始化构造函数的时候合并了根Vue构造函数的options

Sub.options = mergeOptions(
    Super.options,
    extendOptions
)
 

合并完之后,该组件自身components中也会有该组件自身的构造函数供自身调用,并且按照组件的合并策略,会将Vue构造函数的components作为一个原型对象合并到组件的components.__proto__下:

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}
 

此时组件的components.__proto__选项下包含Vue.components下的所有组件,在该组件下就可以使用transition以及其它全局的组件。

改造一下例子:

const MyComp = Vue.component('MyComp', {
  template: `
    <div class="my-comp">MyComp</div>
  `,
  data() {
    return {
      msg: 'Hello MyComp'
    };
  },
});

const App = Vue.component('App', {
  template: `
    <div class="my-comp">
      <MyComp />
    </div>
  `,
  data() {
    return {
      msg: 'Hello App'
    };
  },
});

new Vue({
  el: '#app',
  render: (h) => h(App)
});
 

App组件在执行的_createElement时,它的tagMyComp,因此会下面这个逻辑。调用resolveAsset返回组件构造函数给Ctor在进行组件 vnode 的创建:

if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    // tag 如果是一个已注册的组件名,则创建一个组件类型的 vnode
    vnode = createComponent(Ctor, data, context, children, tag)
}
 

resolveAsset通过一系列条件与原型链查找(由于前面提到过,组件构造函数在初始化时会合并父实例的options,并且在initInternalComponent中组件构造函数的options定义在__proto__上),将需要的组件构造函数找出:

export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  
  // check local registration variations first
  // 从 components 里取出组件构造函数返回
  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 (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    )
  }
  return res
}
 

局部注册

例子:

const localComp = Vue.extend({
  template: '<h1>Local component</h1>'
});

new Vue({
  el: '#app',
  components: {
    localComp
  },
});
 

组件注册由于直接在选项中编写,会直接进入mergeOptions的组件合并策略环节。将传入的components直接合并到vm.$options.components, 这样就可以在resolveAsset的时候拿到这个组件的构造函数,并作为createComponent 的钩子的参数 :

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}
 

二者区别:

Vue.component全局创建的组件直接向Vue构造函数的options.components选项中添加,子组件创建时会合并Vueoptions并将使用原型链串联起来,所以在子组件内都可以使用全局注册的组件。

局部创建的组件只会保存在当前$options.component中,其它组件不会合并当前的$options,所以只能在当前组件内访问注册的组件。

理解组件注册最重要的地方就是组件 options 的合并、组件合并策略、initInternalComponent三个地方。


异步组件

异步组件在需要使用(render)到的时候才会去创建一个组件,是一种优化手段。看一个基本例子,AsyncComp只有在render时才会去创建组件实例:

const AsyncComp = Vue.component('AsyncComp', (resolve, reject) => {
  // require(MyComp, resolve);
  setTimeout(() => {
    resolve({
      template: '<h1>My component</h1>'
    });
  }, 1000);
});

new Vue({
  render: (h) => h(AsyncComp)
}).$mount('#app');
 

异步组件第二个参数接收一个工厂函数,在完成异步操作后将组件 options 传给resolve函数。在Vue.component方法的处理中,不会将工厂函数变成组件的构造函数,仅仅是保存到Vue.options中:

// 调用 Vue.component
if (type === 'component' && isPlainObject(definition)) {
    definition.name = definition.name || id
    // 调用实例上的 exntend 方法创建组件
    // 相当于 Vue.extend(definition /* options */)
    definition = this.options._base.extend(definition)
}

// 向 options 的 components 选项中存入工厂函数
this.options[type + 's'][id] = definition
return definition
 

在不对组件进行 render 的情况下,组件上没有optionscid以及其它方法,只是一个函数体。因为在上述代码中并没有执行到extend方法。当组件被渲染的时候,还是会走到createElement >createComponent逻辑,对应例子,createComponent会执行这一块逻辑:

// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
    // 走到这里,表示是一个异步组件
    // 将工厂函数保存
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
        // return a placeholder node for async component, which is rendered
        // as a comment node but preserves all the raw information for the node.
        // the information will be used for async server-rendering and hydration.
        return createAsyncPlaceholder(
            asyncFactory,
            data,
            context,
            children,
            tag
        )
    }
}
 

resolveAsyncComponent用来解析异步组件,并处理了工厂函数、promise、高级异步组件三种异步组件写法。

工厂函数

写法就是上面的例子。在工厂函数流程中定义了resolvereject两个方法,它们使用once方法做了一层包装,来确保对同一个组件只执行一次:

/**
 * Ensure a function is called only once.
 */
export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

const resolve = once((res: Object | Class<Component>) => {
    // cache resolved
    // ensureCtor 实际是一个闭包,它返回组件构造函数
    // 这里相当于调用了组件的构造函数,也就是 vm._init
    // _init 方法没有返回值,所以 factory.resolved 是一个 undefined,表示组件构造函数已被调用
    factory.resolved = ensureCtor(res, baseCtor)
    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 => {
    process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : '')
    )
    if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
    }
})
 

resolve方法使用ensureCtor方法将传入的组件转为构造函数,在ensureCtor中实际将组件options调用Vue.extend来生成组件构造函数执行extend逻辑,这也就是按需引入组件的本质:

function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}
 

在异步组件流程中,resolve接下来执行forceRender方法,将当前渲染的每个实例都执行一次$forceUpdate方法,$forceUpdate调用了实例上的渲染 watcher 的update方法触发组件重新渲染。 之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate 可以强制组件重新渲染一次:

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

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

Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
        vm._watcher.update()
    }
}
 

最后调用工厂函数,返回到createComponent中返回一个异步组件占位符 vnode:

const res = factory(resolve, reject)

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

promise 异步组件

写法:

Vue.component(
  'AsyncComp', 
  () => import('SomeComp')
);
 

解析 promise 异步组件执行() => import()时,res返回一个 Promise 对象。最后将resolvereject作为res.then方法参数调用这两个方法,就完成了 promise 异步组件写法:

const res = factory(resolve, reject)  // Promise

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

高级异步组件(较少使用,详情看源码)

异步组件和普通组件区别

普通组件在定义时以及执行了组件的构造函数,完成了组件的合并配置等一些初始化的工作;异步组件在没有进入 render 流程时则没有进行组件初始化,待到 render 的时候才会执行extend生成组件的构造函数及其初始化。代码逻辑层面,就是何时执行extend的区别

回复

我来回复
  • 暂无回复内容