初始化
这篇主要分析一下Vue的初始化过程,当我们引用构建文件或者使用脚手架构建项目后,入口文件会创建一个Vue实例,前提是执行了import Vue from 'vue'后(初始化Vue后):
var vm = new Vue({
// 选项
})一个 Vue 应用由一个通过 new Vue 创建的根 Vue实例,以及可选的嵌套的、可复用的组件树组成。
那么这里new Vue的过程主要做了什么呢,接下来一一分析。
Vue构造函数
这里先说一下,我们怎么从源码中一步步找到Vue构造函数。在前置准备中有提到过 Vue 的 build 构建过程,/scripts/config.js是构建的配置文件,因为这里分析的是umd的完整版vue.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
import Vue from 'core/index'以此类推跳转,最终在/core/instance/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)
}Quiz Time👇
为什么使用function 构造函数,而不使用 class?
✅ 这是为了方便后续给vue实例混入实例成员,直接操作原型 prototype,实例成员包含实例方法和属性, 使用 class 不能实现吗?其实是一样的,只是没必要再用语法糖去实现。也不需要实现继承的功能场景。
实例成员初始化
从上面可以得出Vue的本质就是一个构造函数,当new Vue()时,实际上调用了Vue的构造函数来实例化对象,其中调用了成员方法__init,这个__init怎么来的,下面会分析。
现在集中这个instance/index文件,除了定义Vue构造函数之外,还调用了一系列的混入操作来扩展 Vue 的功能,实际上是在不同文件里定义原型上的方法。
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等。
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模块中的的内容都非常关键,涉及到之后将要分析的响应式原理,先大概看一下实现:
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
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注册事件为例,
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
// VNODE渲染成真实的dom,首次渲染和数据更新时都会调用
Vue.prototype._update = function(){}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function(){}renderMixin
Vue.prototype.$nextTick = function(){}
Vue.prototype._render = function(){}静态成员初始化
刚才我们分析了instance/index文件中Vue实例成员的初始化,那么core/index.js中实现的是Vue静态成员的初始化,通过initGlobalAPI(Vue)实现。
静态属性Vue.config
js// 初始化 Vue.config 对象 Object.defineProperty(Vue, 'config', configDef)Vue.options
jsVue.options = Object.create(null) //扩展为components directives filters //用来存储全局的组件,指令,过滤器 ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) })静态方法
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)- 注册与平台相关的全局组件,
v-transition,v-transition-group
extend(Vue.options.components, platformComponents)- 全局方法:
Vue.prototype.__patch__,位置:(runtime/index.js)
// 虚拟dom转换成真实dom
Vue.prototype.__patch__ = inBrowser ? patch : noopVue.prototype.$mount,位置:(runtime/index.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 的内部实现即可明白。
/**
* 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
}
}