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;
vm._uid = uid++;
let startTag, endTag;
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`;
endTag = `vue-perf-end:${vm._uid}`;
mark(startTag);
}
vm._isVue = true;
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
if (process.env.NODE_ENV !== "production") {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm);
initState(vm);
initProvide(vm);
callHook(vm, "created");
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;
vm._uid = uid++;
let startTag, endTag;
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;
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的浏览器
vm._isVue = true;
if (options && options._isComponent) {
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) {
Ctor.superOptions = superOptions;
var modifiedOptions = resolveModifiedOptions(Ctor);
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'
...
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
...
import model from './model'
import show from './show'
export default {
model,
show
}
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)
...
}
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
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) {
Ctor.superOptions = superOptions
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;
var latest = Ctor.options;
var sealed = Ctor.sealedOptions;
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
接着合并配置之后,就会在开发阶段初始化代理
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) {
const options = vm.$options;
const handlers =
options.render && options.render._withStripped
? getHandler
: 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) {
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'
)
结合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 源码分析:下面的几个初始化函数非常重要
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm);
initState(vm);
initProvide(vm);
callHook(vm, "created");
initLifeCycle
export function initLifecycle(vm: Component) {
const options = vm.$options;
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 变量
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 一些属性赋值。这些属性的作用如下表。
initEvents
initRender
callHook
initInjections
initState
initProvide
callHook
|