乐趣区

Vue源码解析(一): 创建vue程序的背后发生了什么

主要大纲:

从 initGlobalAPI 方法看 Vue.config 全局配置
寻根问祖 -Vue 的构造函数的出生地

先来一段最常见的 vue 代码 demo
<div id=”app”>
{{message}}
</div>
// js
var vm = new Vue({
el: ‘#app’,
data: {
message:‘hello vue’
}
})

上面已经创建了一个 vue 应用程序;从上面很容易就看出来 Vue 是一个构造器,vm 是用这个构造器构造出来的实例化对象,实例化的时候传入了参数,参数中包括 el 和 data 上述延伸了 3 个问题:

Vue 构造器是什么模样?
Vm 可以使用的方法,即 vue 的开放 API 都在源码里面怎么实现的?
我们传入构造方法内的参数发生了什么

这些问题是我们解锁 vue 源码的最开始的步骤,所以我们不妨通过 vue 源码的入口开始寻找这些源码的实现

在源码的 src/platforms/web 下面放着不同版本的构建 entry 文件,这些文件中导出 export 的 Vue,都是从 src/core/instance/index 这个文件 import 过来的,我们先看下入口文件能带给我们什么答案:
// src/core/instance/index
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’
initGlobalAPI(Vue)

这个入口文件做了 3 件事:

引用了 ./instance/index, 暴露了 vue 的来源,即构造器
调用 initGlobalAPI 方法,将 vue 传进去,给 vue 拓展了全局静态方法

将 vue 暴露出去

这个入口文件的意义,是在暴露 vue 之前,给 vue 通过 initGlobalAPI 方法给 vue 拓展了全局静态方法,对应 Vue 的外部 API 是 Vue.config,包含了 Vue 的全局配置,
Vue.config.silent // 日志与警告
Vue.config.errorHandler // 这个处理函数被调用的时候,可以获取错误信息和 Vue 实例
Vue.config.devtools // 配置是否允许 vue-devtools 检查代码
…..

从 initGlobalAPI 方法看 Vue.config 全局配置
initGlobalAPI 方法定义了 configDef 对象,它的 getter 方法会的属性值是 config,setter 方法给出警告不允许修改。最后在 vue 上添加了 config 属性,属性描述返回 configDef 对象
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== ‘production’) {
configDef.set = () => {
warn(
‘Do not replace the Vue.config object, set individual fields instead.’
)
}
}
Object.defineProperty(Vue, ‘config’, configDef) // 添加 config 属性

除此之外,还定义了 util 属性,但是并没有暴露到外面,也并不建议外部去使用
寻根问祖 -Vue 的构造函数的出生地
了解了构造函数,也就知道了 new vue() 的时候发生了什么下面这段代码就是 Vue 的构造方法,我们可以直观的看出 vue 构造器是使用 ES5 的 Function 去实现类,是因为可以通过 prototype 往 vue 原型上拓展很多方法,把这些方法拆分到不同的文件 / 模块下,这样更有利于代码的维护,与协同开发比如在这个文件中,可以看到把 Vue 当作一个参数传进下面的 **Mixin 方法中,这些方法都是通过接收 vue,在它的 prototype 上面定义一些功能的;
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’)
// vue 必须是 new vue() 的实例化对象
}
console.log(‘options’, options)

this._init(options) // 调用内部_init 方法
}
initMixin(Vue) // 在 created 生命周期函数之前的操作
stateMixin(Vue) // 利用 definedProperty 进行静态数据的订阅发布
eventsMixin(Vue) // 实例事件流的注入, 利用的是订阅发布模式的事件流构造
lifecycleMixin(Vue) //
renderMixin(Vue) // 实现 _render 渲染虚拟 dom
export default Vue

这个构造函数的最核心点, 就是 this._init(options)
在此处打断点,可以看到参数 options 传进来的就是外面我们实例化时传入的参数 el 和 data
new Vue({
el: ‘#app’,
data: {
message:‘hello vue’
}
})

这个_init 方法出自 initMixin 函数看完这个函数,我们梳理出整个初始化阶段源码的几个重要的节点

初始化 options 参数进行合并配置
初始化生命周期
初始化时间系统
初始化 state, 包括 data、props、computed、watcher

export function initMixin (Vue: Class<Component>) {
console.log(‘Vue’, Vue)
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid 实例化的 uid 递增 1
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */

// 用_isVue 来标识当前的实例是个 Vue 实例,这样做是为了后续被 observed
vm._isVue = true
// 合并配置 options, 并判断是否是内部 Component 的 options 的初始化
if (options && options._isComponent) {
// 内部
initInternalComponent(vm, options)
} else {
// 非内部
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 在 render 中将 this 指向 vm._renderProxy
if (process.env.NODE_ENV !== ‘production’) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化生命周期
initLifecycle(vm)
// 初始化事件注册
initEvents(vm)
// 初始化渲染
initRender(vm)
// 触发回掉函数中的 beforeCreate 钩子函数
callHook(vm, ‘beforeCreate’)
initInjections(vm) // resolve injections before data/props
// 初始化 vm 的状态, 包括 data、props、computed、watcher 等
initState(vm)
initProvide(vm) // resolve provide after data/props
// vm 已经创建好来,回掉 created 钩子函数
callHook(vm, ‘created’)
/* istanbul ignore if */

// 将实例进行挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}

退出移动版