问题背景
最近发现远程加载组件后,在组件初始化过程中,浏览器控制台报错:
于是我按照代码流程查看错误原因。找到报告错误的代码:
const originalVal = comp.props[key].default || '';
Comp 是一个在远程加载的单文件 Vue 组件之后编译并发布到 CDN 的组件。我们知道,Vue 文件的组件经过 vue-loader 处理后,一般都会变成这种数据结构的对象:
总体结构和我们平时写的组件选项类似。该对象在Vue中被处理为全局h函数后即可显示。不过这次查看从远程环境加载的对象后,会显示为:
如果你熟悉Vue,或许一眼就能看出问题所在。不过由于我平时比较关注Vue的响应能力以及它的各种接口API,所以我对Vue的整体理解有些欠缺。
问题原因:外部组件被别人更新了。更新后,组件不会写入默认导出选项;但默认导出 Vue.extend(options);
那为什么Vue.extend后外部加载后会报初始化错误,而开发者通过组件渲染自己测试却是正常的呢?
Vue 组件
要详细了解上述具体原因,需要对Vue组件有深入的了解。当我们谈论Vue组件化时,我通常会想到Vue.component,但很少使用Vue.extend。
Vue.extend原理分析
当我第一次看到扩展方法时,我其实觉得很奇怪。后来我正确理解后,觉得这个名字很贴切。
Java中的extends是一个关键字,意思是继承。这里的Vue.extend也有继承的意思。 extend 基于 Vue 构造函数创建一个新的构造函数
这里可以参考Vue在2.0.0版本中对extend方法的实现。其实整个扩展过程还是比较清晰的(可以参考代码中的小注释)。
主要通过Object.create方法实现原型的链式继承(使用Object.create,参见上一篇文章:链接),返回的Sub是一个构造函数,通过new方法调用后,它是通过调用Vue原型链中的_init方法来初始化的。后续流程与new Vue(options)创建Vue实例基本相同。
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var isFirstExtend = Super.cid === 0;
if (isFirstExtend && extendOptions._Ctor) {
return extendOptions._Ctor
}
var name = extendOptions.name || Super.options.name;
// 1. 定义extend返回对象
var Sub = function VueComponent (options) {
this._init(options);
};
// 1.1 通过Object.create方法进行原型链继承
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
// 2. 设置构造函数上的options, 用来存储配置
Sub.options = mergeOptions(
Super.options,
extendOptions
);
Sub['super'] = Super;
Sub.extend = Super.extend;
config._assetTypes.forEach(function (type) {
Sub[type] = Super[type];
});
if (name) {
Sub.options.components[name] = Sub;
}
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
if (isFirstExtend) {
extendOptions._Ctor = Sub;
}
// 3. 返回新的构造函数
return Sub
};
Vue.Component原理分析
注册组件时,Vue.component是经常使用的方法。一般来说,调用方法大家都很熟悉。简单介绍一下Vue 2.0.0版本的实现源码(去掉了一些不相关组件功能中的分支),方便和Vue.extend进行对比
Vue[type] = function (
id,
definition
) {
if (!definition) {
// 直接获取组件
return this.options[type + 's'][id]
} else {
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id;
// 定义组件的构造函数
definition = Vue.extend(definition);
}
this.options[type + 's'][id] = definition;
return definition
}
};
从Vue中的代码实现中可以得到以下信息:
- Vue.component 功能在 Vue.extend 的基础上进行了两次密封。获取组件的构造函数(Vue.extend 返回一个构造函数)后,该构造函数被放置在 Vue 的 options 属性上。
- 可以直接从Vue.component函数中获取对应的组件构造函数
- 可以通过Vue.component函数判断组件是否存在
为什么在Vue.extend(options)中更改测试项后仍然显示正常?
这是因为Vue使用h函数(createElement方法)时,会对传入的参数进行评估,然后进行分类处理。事实上,最终结果被转换为组件的构造函数。然而,我们的原生代码逻辑并不以构造函数的形式评估和处理组件,从而导致异常。
一般有两种解决方案:
- 标准化内部编写组件的方式,统一为一种格式,这样开发者写代码时会更清晰
- 借鉴Vue的做法,让两种组件的写法兼容
总结
Vue.extend方法创建组件的构造函数,调用new后可以获得组件的实例对象。
最重要的是弄清楚你平时写的选项和组件实例对象之间的关系。具体换算关系我们参考下图:
stateDiagram-v2
Options --> Vue
Options --> Vue.extend
Options --> Vue.component
Vue.component-->Vue.extend
Vue --> instance
Vue.extend --> instance
PS。以上是我个人对Vue组件的理解。有什么不合适的地方请告诉我~
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。