博客更新地址啦~,欢迎访问:https://jerryyuanj.github.io/
在上一节分析了,Vue的构造函数中,只有一句this._init(options)
,可见这行代码的重要性。今天我们就来详细的看看这个函数主要干了什么事。为了不那么抽象,我会用一个简单的例子来贯穿整个讲解。这个例子非常简单。
var app = new Vue({
el: '#app',
data(){
return {
name: 'hello'
}
}
})
如上一节所说,这个 _init
方法在 initMixin
中。Ok,我们来看看:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// 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 {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 响应式入口
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
首先,把当前实例(this
)赋给vm
变量,在给这个实例的uid
自增。接下来我们会看到这么些东西,
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
....一些逻辑....
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
...其他逻辑...
不难看出,两段 if 的作用,就是计算中间包裹的逻辑代码的性能的。这个我们在以后的分析中会直接跳过,有时间会单独写一节来讲解。这不影响我们分析逻辑代码。
我们继续,然后到了这句代码:
vm._isVue = true
通过注释可以看到,这个_isVue
标记是为了不让vue的响应式系统观测它。后面说到响应式原理的时候会遇到。
接着我们就会碰到第一个比较复杂且重要的逻辑了:
// 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.
// 优化内部组件的实例化过程
// 因为动态的 options 合并是很慢的, 而且内部组件也不需要这些合并操作
initInternalComponent(vm, options)
} else {
// 对 $options 赋值
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
这段逻辑主要是合并options。结合上面的例子,此时我们的:
options = {el: '#app', data(){return {name: 'hello'}}
所以此时走的是 else
分支。那么if分支中是做什么的呢?它判断了_isComponent
属性,结合注释,它应该Vue内部处理组件实例化的。先不管,继续走我们的逻辑。此时应该是给 vm.$options 赋值了,但是这个赋值过程是通过一个mergeOptions
函数来实现的。这个函数很重要。我们来看看这个函数的实现。
建议你泡杯咖啡☕️或者放松放松,下面的函数包含的逻辑还是挺多的,以防你看一半不想看了,hahah。
该函数位于:src/core/util/options.js
先看下代码:
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*
* 将两个option对象合并到一个新的对象中
* 核心功能,在初始化和继承中继承
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
这是个核心功能,在vue的实例化和继承的时候都会用到。它接受三个参数,一个 parent对象, 一个 child对象, 一个vue实例vm。返回一个options对象。我们一行一行来看。
首先,在非生产环境下,先检查 child 对象的名称。
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
这个checkComponents()
方法就在当前文件中,实现很简单,看看:
/**
* Validate component names 检查组件的名字
*/
function checkComponents (options: Object) {
// 遍历options中的所有components, 检查它们的名称是否合法
for (const key in options.components) {
validateComponentName(key)
}
}
export function validateComponentName (name: string) {
// 从警告中我们可以看到,组件的名称必须是只能含有如下的内容:数字,短横线(-),字母
// 并且组件名称必须以字母开头
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
)
}
// 不能是内置标签或保留字
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
)
}
}
在本例中,我们的child没有components,所以这里也就不需要检查了。继续往下看:
if (typeof child === 'function') {
child = child.options
}
我们当前的例子也不满足,因为我们的child此时是object,所以也不需要管。
接下来是三个normalize:
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
normalize是什么意思呢?规范化。为什么要规范化?因为它提供了多种配置方式供开发者使用,内部想要统一处理就必须要做规范化。这也是一种编程思想。我们先来分析这三个normalize方法。
拿我们熟悉的props来举例子,下面的方式都是正确的配置:
props: ['name']
props: {
name: String
}
props: {
name: {
type: String,
required: true
}
}
如果vue内部要用到这个props,那么它该如何确定props的类型呢?是的,几个if…else的确能解决问题,但是如果配置的方式多了,或者处理这个props的地方多了,对于维护来说,是非常可怕的。所以,最好的方式是将他们处理成相同的数据格式,一劳永逸。这就是要介绍的 normalize。我们先看对props的normalize:
/**
* Ensure all props option syntax are normalized into the
* Object-based format.
*
* 确保所有的 props 选项语法都被规范化成对象格式的
*/
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
// props 是数组的情况
if (Array.isArray(props)) {
i = props.length
// 遍历数组
while (i--) {
val = props[i]
// 如果数组中的某一项是字符串的话,名字转成驼峰式的
// 接着给 res 添加该属性,默认的type=null
// 即: ['first-name', 'last-name'] 会被转成
// {firstName: {type: null}, lastName: {type: null}}
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
// 如果数组中某项不是字符串的话会报错
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
// props 是对象的情况
} else if (isPlainObject(props)) {
// 遍历key
for (const key in props) {
// 拿到值
val = props[key]
// 把key驼峰化
name = camelize(key)
// 如果值是对象的话,就用它作为值;如果不是,设置 {type: null}
// 如上面的第二个例子:props: {name: String} 走的就是第二个条件
// 第三个例子:props:{name: {type: String}} 就是使用自己
res[name] = isPlainObject(val)
? val
: { type: val }
}
// 都不满足,警告
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
// 把最终规范化的结果赋给 options.props
options.props = res
}
注释写的很详细了,对于上面的三种情况,最终会分别规范化成这样的对象格式:
props: { name: {type: null} }
props: {name: {type: String}}
props: {name: { type: String, required: true }}
再来看看对inject的规范化再来看看对inject的规范化normalizeInject(关于inject/provide可以参考https://cn.vuejs.org/v2/api/#provide-inject)
/**
* Normalize all injections into Object-based format
*/
function normalizeInject (options: Object, vm: ?Component) {
// 先缓存下这个 inject 对象
const inject = options.inject
if (!inject) return
// 再将这个 inject 对象置空,并且赋值给 normalized 变量
// 这样做的好处是,此时 normalized 和 options.inject 都指向同一个对象,
// 下面对 normalized 的操作实际上就是对 options.inject 的操作了
const normalized = options.inject = {}
// 开始对一开始缓存的 inject 对象做判断了
if (Array.isArray(inject)) { // 数组
for (let i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] }
}
} else if (isPlainObject(inject)) { // 对象
for (const key in inject) {
const val = inject[key]
normalized[key] = isPlainObject(val)
? extend({ from: key }, val) // 如果值是一个对象的话,给它添加一个from属性
: { from: val }
}
} else if (process.env.NODE_ENV !== 'production') { // 如果不是数组或对象的话,报个错
warn(
`Invalid value for option "inject": expected an Array or an Object, ` +
`but got ${toRawType(inject)}.`,
vm
)
}
}
这里要结合 inject/provide 的文档来看了,inject
的值可以是数组或者对象,跟我们的props
差不多。所以两个判断分支做的事情也差不多。这里我们就直接以两个例子看看:
inject: ['foo', 'bar']
inject: {
foo: { default: 'hello' }
}
第一个很显然走的是数组的分支,在循环遍历数组以后,给normalized
弄成了这样子(正如在注释中提到的,这里的normalized
对象就是inject
对象),所以我们的inject
对象就成了:
inject: { 'foo': {from: 'foo'}, 'bar': {from: 'bar'} }
第二个例子走的是对象的分支,就会给我们的inject
变成:
inject: { foo: {from: 'foo', default: 'hello'}}
怎么样,是不是跟props差不多,没什么复杂的地方。
最后再看看 normalizeDirectives ,规范化指令。对比上面两种,这个就相对简单些了:
/**
* Normalize raw function directives into object format.
*/
function normalizeDirectives (options: Object) {
const dirs = options.directives
if (dirs) {
for (const key in dirs) {
const def = dirs[key]
if (typeof def === 'function') { // 会把函数定义的指令转成对象形式
dirs[key] = { bind: def, update: def }
}
}
}
}
可以看到,代码很简单,我们还是以一个例子来看看:
directives: {
focus: {
inserted(){...},
update(){....}
},
remove(){....}
}
可以看到,上面的directives
定义了两个指令,一个focus,一个remove。其中focus是以对象形式定义的,remove是以函数形式定义的。经过normalizeDirectives
处理后,会把以函数形式定义的规范化成以对象形式定义的,也就是把这个函数作为bind钩子和update钩子的回调
:
directives: {
focus: {
inserted(){...},
update(){....}
},
remove:{
bind(){...}
update(){...}
}
}
Ok,三个normalize介绍完了,没有想象中那么难理解吧。在本例中,由于不想在分析的时候弄的太复杂,就没有定义props, injects, directives这些选项,但是上面的三段分析,对我们理解 Vue 在处理这些属性时的内部机制是很有帮助的,所以才花了这么多篇幅介绍。
好了,normalize完以后,接下来到了这一段代码:
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
这段代码主要是用来处理 extends
和 mixin
的,递归调用自身(mergeOptions方法)。没有什么好说的。而且本例中,child._base = Vue
, 所以这段逻辑是不会进来的。以后分析到的时候再说。
总结:本节主要介绍了Vue在初始化的时候的一个重要的步骤,选项规范化(normalize)。它的主要作用就是将Vue暴露给开发者的props, injects, directives的配置方式,在内部做了一个统一处理——都转成对象。这样不论在内部操作这些属性的时候,就可以按照一致的标准去处理,而不用分情况来处理不同风格的配置。
(本来打算这节把merge options的内容也放进来,但是写到后面发现太多了,就放到下一节了)