new Vue 发生了什么
在我的上一篇文章中《Vue源码精解_01_从构建开始》已经解释过,当我们执行new 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)
}
而Vue实际上是用Function实现的类,调用内部的this_init方法,而这个方法在上一篇讲过这个是执行initMixin(Vue)对Vue的扩展,在Vue.prototype上实现了_init方法。这个方法在core/instance/init.js文件中
export function initMixin(Vue: Class<Component>) {
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 */
// 开发阶段初始化 proxy
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);
}
};
}
我们逐一来分析上述代码
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);
}
首先缓存当前的上下文到vm变量中,方便之后调用。然后设置_uid属性,这个属性是唯一的。当触发init方法,新建Vue实例时(当组件渲染时也会触发)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);
}
上面这段代码主要是用来测试代码性能的,在这个时候相当于打了一个“标记点”来测试性能。但是只适用于开发模式和支持performance.mark API的浏览器
// 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
);
}
通过代码的注释我们可以知道,这是对options做一个合并,但是在合并前执行了vm._isVue = true乍看起来好像不太明白,好像是说防止当前实例被 observed实例化。我们可以简单看下observer的代码,之后研究响应式会详细讲解
export function observe (value: any, asRootData: ?boolean): Observer | void {
...
else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
...
}
如果传入值的_isVue为true时(即传入的值是Vue实例本身)不会新建observer实例(这里可以暂时理解Vue的实例不需要监听变化)。
再回到init源码部分
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
mergeOptions
如果当前的Vue实例是组件的话,就执行initInternalComponent方法。(这个方法的主要作用就是为vm.$options添加一些属性,后面讲到组件的时候再详细介绍)否则当前的操作是实例化Vue对象,因为我们从入口开始是通过new Vue实例化的对象,所以调用mergeOptions方法,这个方法接受三个参数。把vm.constructor给到resolveConstructorOptions调用得到的结果作为mergeOptions的第一个参数,并把options作为第二个参数,这个options是在 new Vue(options)的时候传进来的,然后传递给this._init(options)最终传递到mergeOptions里面,做合并。第三个参数是vm。那么这个方法是如何合并的呢?我们先来研究resolveConstructorOptions中的内容
function resolveConstructorOptions (Ctor) {
var options = Ctor.options;
if (Ctor.super) {
var superOptions = resolveConstructorOptions(Ctor.super);
var cachedSuperOptions = Ctor.superOptions;
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions;
// check if there are any late-modified/attached options (#4976)
var modifiedOptions = resolveModifiedOptions(Ctor);
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions);
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
if (options.name) {
options.components[options.name] = Ctor;
}
}
}
return options
}
这个方法要分成两种情况来说明。第一种是Ctor是基础Vue构造器的情况,另一种Ctor是通过Vue.extend方法扩展的情况
Ctor是基础Vue构造器
比如是通过new Vue创建的实例,Ctor其实就是基础的构造函数,直接返回options,我们在源码中调试输出
<script src="./dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
message: "hello world",
},
});
console.dir(vm.constructor.options);
</script>

