Skip to content

初始化

这篇主要分析一下Vue的初始化过程,当我们引用构建文件或者使用脚手架构建项目后,入口文件会创建一个Vue实例,前提是执行了import Vue from 'vue'后(初始化Vue后):

javascript
var vm = new Vue({
  // 选项
})

一个 Vue 应用由一个通过 new Vue 创建的根 Vue实例,以及可选的嵌套的、可复用的组件树组成。

那么这里new Vue的过程主要做了什么呢,接下来一一分析。

Vue构造函数

这里先说一下,我们怎么从源码中一步步找到Vue构造函数。在前置准备中有提到过 Vuebuild 构建过程,/scripts/config.js是构建的配置文件,因为这里分析的是umd的完整版vue.js文件,找到对应的配置,如下:

js
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

对应的构建入口文件是 /platforms/web/entry-runtime-with-compiler.js,打开该文件,继续寻找Vue

// entry-runtime-with-compiler.js
...
import Vue from './runtime/index'
...

跳转到runtime/index.js中,继续寻找Vue

js
import Vue from 'core/index'

以此类推跳转,最终在/core/instance/index中找到了 Vue 构造函数。

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

Quiz Time👇

为什么使用function 构造函数,而不使用 class?
✅ 这是为了方便后续给vue实例混入实例成员,直接操作原型 prototype,实例成员包含实例方法和属性, 使用 class 不能实现吗?其实是一样的,只是没必要再用语法糖去实现。也不需要实现继承的功能场景。

实例成员初始化

从上面可以得出Vue的本质就是一个构造函数,当new Vue()时,实际上调用了Vue的构造函数来实例化对象,其中调用了成员方法__init,这个__init怎么来的,下面会分析。

现在集中这个instance/index文件,除了定义Vue构造函数之外,还调用了一系列的混入操作来扩展 Vue 的功能,实际上是在不同文件里定义原型上的方法。

js
function Vue(options) {
  ...
  this._init(options)
}
// 注册vm的_init()方法,初始化vm
initMixin(Vue)

// 注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)

// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)

// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)

// 混入和实例方法
// $nexttick/render
renderMixin(Vue)

下面将详细分析这些功能。

initMixin

主要注册了_init方法,在_init中完成了一系列初始化操作,包括合并配置,初始化事件中心,初始化生命周期,初始化props/methods/data/computed/watch等。

js
 Vue.prototype._init = function (options?: Object) {
    ...
   // 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 {
      // 合并用户传入的options和vue的构造函数的options
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    ...
    // 生命周期相关变量初始化
    // $children/$parent/$root/$refs
    initLifecycle(vm)

    // vm的事件监听初始化,父组件绑定在当前组件上的事件
    initEvents(vm)

    //$slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)

    // 触发钩子beforeCreate
    callHook(vm, 'beforeCreate')

    // 依赖注入,把inject的成员注入到vm上
    initInjections(vm) // resolve injections before data/props

    // 初始化vm的_props/methods/_data/computed/watch
    initState(vm)

    // 初始化provide
    initProvide(vm) // resolve provide after data/props

    // 触发钩子created
    callHook(vm, 'created')

    ...
    //最后
    // 调用$mount()挂载,渲染页面
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }

 }

从源码可以看出,init将不同的功能拆分成不同的模块进行实现,逻辑线清晰,下面着重贴一下重要模块initState的核心实现部分。

initState

initState模块中的的内容都非常关键,涉及到之后将要分析的响应式原理,先大概看一下实现:

js
  vm._watchers = []
  const opts = vm.$options
  // 将opts.props变成响应式注入到vue实例中
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }

Quiz Time👇

data 中的属性可以和 methods 或者 prop 或者 computed 一致吗? ✅ 不可以,因为他们最终都会被挂载到vm实例上,配置将合并化,所以不能重复。

组件中 data 为什么必须是一个函数? ✅ 数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data, 就会造成一个变了全都会变的结果。

stateMixin

js
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

Vue.prototype.$set = set
Vue.prototype.$delete = del

Vue.prototype.$watch = function(){}

eventsMixin

on注册事件为例,

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

lifecycleMixin

js
// VNODE渲染成真实的dom,首次渲染和数据更新时都会调用
Vue.prototype._update = function(){}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function(){}

renderMixin

js
Vue.prototype.$nextTick = function(){}
Vue.prototype._render = function(){}

静态成员初始化

刚才我们分析了instance/index文件中Vue实例成员的初始化,那么core/index.js中实现的是Vue静态成员的初始化,通过initGlobalAPI(Vue)实现。

  1. 静态属性Vue.config

    js
    // 初始化 Vue.config 对象
     Object.defineProperty(Vue, 'config', configDef)
  2. Vue.options

    js
    Vue.options = Object.create(null)
    //扩展为components directives filters
    //用来存储全局的组件,指令,过滤器
    ASSET_TYPES.forEach(type => {
      Vue.options[type + 's'] = Object.create(null)
    })
  3. 静态方法

    js
    // 直接挂载到vue的构造函数上
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = nextTick
    
    /* 注册Vue.use()用来注册插件 */
    initUse(Vue)
    
    /* 实现混入功能 */
    initMixin(Vue)
    
    /* 注册Vue.extend 基于传入的options返回一个组件的构造函数
    实际上里面就是继承自Vue的组件
    */

initExtend(Vue)

/* 注册Vue.directive(),Vue.component(), Vue.filter()*/ initAssetRegisters(Vue)




## 平台相关
1. 注册与平台相关的全局指令(`/src/platforms/(web/weex)/runtime/index.js`),`v-model`,`v-show`
```js
extend(Vue.options.directives, platformDirectives)
  1. 注册与平台相关的全局组件,v-transitionv-transition-group
js
extend(Vue.options.components, platformComponents)
  1. 全局方法:Vue.prototype.__patch__,位置:(runtime/index.js)
js
// 虚拟dom转换成真实dom
Vue.prototype.__patch__ = inBrowser ? patch : noop
  1. Vue.prototype.$mount,位置:(runtime/index.js)
js
// 给实例增加了一个mount,挂载
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 获取el,因为运行时版本没有编译的过程
  el = el && inBrowser ? query(el) : undefined
  // 与浏览器无关的一个核心方法
  return mountComponent(this, el, hydrating)
}

Quiz Time👇

这里为什么要再次赋值一次 el?
✅ 这是为了兼顾运行时版本,因为运行时版本中没有编译的过程,编译的过程中会将 el 转化为元素,查看 query 的内部实现即可明白。
js
/**
 * Query an element selector if it's not an element already.
 */
export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}