返回

专栏第四篇-初始化干了什么事

  上一节中我们知道 Vue就是一个构造函数,在使用 Vue的时候需要实例化 Vue,这一章我们就来看看在实例化的时候内部做了一些什么操作。

零、调用了_init方法

1
2
3
4
5
6
function Vue(){
if(!this instanceof Vue){
return warn("必须使用 new 关键字来实例化")
}
this._init();
}

除了在构造函数内部对是否使用 new 关键字进行判断。

还在校验过后调用了_init方法。

从 init 这个名字不难看出,肯定在内部做了一些初始化的操作。

该方法是在Vue被引入时调用 initMixin 方法在Vue上注入的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
let uid = 0;
export function initMixin(Vue){
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++

let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

// a flag to mark this as a Vue instance without having to do instanceof
// check
vm._isVue = true
// avoid instances from being observed
vm.__v_skip = true
// effect scope
vm._scope = new EffectScope(true /* detached */)
// #13134 edge case where a child component is manually created during the
// render of a parent component
vm._scope.parent = undefined
vm._scope._vm = 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 as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (__DEV__ && 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)
}
}
}

接下来我们会详细解释下每一步中具体都做了什么。

一、vm._uid

1
2
3
4
5
let uid = 0;

Vue.prototype.init=function(){
vm._uid = uid++
}

在_init方法的第一行可以看到在实例上挂载了一个vm._uid属性。

每个Vue实例都会有一个唯一的_uid(Unique Identifier,唯一标识符

这个标识符是在 Vue 实例创建时由一个递增的计数器生成的,其主要用途是在内部处理中提供唯一性。

尤其是在涉及到实例间的比较或者跟踪的时候。

二、追踪初始化消耗时间

1
2
3
4
5
6
7
8
9
10
11
12
// _init 函数刚执行时
let startTag, endTag
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// _init 函数初始化逻辑完成
if (__DEV__ && config.performance && mark) {
mark(endTag)
measure(`vue ${vm._uid} init`, startTag, endTag)
}

2.1 覆盖默认配置

我们知道在 Vue 可以覆盖默认配置。

1
2
// 开启 vue性能配置
Vue.config.performance = true;

那么 vue 是如何做到的呢?

core/config.js文件是vue的默认配置文件。
1
2
3
4
5
// core/config.js
export default {
// 默认不开启perf
performance: false
}

Vue被引入时,在 initGlobalAPI方法中给 Vue设置了 config属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import config from '../config'
export function initGlobalAPI(Vue){
// 省略部分代码
const configDef = {};
configDef.get = () => config;

if (__DEV__) {
configDef.set = () => {
warn(
'不要替换Vue.配置对象,而是设置单独的字段'
)
}
}

Object.defineProperty(Vue,'config',configDef);
}

而引入的config就是Vue的默认配置文件,其导出内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
export default {
/**
* Option merge strategies (used in core/util/options)
*/
// $flow-disable-line
optionMergeStrategies: Object.create(null),

/**
* Whether to suppress warnings.
*/
silent: false,

/**
* Show production mode tip message on boot?
*/
productionTip: __DEV__,

/**
* Whether to enable devtools
*/
devtools: __DEV__,

/**
* Whether to record perf
*/
performance: false,

/**
* Error handler for watcher errors
*/
errorHandler: null,

/**
* Warn handler for watcher warns
*/
warnHandler: null,

/**
* Ignore certain custom elements
*/
ignoredElements: [],

/**
* Custom user key aliases for v-on
*/
// $flow-disable-line
keyCodes: Object.create(null),

/**
* Check if a tag is reserved so that it cannot be registered as a
* component. This is platform-dependent and may be overwritten.
*/
isReservedTag: no,

/**
* Check if an attribute is reserved so that it cannot be used as a component
* prop. This is platform-dependent and may be overwritten.
*/
isReservedAttr: no,

/**
* Check if a tag is an unknown element.
* Platform-dependent.
*/
isUnknownElement: no,

/**
* Get the namespace of an element
*/
getTagNamespace: noop,

/**
* Parse the real tag name for the specific platform.
*/
parsePlatformTagName: identity,

/**
* Check if an attribute must be bound using property, e.g. value
* Platform-dependent.
*/
mustUseProp: no,

/**
* Perform updates asynchronously. Intended to be used by Vue Test Utils
* This will significantly reduce performance if set to false.
*/
async: true,

/**
* Exposed for legacy reasons
*/
_lifecycleHooks: LIFECYCLE_HOOKS
} as unknown as Config

根据上面的配置可以知道 Vue.config获取的就是config文件中中配置的内容。

export或export default一个对象时,对象本身在外部脚本中是不能修改的。但是对象的属性在外部脚本中都是可以修改的。

所以你可以通过Vue.config.xxx = xxx 来设置配置或者覆盖默认的配置。

2.2 mark函数 & measure函数

mark函数 和 measure函数是vue中进行性能检测的函数。

函数位于core/util/perf文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 判断是否在浏览器中
import { inBrowser } from './env'

export let mark
export let measure

// 只有在开发环境中才会设置 mark 和 measure
if (__DEV__) {
const perf = inBrowser && window.performance
if (
perf &&
perf.mark &&
perf.measure &&
perf.clearMarks &&
perf.clearMeasures
) {
mark = tag => perf.mark(tag)
measure = (name, startTag, endTag) => {
perf.measure(name, startTag, endTag)
perf.clearMarks(startTag)
perf.clearMarks(endTag)
}
}
}

实际上就是调用了window.performance相关的 api:

  1. performance.mark:主要用于创建标记
  2. performance.measure: 主要用于记录两个标记的时间间隔
  3. performance.clearMarks: 用于清除标记
1
2
3
4
5
6
7
8
9
window.performance.mark("_start")
for(let i=0;i<10000;i++){
console.log();
}
window.performance.mark("_end")
window.performance.measure("timestamp","_start","_end")

// 可以获取直接间隔
window.performance.getEntriesByName("timestamp")[0]

可以使用getEntriesByName获取两个标记期间代码执行的时间。

从而这里通过这个方法可以获取到初始化_init函数执行的时机。

2.3 总结

在 _init 函数开始时打一个名为“vue-perf-start”的标记。

然后在逻辑处理结束后打一个名为“vue-perf-end”的标记。

最后通过measure函数设置一个 measure 对象,里面包含两个标记之间的间隔时间。

这个间隔时间就可以看成这个实例初始化花费的时间,以此来评测性能。

三. vm._isVue

1
2
3
Vue.prototype.init=function(){
vm._isVue = true
}

每个 vue实例 在初始时都会设置_isVue变量。

这个变量可以在内部/扩展插件中判断当前对象是否是一个有效的 vue实例。

四、vm.__v_skip

1
2
3
Vue.prototype.init=function(){
vm.__v_skip = true
}

在初始化时将该变量设置为 true。

这个变量用于指示Vue的相应是系统跳过对该对象的观测。

当一个对象被标记为 __v_skip = true 时,Vue 不会对这个对象进行深度观测,这意味着对象内部的属性变化将不会触发视图更新。

五、合并选项

1
2
3
4
5
6
7
8
9
10
11
12
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 as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}