那么这个options是在哪里定义的呢?它是什么时候跑到Vue上的?(vm.constructor --> Vue),我们应该如何去找这个options?从构建开始!入口文件的地址在web/entry-runtime-with-compiler.js,这个东西我在上一篇文章中已经讲过如何从构建开始找
这个文件里面有对Vue的第一层包装,但是这层包装里面没有options相关的内容,所以这个文件这里不展开讲,后面讲挂载的时候会详细说明。里面有这行文件引入
...
import Vue from "./runtime/index";
...
Vue构造函数的第二层包装就在这个文件里
...
import Vue from 'core/index' // 第三层包装
import platformDirectives from './directives/index'
import platformComponents from './components/index'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
...
// platformDirectives相关
// 这里导出Vue全局指令model,show
import model from './model'
import show from './show'
export default {
model,
show
}
// platformComponents相关
// 这里导出Vue全局组件Transition,TransitionGroup
import Transition from './transition'
import TransitionGroup from './transition-group'
export default {
Transition,
TransitionGroup
}
上面的代码主要是给Vue.options.directives添加model,show属性,给Vue.options.components添加Transition,TransitionGroup属性。那么还有filters,_base属性,以及components中的KeepAlive又是怎么来的呢?
这就要看Vue的第三层包装里都做了些什么?找到core/index.js,同样我们只看Vue.options相关代码。
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue)
...
那options就是这个时候跑到Vue上去的,initGlobalAPI在上一节带过,我们现在看看/global-api/index.js的代码。
...
import { initExtend } from "./extend";
import { ASSET_TYPES } from 'shared/constants'
...
export function initGlobalAPI (Vue: GlobalAPI) {
...
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
...
}
// shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
// core/components/index
import KeepAlive from './keep-alive'
export default {
KeepAlive
}
上面这层包装就把filters,_base和components中的KeepAlive都实现了。通过这三层包装,Vue构造函数的options对象就生成了
Ctor.super
通过Vue.extends构造子类时。Vue.extends方法会为Ctor添加一个super属性,指向父类构造器。(提一下,Vue3中已经将Vue.extends方法废弃,原因官网vue3文档有解释)
Vue.extend = function (extendOptions: Object): Function {
...
Sub['super'] = Super
...
}
所以当Ctor时基础构造器的时候,resolveConstructorOptions方法返回基础构造器的options。除了Ctor是基础构造器之外,还有一种是Ctor是通过Vue.extend构造的子类。这种情况比较复杂。Ctor上有了super属性,就会去执行if块内的代码。首先递归调用resolveConstructorOptions方法,返回"父类"上的options并赋值给superOptions变量,然后把"自身"的options赋值给cachedSuperOptions变量。然后比较这两个变量的值,当这两个变量值不等时,说明"父类"的options改变过了。例如执行了Vue.mixin方法,这时候就需要把"自身"的superOptions属性替换成最新的。然后检查是否自身的options是否发生变化。resolveModifiedOptions的功能就是这个
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
....
}
举个例子说明一下:
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>'
})
Vue.mixin({ data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}})
new Profile().$mount('#example')
由于Vue.mixin改变了"父类"的options。源码中superOptions和cachedSuperOptions就不相等了,就更新自身的superOptions属性。接下来执行resolveModifiedOptions
function resolveModifiedOptions (Ctor) {
var modified; // 定义modified变量
var latest = Ctor.options; // 自身的options
var sealed = Ctor.sealedOptions; // 构造"自身"时传入的options
for (var key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) { modified = {}; }
modified[key] = latest[key];
}
}
return modified
}
遍历当前构造器上的options属性,如果在"自身"封装的options里没有,则证明是新添加的。执行if内的语句。最终返回modified变量(即自身新添加的options)。
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
如果”自身“有新添加的options,则把新添加的options属性添加到Ctor.extendOptions属性上。调用mergeOptions方法合并"父类"构造器上的options和”自身“上的extendOptions
resolveConstructorOptions,它的主要功能是解析当前实例构造函数上的options,在解析完其构造函数上的options之后,需要把构造函数上的options和实例化时传入的options进行合并操作并生成一个新的options。mergeOptions具体的内容我打算放到组件化里面去分析,这里暂时知道又这个合并配置的操作。合并后vm.$options大致的内容如下:
vm.$options = {
components: { },
created: [
function created() {
console.log('parent created')
}
],
directives: { },
filters: { },
_base: function Vue(options) {
// ...
},
el: "#app",
render: function (h) {
//...
}
}
initProxy
接着合并配置之后,就会在开发阶段初始化代理
// 开发阶段初始化 proxy
if (process.env.NODE_ENV !== "production") {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
如果不是开发环境,则vue实例的_renderProxy属性指向vue实例本身
进到initProxy方法内
...
const hasProxy = typeof Proxy !== "undefined" && isNative(Proxy);
...
initProxy = function initProxy(vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options;
const handlers =
options.render && options.render._withStripped
? getHandler
: hasHandler;
// handlers 这个场景下 指向 hasHandler
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
export { initProxy };
首先判断当前环境是否支持 Proxy API,如果options上存在render属性,且render属性上存在_withStripped属性,则proxy的traps(traps其实也就是自定义方法)采用getHandler方法,否则采用hasHandler方法
接下来看看getHandler和hasHandler方法
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
warnNonPresent(target, key)
}
return target[key]
}
}
getHandler方法主要是针对读取代理对象的某个属性时进行的操作。当访问的属性不是string类型或者属性值在被代理的对象上不存在,则抛出错误提示,否则就返回该属性值。
该方法可以在开发者错误的调用vm属性时,提供提示作用。
const hasHandler = {
has (target, key) {
// 首先使用in操作符判断该属性是否在vm实例上存在。
const has = key in target
// 判断属性名称是否可用
const isAllowed = allowedGlobals(key) || key.charAt(0) === '_'
if (!has && !isAllowed) {
warnNonPresent(target, key)
}
return has || !isAllowed
}
}
hasHandler方法的应用场景在于查看vm实例是否拥有某个属性。比如调用for in循环遍历vm实例属性时,会触发hasHandler方法,方法中首先使用in操作符判断该属性是否在vm实例上存在。然后判断属性名称是否可用。
allowedGlobals的定义如下:
const allowedGlobals = makeMap(
'Infinity,undefined,NaN,isFinite,isNaN,' +
'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
'require' // for Webpack/Browserify
)
结合makeMap函数一起来看
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
const map = Object.create(null)
const list: Array<string> = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
分析makeMap,其作用是通过传入的string参数来生成映射表。如传入下列参数
'Infinity,undefined,NaN,isFinite,isNaN,'
....
通过makeMap方法可以生成下面这样的一个映射表
{
Infinity: true,
undefined: true
......
}
allowedGlobals最终存储的是一个代表特殊属性名称的映射表。
所以结合has和isAllowed属性,我们知道当读取对象属性时,如果属性名在vm上不存在,且不在特殊属性名称映射表中,或没有以_符号开头。则抛出异常。
最后回到initProxy代码中:
if (hasProxy) {
...
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
如果Proxy属性存在,则把包装后的vm属性赋值给_renderProxy属性值。否则把vm是实例本身赋值给_renderProxy属性
核心初始化
接着init源码分析:下面的几个初始化函数非常重要
// 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");
initLifeCycle
export function initLifecycle(vm: Component) {
const options = vm.$options;
// locate first non-abstract parent
let parent = options.parent;
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
vm.$refs = {};
vm._watcher = null;
vm._inactive = null;
vm._directInactive = false;
vm._isMounted = false;
vm._isDestroyed = false;
vm._isBeingDestroyed = false;
}
首先将mergeOptions后的vm.$options赋值给options变量
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
作者首先对这段代码提供了一行注释
locate first non-abstract parent
定位一个非抽象的父组件

抽象组件自身不会渲染一个DOM元素,也不会出现在组件的父组件链中
let parent = options.parent
if (parent && !options.abstract) {
...
}
当前vm实例有父实例parent,则赋值给parent变量。如果父实例存在,且该实例不是抽象组件。则执行下面代码
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
如果父实例parent是抽象组件,则继续找parent上的parent。直到找到非抽象组件为止。之后把当前vm实例push到定位的第一个非抽象parent的$children属性上。这样我们就说完了怎么找vm的parent属性。
之后我们回到initLifecycle继续往下看
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
这些代码都是为vm一些属性赋值。这些属性的作用如下表。


本文详细解析了newVue时Vue实例的创建过程,包括.uid的生成、性能标记、选项合并及组件初始化等关键步骤,展示了Vue作为构造函数的使用方式和内部核心初始化机制。
224





