Vue2.6x源码解析(一):Vue初始化过程

317 阅读5分钟

系列文章:

1,源码目录结构

Vue.2.6x的目录结构如下:

├── scripts         # 与构建相关的脚本和配置文件
├── dist            # 构建后的文件
├── flow            # Flow的类型声明
├── packages        # vue-server-renderer和vue-template-compiler,它们作为单独的NPM包发布
├── test            # 所有的测试代码
├── src             # 源代码
│ ├── compiler      # 与模板编译相关的代码
│ ├── core          # 通用的、与平台无关的运行时代码 // 核心源码
│ │ ├── observer    # 实现变化侦测的代码 
│ │ ├── vdom        # 实现虚拟DOM的代码
│ │ ├── instance    # Vue.js实例的构造函数和原型方法
│ │ ├── global-api  # 全局API的代码
│ │ └── components  # 通用的抽象组件
│ ├── platforms     # 特定平台代码web/weex
│ ├── server        # 与服务端渲染相关的代码
│ ├── sfc           # 单文件组件(* .vue文件)解析逻辑
│ └── shared        # 整个项目的公用工具代码
└── types           # TypeScript类型定义

重点: src/core目录下是Vue.js核心源码,这部分逻辑是与平台无关的,是Vue框架的核心运行时。

2,Vue从哪里来

我们启动一个Vue项目的时候都知道,项目的入口文件是src/main.js,在这个文件里我们会引入一个Vue构造函数,那么这个Vue是从哪里来的呢?

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

根据项目依赖Vue源码里面的package.json文件中,我们可以得到结果:

// package.json
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js"

也就是说我们通过Vue-cli脚手架搭建的一个Vue项目,会通过模块化的方式引入dist/vue.runtime.esm.js这个文件,找到这个文件,我们在最后一行代码可以发现,文件最终其实就是导出了一个Vue构造函数,而我们在main.js文件里就是引入了这个构造函数。

// dist/vue.runtime.esm.js
export default Vue;

3,Vue的定义

通过前面我们知道了Import导入的Vue构造函数从何而来,下面我们从Vue2.6x源码中寻找Vue构造函数。

src/core/instance/index.js我们可以找到Vue这个构造函数:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
​
# 定义Vue构造函数:
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)
}
​
# 在Vue.prototype原型上挂载方法
initMixin(Vue)  # 挂载 _init方法【重点】
stateMixin(Vue) // 挂载数据相关的方法:$watch、$set、$delete...
eventsMixin(Vue) // 挂载事件相关的方法: $on 、$once、$off、$emit...
lifecycleMixin(Vue) // 挂载生命周期相关的实例方法:_update、$forceUpdate、$destroy...
renderMixin(Vue)  // 挂载 $nextTick、_render 以及_o/_n/_l等等十几个快捷方法// 上面的方法挂载完毕之后,导出Vue,继续挂载全局API
export default Vue

这里我们知道每个初始化函数大概挂载了哪些方法就可以了,后面再详细介绍。

这里在定义了一个Vue构造函数之后,就开始向Vue.prototype原型对象挂载了很多的方法,我们依次来看挂载了哪些方法。

initMixin

挂载了一个_init初始化方法:

function initMixin (Vue: Class<Component>) {
    # 挂载了一个init初始化方法
    Vue.prototype._init = function (options?: Object) {
        ...
    }
}
stateMixin

挂载了一些操作数据的方法:

function stateMixin (Vue: Class<Component>) {
  ...
  
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  // $set $delete原理都是observer/index.js里面的set和del方法:set(defineReactive)、del(delete关键字)
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  Vue.prototype.$watch = function () {}
}
eventsMixin

挂载了一些绑定事件的方法:

function eventsMixin (Vue: Class<Component>) {
    // 绑定事件,循环向vm._events属性中添加事件
    Vue.prototype.$on = function () {}
    // 绑定一次性事件
    Vue.prototype.$once = function () {}
    // 解绑事件
    Vue.prototype.$off = function () {}
    // 触发事件
    Vue.prototype.$emit = function () {}
}
lifecycleMixin

挂载了一些组件更新卸载的方法:

function lifecycleMixin (Vue: Class<Component>) {
    # 组件更新的方法,【重点】
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
    // 强制组件更新
    Vue.prototype.$forceUpdate = function () {}
    // 组件销毁
    Vue.prototype.$destroy = function () {}
}
renderMixin

挂载了组件渲染的相关方法:

function renderMixin (Vue: Class<Component>) {
    # 挂载运行时的便利属性: vm.prototype._o/_n/_s;为render代码字符串运行时使用【重点】
    installRenderHelpers(Vue.prototype)
    // 挂载vm.$nextTick方法
    Vue.prototype.$nextTick = function (fn: Function) {}
    # 挂载组件渲染方法【重点】
    Vue.prototype._render = function () {}
}

Vue.prototype原型方法挂载完成之后,我们继续查看对Vue构造函数的处理。

src/core/index.js文件里面,我们找到了导出之后的Vue构造函数:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
# 挂载全局API (set、delete、nextTick、use、mixin、extend等多个方法)
initGlobalAPI(Vue)
​
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})
​
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})
​
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})
// 设置版本号
Vue.version = '__VERSION__'
​
# 导出vue
export default Vue

上面我们主要关注initGlobalAPI(Vue)这行代码,初始化全局API:

