(封面图片来源于网络,侵删)
「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。
上一篇介绍了vue源码运行和调试步骤,这篇将探寻vue实例选项对象初始化相关内容。
一、测试文件创建
我们需要创建自己的测试文件,这里可以有下面两种方式,本文按照第二种方式进行测试,测试文件路径(vue-dev\examples\user-define\index.html),这里引入vue.js源码文件而不是vue.min.js压缩文件,是为了便于调试。
- 创建自己的demo文件夹,将上一篇文章中介绍到的打包后的dist目录下的vue.js文件引入到自己的文件中。
- 直接在源码文件夹的examples文件夹下创建自己的文件,同时引入dist目录下的vue.js文件。
二、选项|DOM
当一个vue实例创建之后,可以传入对应的选项对象。选项对象提供了对应的属性,以及想要创建的行为。根据API列表中的完整定义,可以一起来看下vue是如何进行初始化的。
在vue.js文件中,我们首先能够看到一个function Vue的函数,该函数定义了Vue对象。所有的vue都是从新建的vue实例开发的,因此我们在测试文件中,使用new Vue的形式进行Vue实例化。如果不使用new初始化,会有对应警告信息。
function Vue (options) {
if (
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
}
// index.html
<body>
<div id="app"></div>
<script>
new Vue({
el: '#app'
})
</script>
</body>
dom选项,提供了页面dom与vue相关联的一些属性和行为。
dom之el
-
el属性给Vue实例提供了一个挂载目标,即将页面定义的dom元素,作为根节点挂载到实例中。在index.html中传入el属性,查看Vue.js文件中Vue初始化函数,可看见调用了_init()方法,该方法为vue的初始化函数,在该函数中,el属性在mount函数中进行挂载。 挂载时,vue会首先判断template属性是否存在,如果template不存在,则直接会读取getOuterHTML函数,将挂载的DOM元素的html作为template,如果template存在,则该模板下的html元素将会替代el对应的元素。
// src/core/instance/init.js Vue.prototype._init = function (options?: Object) { if (vm.options.el) { vm.mount(vm.$options.el) // el挂载到vm实例上 } }
dom之template
- template属性,模板元素优先级别最高,如果存在该属性,则该属性会替代el属性的html内容当做模板。该属性以"#"作为标识,如果没有该"#",会报错。上图中展示的是,当页面初始化时,首先读取的dom元素中的内容,下图展示,定义了template属性后,该属性中的html内容会替代el对应dom中的元素。模板解析原理后续文章介绍
if (!options.render) {
if (template) {
...template存在时候,判断template是否以#开头,做相应处理
}
else if (el) {
// el属性时,读取el对应dom中的html元素作为模板
template = getOuterHTML(el);
}
```
if (template) {
// template存在,调用compileToFunctions函数进行模板解析
var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
}
}
dom之render
- render属性,提供一个createElement方法,生成对应的模板内容,该属性允许使用纯js编码创建页面模板。有render属性存在时,模板内容以render定义的为主。
三、选项|数据
挂载的dom元素有了之后,需要向该dom节点中,增加丰富内容,页面最主要的就是展现数据,同时对数据进行操作。因此有了数据data、props属性,以及数据交互(如监测、计算等)属性methods、computed、watch等。
数据之data
- data属性中定义了需要使用到的数据,该数据在定义时,就加入到了响应式系统中。对象必须是纯粹的对象,或者定义为函数返回对象的形式。通过_init()初始化函数的调试,我们发现在initState函数之后,vm中的data属性有的Observe选项,因此可以找到初始化数据选项的入口函数。
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm); // init数据选项初始化函数
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
在该函数中打断点进行调试,可看见该函数对props、methods、data、computed、watch分别进行了初始化。在initData函数中,主要对数据进行了初始化,同时将数据加入到了响应式系统中。响应式原理下一章再讲。
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); } // props初始化
if (opts.methods) { initMethods(vm, opts.methods); } // method初始化
if (opts.data) { // data初始化
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); } // computed初始化
if (opts.watch && opts.watch !== nativeWatch) { // watch初始化
initWatch(vm, opts.watch);
}
}
// initData函数中讲data属性加入到响应式系统中
observe(data, true /* asRootData */);
数据之props
- props属性,由于组件父子关系的存在,因此出现了props属性,该属性接受来自父组件传递的内容,该属性可以为数组或对象类型。在上面提到的initProps方法中,进行了props初始化操作。该方法中主要将props中对应的数据加入到observe响应系统中。
function initProps(vm, propsOptions) {
var loop = function ( key ) {
defineReactive(props, key, value, function () {
};
// props中数据属性
for (var key in propsOptions) loop( key );
}
数据之methods
- methods属性,定义了数据交互的方法,该methods整体作为对象定义。在initMethods进行初始化操作。在该函数中,会校验函数是否为function类型,同时会校验是否在props中定义过同名属性,最主要的是会将该对象中定义的方法全部绑定到vm实例中。
function initMethods() {
// vm实例绑定methods,以便后面直接通过this.methodsName的形式使用
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
数据之watch
- watch属性,是vue的侦听器,通过watch属性,可以自定义变化后的通用方法,同时可以在得到最终结果前,设置中间状态,相较于computed属性,watch能更加灵活的自定义异步函数。在initWatch方法中,将会调用$watch方法,遍历watch中的每一个属性,执行每个属性的回调函数。
function initWatch (vm, watch) {
// 创建watcher
createWatcher(vm, key, handler);
}
function createWatcher() {
// 调用$watcher回调,该函数存放在
return vm.$watch(expOrFn, handler, options)
}
数据之computed
- computed属性,在函数中定义了属性的getter、setter属性,同时将computed下的数据加入到vue实例中。这里有一个最重要的步骤,会给每一个computed属性,创建Watcher,便于监听其变化。同时会将computed属性的结果缓存,知道依赖的data属性变化才会对其进行重新计算。computed和watch的区别具体实现会在后续介绍。,这篇只介绍初始化内容。
function initComputed (vm, computed) {
for (var key in computed) {
// 读取属性,显式设置getter属性和不设置直接读取
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
// 将属性添加到Watcher中
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
// 定义computed,结果缓存
defineComputed(vm, key, userDef);
}
}
四、选项|生命周期钩子
在Vue.prototype.initMixin()中,对生命周期钩子函数也进行了初始化。其中通过调用callHook函数,执行生命周期钩子。下面的代码提供了寻找生命周期函数的思路,具体生命周期相关内容,可以根据生命周期函数图示进行查找,或者根据api生命周期函数描述来学习。
Vue.prototype._init = function (options) {
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate'); // 在数据初始化、监听之前,执行
initInjections(vm);
initState(vm);
initProvide(vm); // 在实例创建完成后被立即同步调用,$el属性还不能使用,
callHook(vm, 'created');
}
// 挂载el函数
function mountComponent () {
callHook(vm, 'beforeMount'); // 在挂载开始之前被调用
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted'); // el挂载
}
}
五、选项|资源
资源中定义的directives、filters、components提供了Vue全局以及局部创建的能力,该资源对象不在init函数中定义,而是在initGlobalApi函数中进行了初始化。通过向Vue实例挂载对应的资源属性,可全局提供Vue.directives()、Vue.filters()、Vue.components()方法。这三种方法在后续章节中介绍
本文主要针对init()初始化方法,介绍了Vue的实例选项对象初始化内容,通过初始化的过程,我们可以了解Vue选项对象的传参形式、生命周期的挂载节点、资源选项的作用等。下一章节,将介绍数据的响应式原理。