Vue实例初始化-合并选项——vue2源码探究(7)

105 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

当我们执行new Vue()之后,Vue实例的初始化和生命周期就开始了:

上一篇文章我们说到,Vue实例初始化的时候,实际上执行了initMixin给Vue原型中声明的_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
    // 性能检查工具
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // 标记这是一个Vue实例,不用再做类型检查了
    vm._isVue = true
    // 跳过响应性监听
    vm.__v_skip = true
    // effect作用域,响应性监听相关功能
    vm._scope = new EffectScope(true /* detached */)
    // 合并选项
    if (options && options._isComponent) {
      // 组件实例创建情况下的选项合并
      initInternalComponent(vm, options as any)
    } else {
      // 根实例创建情况下的选项合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    // 为渲染函数指定上下文,主要用于开发模式下的检查
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
 
    vm._self = vm
    initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件
    initRender(vm) // 初始化渲染函数
    callHook(vm, 'beforeCreate', undefined, false /* setContext */) // 触发beforeCreate生命周期
    initInjections(vm) // 在处理data/props之前处理依赖注入的inject
    initState(vm) // 处理data/props
    initProvide(vm) // 在处理data/props之后处理依赖注入的provide
    callHook(vm, 'created') // 触发created生命周期

    // 性能检查工具
    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)
    }
  }
}

所以整个实例初始化期间,_init方法主要做了以下几件事:

  • 合并传入选项
  • 为渲染函数设置上下文(主要是开发模式下检查用,生产环境就是实例本身)
  • 触发相关生命周期
  • 初始化生命周期
  • 初始化事件
  • 初始化渲染函数
  • 处理依赖注入
  • 处理data/props
  • 开始模板编译

整个过程比较复杂我们拆开说,今天先说合并传入选项。

合并传入选项

合并传入选项这里做了个判断,将组件实例创建和根实例创建分开进行处理。

组件实例创建时的选项合并

组件实例创建时的选项合并使用了initInternalComponent方法,initInternalComponent方法接受两个参数,第一个参数是组件实例,即this。第二个参数是组件构造函数中传入的optionoption中主要有三个属性值,isComponent就在之前判断是组件实例还是根实例,_parentVode是组件实例的vnode对象,parent是该组件的父组件实例对象,initInternalComponent方法的源代码如下:

// 源码文件:src\core\instance\init.ts
export function initInternalComponent(
  vm: Component,
  options: InternalComponentOptions
) {
  // 将构造函数的options挂载到vm.$options的__proto__上
  const opts = (vm.$options = Object.create((vm.constructor as any).options))
  // 接下把传入参数的option的`_parentVode`和`parent`挂载到组件实例`$options`上
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  // 然后把父组件里的vnode上的四个属性挂载到我们的`$options`上,主要就是props和@的监听
  const vnodeComponentOptions = parentVnode.componentOptions!
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  // 如果传入的option中如果有render,把render相关的也挂载到$options上
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

根实例创建时的选项合并

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

根实例创建时合并选项会有亿点点复杂,首先调用了mergeOptions方法,参数一个是new Vue(options)时传进来的参数options,再有一个就是resolveConstructorOptions方法的返回值了。

// 源码文件:src\core\instance\init.ts
export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

super不存在

如果super不存在时,就直接返回构造函数上的option了。这个构造函数里的option是怎么来的呢?它是在src\core\global-api\index.ts文件的initGlobalAPI方法中实现的:

// 源码文件:src\core\global-api\index.ts
export function initGlobalAPI(Vue: GlobalAPI) {
    //...
    Vue.options = Object.create(null)
    ASSET_TYPES.forEach(type => {
        Vue.options[type + 's'] = Object.create(null)
    })
    
    Vue.options._base = Vue

    extend(Vue.options.components, builtInComponents)
    //...
}

其实就是遍历了ASSET_TYPES:

// 源码文件:src\shared\constants.ts
export const ASSET_TYPES = ['component', 'directive', 'filter'] as const

生成了这样的结构(是不是有些熟悉?):

options: {
    components: null,
    directives: null,
    filters: null,
}

之后将实例构造函数挂载到_base上,再通过extend方法把全局组件<keep-alive>加入到optionscomponents上:

// 源码文件:src\core\global-api\index.ts
Vue.options._base = Vue

extend(Vue.options.components, builtInComponents)
// 源码文件:src\core\components\index.ts
import KeepAlive from './keep-alive'

export default {
  KeepAlive
}

这也就是为什么<keep-alive>可以直接使用的原因了。

super存在

根据刚才的分析,根实例中不会存在super,那这个后面的判断在什么时候执行呢?可以看到resolveConstructorOptions是export出去的,也就是说,有可能在其他地方调用的时候,参数中的实例存在super,全局搜索之后,我们在src\core\vdom\create-component.ts文件中发现了它:

// 源码文件: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 {
  // ...
  resolveConstructorOptions(Ctor as typeof Component)
  // ...
}

也就是说我们在createComponent,即构造一个组件实例的时候,也会调用这个resolveConstructorOptions,这时候函数接收的Ctor来自于Ctor = baseCtor.extend(Ctor),而这个extend就是初始化时挂载到Vue构造函数上的extend方法,这个方法正是这时用来返回Ctor.options值的,暂时没在实例初始化中使用,先不展开讲了。

mergeOptions进行合并

mergeOptions方法在src\core\util\options.ts文件中:

// 源码文件:src\core\util\options.ts
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)

  // 在子选项上应用extend和mixin,
  // 但前提是它不是另一个 mergeOptions 调用后产生的对象。
  // 只有合并过的选项会有_base选项
  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
}

三个参数分别为旧的选项parent、新的选项child以及实例本身,最后合成一个对象返回。

首先通过递归处理extendmixins

// 源码文件:src\core\util\options.ts
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)
  }
}

再新建一个空对象,遍历 parent,把parent中的每一项通过调用 mergeField函数合并到空对象options里,接着再遍历 child,把存在于child里但又不在 parent中 的属性继续调用 mergeField函数合并到空对象options里:

// 源码文件:src\core\util\options.ts
const options: ComponentOptions = {} as any
let key
for (key in parent) {
  mergeField(key)
}
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}

mergeField则是通过strats中的不同策略对不同类型的option进行处理,比如ASSET(也就是刚才提过的components,directivefilters)是使用extend方法:

// 源码文件:src\core\util\options.ts
function mergeAssets(
  parentVal: Object | null,
  childVal: Object | null,
  vm: Component | null,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    __DEV__ && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

而对于生命周期选项来说,是构成一个数组:

// 源码文件:src\core\util\options.ts
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
}

function dedupeHooks(hooks: any) {
  const res: Array<any> = []
  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] = mergeLifecycleHook
})

之所以要构成一个数组,是因为我们在mixin的时候,生命周期钩子是排列下来执行而不会覆盖的,也就是说插件中的生命周期方法和实例中的生命周期方法会逐次调用。

采用不同策略之后,返回值就是最终合并成的options被挂载到$options上:

image.png