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