第五步是执行合并 options的操作,那么这个 options是什么呢?可以说 Vue中的大部分内容都是基于 options来完成的,这个 options中存储了用户传入的选项以及一些内置的 Vue选项。

由于options在 Vue中比较重要,关于这一块我们后面会专门开一篇文章来详细解释。

在这篇专栏中具体说明了合并选项:第五篇专栏

六、设置数据代理检测

1
2
3
4
5
if (__DEV__) {
initProxy(vm)
}else {
vm._renderProxy = vm
}

这里的目的是在开发环境下给vm设置代理,从而达到开发时提示的功能,这个我们后续也会单开一篇文章进行说明。

七、vm._self = vm

1
2
3
Vue.prototype.init=function(){
vm._self = vm
}

将_self属性指向自身这个实例。

可以确保在任何情况下都能正确引用当前实例。

八、执行一些初始化操作

后面我们会执行一些用于初始化的操作。

内部具体做了什么我们咱不考虑,在后面的章节中我们会一一进行讨论。

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化生命周期
initLifeCycle(vm);
// 初始化事件中心
initEvents(vm)
// 初始化渲染
initRender(vm)
// 初始化 inject
initInjections(vm)
// 初始化 props、data、computed 等
initState(vm)
// 初始化 provider
initProvide(vm)

这些函数基本上都是在实例上挂载了一些方法,比如渲染执行的_update就是在 initLifecycle中定义的、跟渲染相关的_render函数就是在 initRender中定义的。

九、执行一些生命钩子

在执行 initRender 后,会调用 beforeCreate 钩子。这表示beforeCreate 钩子 调用的时机为Vue实例化 data数据前,所以此时获取 data是无法获取到的,因为这时候还没有初始化 data。

在执行完最后一个初始化函数initProvide后,会调用 created钩子。这表示created钩子 调用的时机为Vue实例初始化后。

1
2
3
4
5
6
// 省略
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
// 省略
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

十、执行挂载

1
2
3
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}

判断是否有vm上是否有$options选项,如果存在$options选项,则执行挂载方法。

十一、总结

在 init函数内部,主要是执行一些初始化操作,比如设置基础的代理。初始化data、给实例上设置_update、_render方法等。


本站总访问量