vue源码分析-6-new Vue(options)

1,444 阅读5分钟

简述

本文主要讨论new一个Vue对象的过程

构造函数

Vue的构造函数定义很简单,定义如下,关键调用了_init方法

文件路径:vue/src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 调用_init方法
  this._init(options)
}

_init()

vue实例的原型方法_init(options)是在initMixin(Vue)方法中定义的

initMixin(Vue)定义在:vue/src/core/instance/init.js文件中

_init(options)主要做了如下几件事情

  • 如果options._isComponent是true也就是如果是组件的话,调用initInternalComponent方法初始化内部组件的options
  • 如果不是组件,处理options,调用mergeOptions方法合并options
  • 调用一系列初始化方法,和两个生命周期钩子
    initLifecycle(vm)
    // 初始化事件
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
  • 最终调用$mount方法进行初次渲染

initInternalComponent()

此方法主要处理子组件的options

initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // 获取组件的options
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  // 获得父vnode的组件options
  const vnodeComponentOptions = parentVnode.componentOptions
  // 赋值组件参数
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  // 如果有render函数
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

resolveConstructorOptions()

此方法的主要作用就是获得构造器的最新的options属性

// 解析构造器上的options
// 返回当前构造器最新的options属性
export function resolveConstructorOptions (Ctor: Class<Component>) {
  // 获得构造器上的options,此options是在initGlobalAPI方法中定义的,Vue.options=xxx
  let options = Ctor.options
  // 如果当前组件是Vue.extend()构造出来的,Ctor.super=true,递归求得父组件的options
  if (Ctor.super) {
    // 一直递归获得最原始的那个Vue构造器的options
    const superOptions = resolveConstructorOptions(Ctor.super)
    // 之后的步骤主要是为了父构造器的options变化了,那么用Vue.extend()构造出来的构造器的options也应该改变
    // Sub在创建定义的时候,赋值过一次options(详见Vue.extend实现)

    // 缓存的构造器options
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // 如果父构造器的options改变了
      // super option changed,
      // need to resolve new options.
      // 当前构造器的superOptions更新为新的
      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
}

initLifecycle(vm)

此方法主要初始化一些Vue实例(或子组件实例)的一些属性

// 此方法主要初始化一些Vue实例(或子组件实例)的一些属性
export function initLifecycle (vm: Component) {
  // 获得options
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  // 如果parent(父组件)存在的话
  // 父组件存在且不是抽象组件,如<keep-alive></keep-alive>就是一个抽象组件
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 父组件实例的$children保存当前组件的实例
    parent.$children.push(vm)
  }

  // 给vue实例的$parent赋值
  vm.$parent = parent
  // 根组件
  vm.$root = parent ? parent.$root : vm
  // 初始化 子组件数组
  vm.$children = []
  // 初始化 refs
  vm.$refs = {}

  // 初始化一些私有属性值 后续会用到
  // 组件实例相应的 watcher 实例对象。
  vm._watcher = null
  // 表示keep-alive中组件状态,如被激活,该值为false,反之为true。
  vm._inactive = null
  // 也是表示keep-alive中组件状态的属性。
  vm._directInactive = false
  // 当前实例是否完成挂载(对应生命周期图示中的mounted)。
  vm._isMounted = false
  // 当前实例是否已经被销毁(对应生命周期图示中的destroyed)。
  vm._isDestroyed = false
  // 当前实例是否正在被销毁,还没有销毁完成(介于生命周期图示中deforeDestroy和destroyed之间)。
  vm._isBeingDestroyed = false
}

initEvents(vm)

initEvent主要调用了updateComponentListeners()方法,updateComponentListeners调用了updateListeners()方法

updateListeners()方法主要作用就是将绑定在组件上的事件,保存至vm._events属性中,并且如果绑定的事件更新了,那么vm._events中的事件也会更新。

export function initEvents (vm: Component) {
  // 初始化实例的_events属性
  vm._events = Object.create(null)
  // 初始化实例的_hasHookEvent属性
  vm._hasHookEvent = false
  // init parent attached events
  // 拿到当前组件绑定的事件
  const listeners = vm.$options._parentListeners
  // 如果有绑定事件
  if (listeners) {
    // 更新当前组件的事件
    updateComponentListeners(vm, listeners)
  }
}

updateListeners传入了几个方法,add,remove,createOnceHandler 这几个方法其实调用的是 vm.$on , vm.$off, vm.$once方法 这三个方法定义在eventsMixin(Vue)方法中

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}
  • Vue.prototype.$on方法作用是给vue实例vm._events 赋值,组件绑定的事件都存在_events(数组)属性上
  • Vue.prototype.$once方法的作用就是执行过一次的方法,就会被移除
  • Vue.prototype.$off方法的作用就是将事件从_events(数组)属性中移除
  • Vue.prototype.$emit方法的作用就是可以执行保存在_events属性中的事件
export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/

  // 监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    // 如果是数组,递归调用
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      // vm的_events属性保存事件,同名的事件都保存在一个数组里
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        // 如果匹配到,标识有钩子事件
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  // 监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。
  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {
      // 执行on()的话,on会先被移除,再执行一次,也就是执行一次之后会被移除
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    // 无参数,移除所有事件
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    // 数组遍历递归移除
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    // 不存在 直接返回
    if (!cbs) {
      return vm
    }
    // 如果fn参数没传,或者为false,移除所有事件
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    // 如果fn参数传了,只移除于fn相等的函数
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

initRender(vm)

此方法主要做了如下几件事情

  • 处理插槽vm.$slot属性,将插槽中的dom对应的vnode赋值给vm.$slot
  • 定义vm._c方法
  • 定义vm.$createElement方法

_c()和$createElement方法主要调用了createElement()方法,createElement方法主要作用是供render函数执行,然后生成vnode

_c() 和 $createElement区别就是 通过内部模板编译生成的render函数会执行_c(), 用户自定义的render函数会执行$createElement方法。

==模板编译成render函数和createElement()生成vode,会在专门的章节中讲解==

此处仅仅知道initRender(vm)主要定义了vm的_c和$createElement方法即可

// 初始化render相关属性和方法
export function initRender (vm: Component) {
  // 初始化_vnode属性
  vm._vnode = null // the root of the child tree
  // 初始化_staticTrees属性
  vm._staticTrees = null // v-once cached trees
  // 获取options
  const options = vm.$options
  // 获得父组件的占位vnode,其实也就是当前组件的vnode
  // 最外层的vm的options._parentVnode是undefined
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  // renderContext === 父组件实例
  const renderContext = parentVnode && parentVnode.context
  // options._renderChildren是写在组件插槽中的dom编译后生成的vnode
  // vm.$slots保存了组件插槽中dom对应的vnode
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  // 作用域插槽属性初始化
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  // 定义_c方法
  // 通过template编译生成的render函数会执行的_c方法
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  // 用户自定义会执行的render方法
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}