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
一些属性赋值。这些属性的作用如下表。