Vue源码分析之new Vue过程

1,147 阅读2分钟

这是我参与更文挑战的第14天,活动详情查看: 更文挑战

前言

写过Vue代码的人都知道,我们在平时创建一个Vue实例的时候有两种写法:

第一种:实例化一个Vue对象

new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

第二种:实例化组件

import App from './App.vue';
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app');

无论哪种方式,都调用了Vue的构造函数,那这个new Vue的具体过程到底做了什么呢??

Vue初始化

Vue的构造函数定义在src/core/instance/index.js中,它主要是调用了一个_init的方法

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')
  }
  this._init(options)
}

_init方法定义在src/core/instance/init.js中

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    
    ...

    // a flag to avoid this being observed
    vm._isVue = 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)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    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')


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

当Vue实例是组件时,执行initInternalComponent方法;否则执行mergeOptions

initInternalComponent

该方法整体来看就是给vm.$options上添加属性,具体的实例化组件后边专门说。

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  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

  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
  }
}

mergeOptions

看代码mergeOptions主要是将parent和child的options配置参数合并,在这里parent就是构造函数的options,child是实例化的时候传入的options。当child._base为true时,定义在extends和mixins中的也会被合并。

defaultStrat与strats两个变量包含了具体的合并策略,这里先不讲,后边专门开一章说这块

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    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 = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  
  
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

几个初始化函数在这里也先不具体展开,到后边会每个展开说。

_init方法主要就是初始化vm,初始化时会依次执行的生命周期钩子beforeCreate,created。初始化完事件,data等。

接下来会判断$options中是否传了el,如果传了就是我们的第一种new Vue的方式,则直接调用了vm.$mount(vm.$options.el);如果没传el,我们会手动调用$mount('#app')。其实无论实例化Vue组件还是实例化Vue对象,我们都可以传el参数过去,当然也可以选择手动调用$mount

看一下官方这个图:

image.png

可以看出无论是否传el参数,最后都是调用了$mount方法,接下来看下这个方法的实现

Vue挂载实例

从官方生命周期图可以看出,调用$mount就意味着要进入模板编译与挂载阶段了。

image.png

Vue是通过$mount实例方法挂载vm的,$mount方法有多个定义,因为它的实现与平台以及构建方式有关,所以会有多个实现方式,但都是通过在原型上定义的mount方法去扩展的。

至于template是怎么转化成render函数的,后边专门来看。这里先看一下Vue原型上定义的$mount方法:src/platform/web/runtime/index.js

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount方法主要接收两个参数:第一个是挂载的元素(可以字符串,也可以DOM对象),第二个是服务端渲染参数。最后真正是调用了mountComponent方法:src/core/instance/lifecycle.js

看一下mountComponent方法的重点部分:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    
    ...
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      ...
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 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, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

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

mountComponet首先会调用beforeMount钩子。然后定义了updateComponent

接下来实例化一个渲染Watcher,这也是mountComponent核心。该Watcher的作用:在new Vue实例的时候执行回调函数(updateComponent);vm实例中的监测数据发生变化时执行回调函数(具体watcher实现后边补充)

vm.$vnode表示Vue实例的父虚拟节点,它为null,则表示当前是根Vue的实例。

updateComponent回调函数的定义很简单:vm._update(vm._render(), hydrating),主要调用了两个方法:_render_update,_render是把Vue实例渲染成一个Vnode,_update是将Vnode渲染成真实DOM

总结

Vue项目的入口文件在new Vue实例的过程中主要做了两件事,一个是初始化vm(各种事件,参数等),一个是挂载Vue实例到'#app'上