// src/core/golbal-api/index.js
​
# 挂载全局API
export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
​
  // 定义全局配置
  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
  }
  
  // 挂载全局的set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
​
  // 定义options属性
  Vue.options = Object.create(null)
  // 定义options对象下components/directives/filters属性, 存储全局组件/指令/过滤器
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
​
  // 设置了一个基础构造器; 标识“基本”构造函数以扩展所有普通对象 =====重点;Vue.options._base
  Vue.options._base = Vue
  // 注册keep-alive全局组件
  extend(Vue.options.components, builtInComponents)
​
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  // 挂载全局API Vue.component、Vue.filter、Vue.directive方法
  initAssetRegisters(Vue)
}

根据上面的代码我们可以发现,这里主要是对Vue构造函数自身挂载了setdeletenextTickusemixin等全局API。

所以我们new Vue()初始化之前,Vue构造函数已经挂载了初始化项目所需要的全局API与原型方法,这也是为什么我们可以在调用new Vue()初始化之前,就可以使用相应的全局API注册一些项目中使用到的资源 (注册插件/ui框架等)

import Vue from 'vue'
import App from './App.vue'// Vue.use(ElementUI);
# 查看Vue结构
console.dir(Vue)
​
// new Vue({
//   render: h => h(App)
// }).$mount('#app')

打印截图:

Vue构造函数:

image-20230403170058595.png

Vue.prototype原型对象:

image-20230403170230459.png

通过打印结果可以发现,在我们调用new Vue()初始化之前,Vue构造函数本身及原型对象就已经挂载了相关的全局API和原型方法。

4,new Vue()初始化

通过前面的代码解析,我们已经知道了Vue构造函数的由来及挂载内容,下面我们开始解析Vue应用的初始化过程:

function Vue (options) {
  this._init(options)
}
# Vue应用初始化
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app');
// this._init()

调用new Vue()初始化实际上就是内部调用了一个this._init()方法,要知道具体的初始化过程,我们就得查看_init方法内容:

// src/core/instance/index.js
# 应用初始化
Vue.prototype._init = function (options?: Object) {
    // 获取当前vue实例
    const vm: Component = this
    // 初始化实例Id
    vm._uid = uid++
    vm._isVue = true
    
    // 处理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
      )
    }
    vm._renderProxy = vm
​
    # 当new Vue() 执行后,触发的一系列初始化流程都是在 _init方法中启动的
    vm._self = vm
    initLifecycle(vm) // 初始化实例属性:初始化一些Vue上的实例属性($parent、$root、$refs、_isMounted、
    initEvents(vm) // 初始化事件:是指将父组件在模板中使用的v-on 注册的事件添加到子组件的事件系统(Vue.js的事件系统)中。
    initRender(vm) // 初始化一些渲染相关属性:(_vnode、$slots、$scopedSlots、_c、$createElement)
    # 触发beforeCreate钩子函数
    callHook(vm, 'beforeCreate')
    initInjections(vm) // 初始化inject
    initState(vm) // 初始化props、methods 、data 、computed 和watch
    initProvide(vm) // 初始化provide
    # 触发created钩子函数
    callHook(vm, 'created')
​
    // 如果用户在实例化Vue.js时传递了el选项,则自动开启挂载
    // 如果没有传递el选项,则无法挂载,且不会进入下一个生命周期流程 (需要手动调用vm.$mount进行挂载)
    # 开始应用加载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

init方法的内容很多,其实主要就关注这几点:

  • 合并options。(当前Vue应用实例没有需要合并的)
  • 初始化Vue应用实例的生命周期,event事件监听,状态选项propsmethodsdatacomputedwatch等【Vue根实例一般都不会传递这些配置参数】。
  • 执行vm.$mount进行应用的渲染与挂载。

扩展:我们要明确一点,Vue2的应用实例和组件实例定义是相同的,因为组件的构造函数继承至Vue构造函数,所以这里他们的初始化逻辑是相同的。而在Vue3中,Vue应用实例和组件实例是完全不相同的定义,所以初始化的逻辑也不相同。

这里的vm.$mount加载方法前面没有提及,实际上这个方法也是挂载到Vue.prototype原型对象上的:

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

这里我们只讲一下大概的过程:

以生产环境为例,所以我们的文件都是已经编译完成的,template模板都已经编译成了render代码字符串。

// 编译后的render函数
var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { attrs: { id: "app" } },
    [
      _c("button", { on: { click: _vm.handleButtonClick } }),
      _vm._v(" "),
      _c("HelloWorld", { on: { customEvent: _vm.handleCustomEvent } })
    ],
    1
  )
}

所以执行vm.$mount大致会经常几个流程:

  • 调用render()方法。
  • 生成vnode虚拟dom对象。
  • 调用update()方法。
  • 调用patch递归渲染。
  • 生成最终的dom
  • 完成页面渲染。

5,总结

最后我们再简单总结一下Vue应用的初始化过程:

  • 创建Vue构造函数,挂载实例方法与原型方法。
  • 注册全局资源(组件/指令/过滤器/插件等等)。
  • 调用new Vue({...}),创建Vue应用实例。
  • 内部执行this.Init方法,开始具体的初始化。
  • 执行$mount方法,开始应用加载。

对于Vue应用的初始化,我们掌握它的主要逻辑即可,一些细节过程我们在下节组件初始化中解析。