wyseven / blog Goto Github PK
View Code? Open in Web Editor NEW写写博客,总结经验,会写一系列文章,目前正在努力写作中。喜欢的话请点star。
写写博客,总结经验,会写一系列文章,目前正在努力写作中。喜欢的话请点star。
在这篇文章中,我们将介绍标准的 JavaScript 模块,目前是如何在前端应用程序中使用的,以及未来我们可能会如何使用它们。
JavaScript 模块有时被称为 ESM,它代表 ECMAScript 模块。
JavaScript模块是构造 JavaScript 代码的一种方法。模块中的代码与其他模块中的代码是隔离的,并且不在全局范围内。
<script>
function hello() {
console.log("hello Bob");
}
</script>
<script>
function hello() {
console.log("hello Fred");
}
</script>
<script>
hello(); // outputs hello Fred
</script>
上面的代码定义了两个函数,没有使用模块,在全局作用域会产生冲突。
JavaScript 模块解决的另一个问题是不必担心 HTML 页面上脚本元素的顺序:
<script>
hello(); // 💥 - Uncaught ReferenceError: hello is not defined
</script>
<script>
function hello() {
console.log("hello");
}
</script>
在上面的示例中,定义 hello 函数的脚本元素需要放在调用 hello 函数的脚本元素之前。 如果有很多这样的 Javascript 文件,就很难管理了。
JavaScript 模块语法是在 ES6 中引入的,通常在我们今天构建的应用程序中使用,如下所示:
import React from 'react';
...
export const HomePage = () => ...
上面的示例导入 React 模块并导出 HomePage 组件。
不过,这段代码并没有使用 JavaScript 模块。取而代之的是,Webpack 将其转换为非原生模块,而采用了 IIFE(立即调用函数表达式)来做 。
值得注意的是,Webpack 确实有一个实验性的 outputModule 特性,允许它以原生模块格式发布。希望 Webpack 5 中包含这个功能!
要声明一个引用 JavaScript 模块代码的脚本元素,需要将类型属性设置为module:
<script type="module">
import { hello } from "/src/a.js";
hello();
</script>
这是 src 文件夹中 a.js 中的 JavaScript:
// /src/a.js
import { hellob } from "/src/b.js";
import { helloc } from "/src/c.js";
export function hello() {
hellob();
helloc();
}
因此,在 a.js 中的 hello 函数,调用了在 b.js 中调用 hellob,在 c.js 中调用 helloc。
这是来自 b.js 和 c.js 的 JavaScript:
// /src/b.js
export function hellob() {
console.log("hello b");
}
// /src/c.js
export function helloc() {
console.log("hello c");
}
请注意,我们需要提供要导入的文件的完整相对路径,并且还需要包含文件扩展名。
我们可能更习惯于一个简单的导入说明符,如下所示:
import { hello } from "a";
稍后我们将再次介绍原生的导入说明符。
还请注意,我们不必在 **HTML **文件中声明所有模块。
浏览器在运行时会去解析它们。
需要注意的是,不能从普通脚本元素使用 JavaScript 模块。
例如,如果我们尝试不使用 type 属性,脚本元素将不会被执行:
<script>
// 💥 - 不能在模块外部使用import语句
import { hello } from "/src/a.js";
hello();
</script>
用 JavaScript 模块编写的代码在默认情况下以 严格模式 执行。
所以没有必要在代码顶部使用 use strict:
<script type="module">
let name = "Fred";
let name = "Bob"; // 💥 - Identifier 'name' has already been declared
</script>
让我们以前面的类似示例为例,其中有 JavaScript 模块 a,b,c。模块 a 依赖于模块 b 和 c。模块 b 和c没有依赖关系。
假设 c.js 中包含运行时错误:
export function helloc() {
consol.log("hello c"); // 💥 - Uncaught ReferenceError: consol is not defined
}
从 HTML 文件调用代码的方法如下:
<script type="module">
import { hello } from "/src/a.js";
hello();
</script>
在 a.js文件中:
import { hellob } from "/src/b.js";
import { helloc } from "/src/c.js";
export function hello() {
hellob();
helloc(); // 💥
hellob(); // never executed 从未执行
}
正如我们所预料的那样,第二次调用 hellob 时永远不会被调用。
如果c.js中的问题是编译错误:
// 注:错写了 function 这个单词
export functio helloc() {
console.log("hello c");
}
模块中没有代码被执行:
<script type="module">
// 💥 - Unexpected token 'export'
// no code is executed
import { hello } from "/src/a.js";
hello();
</script>
其他模块中的代码可以正常执行。[注:再有一个script 设置 type 为 module,可以正常执行,并不会受报错模块的影响,因为每个模块是独立的,没依赖关系互不受影响]
_
所有的现代浏览器都支持原生模块,但不幸的是,IE不支持。
但是,有一种方法可以让我们在支持原生模块的浏览器上使用它们,并为不支持它们的浏览器提供一种退路。
使用 script 元素上的 nomodule 属性来实现这一点:
[注:设置了 nomodule,在支持原生模块的浏览器中不执行,可用于在不支持模块化JavaScript的旧浏览器中提供回退脚本]
<!--支持原生模块浏览器执行-->
<script type="module" src="app.js"></script>
<!--不支持原生模块浏览器执行-->
<script nomodule src="classic-app-bundle.js"></script>
Rollup,可以很好地输出 ES 模块文件和非 ES 模块文件:
export default [{
...
output: {
file: 'bundle-esm.js',
format: 'es'
}
},{
...
output: {
file: 'bundle.js',
format: 'iife'
}
}];
让我们看看一个示例,其中有引用来自 CDN 的模块:
<script type="module">
import intersection from "https://cdn.jsdelivr.net/npm/[email protected]/intersection.min.js";
console.log(intersection([2, 1], [2, 3]));
</script>
模块依赖于其他模块,而这些模块又依赖于其他模块。因此,在执行脚本元素之前,会下载并解析所有依赖项。
JavaScript 模块可以预加载使用 modulepreload 资源提示:
<link
rel="modulepreload"
href="https://cdn.jsdelivr.net/npm/[email protected]/intersection.js"
/>
即在其他模块下载之前先下载并解析此模块:
目前只有 Chrome 和 Edge 支持预加载模块。**Firefox **和 Safari 将对模块正常下载。
动态导入 是在运行时可以根据不同条件在其中导入代码:
<script type="module">
if (new Date().getSeconds() < 30) {
import("/src/a.js").then(({ helloa }) =>
helloa()
);
} else {
import("/src/b.js").then(({ hellob }) =>
hellob()
);
}
</script>
这对于某些使用率较低的大型模块很有用。这也可以减少浏览器中应用程序的内存占用。
回到我们如何在import语句中引用模块:
import { hello } from "/src/a.js";
import intersection from "https://cdn.jsdelivr.net/npm/[email protected]/intersection.min.js
如果我们仔细想想,除非指定模块的完整路径,否则浏览器怎么知道在哪里找到它呢?
所以,语法是有意义的,即使我们不习惯它。
有一种方法可以将导入说明符与一个被提议的称为导入映射(import-maps)的特性一起使用。
这是一个在特殊的 importmap 脚本元素中定义的映射,需要在引用模块的脚本元素之前定义:
<script type="importmap">
{
"imports": {
"b": "/src/b.js",
"lowdash-intersection": "https://cdn.jsdelivr.net/npm/[email protected]/intersection.min.js"
}
}
</script>
为每个依赖模块提供一个纯导入说明符名称。然后可以在 import 语句中使用定义好的说明符:
<script type="module">
import { hellob } from "b";
hellob();
import intersection from "lowdash-intersection";
console.log(intersection([2, 1], [2, 3]));
</script>
目前,导入映射在浏览器中不可用。但是,此功能可通过以下实验性标记在 Chrome 中使用:chrome:// flags /#enable-experimental-web-platform-features
**
重要的一点是,库必须发布为原生模块格式,以便开发者将库用作原生模块使用。不幸的是,目前这种情况并不常见。例如,React尚未发布为原生模块。
与 IIFE 模块之类的模块相比,原生模块的一个好处是需要下载到浏览器、解析然后执行,相对来说代码更少。
原生模块也可以并行地、异步地下载和解析。因此,原生模块对于大型依赖树可能执行得更快。
此外,预加载模块可能意味着用户可以更快地与页面交互,因为这些代码是从主线程中解析出来的。
除了性能上的提高之外,新的浏览器特性还可能构建在模块之上,因此使用原生模块是一种验证未来的代码。
当前最流行的浏览器都可以使用本机模块,并且可以为IE提供备份。Rollup已经可以以这种格式发布了,而且Webpack支持似乎正在进行中。现在,我们所需要的是更多的库开始以原生模块发布。
github博客地址:https://github.com/WYseven/blog,欢迎star。
如果对你有帮助,请关注【前端技能解锁】:
本文主要为三方面的内容:
学习的目的是对装饰者模式模式有进一步的理解,并运用在自己的项目中;对TypeScript装饰器的理解,更好的使用装饰器,例如在 nodejs web 框架中、 vue-property-decorator 中,或者是自定义装饰器,能熟练运用并掌握其基本的实现原理。
装饰者模式(Decorator Pattern)也称为装饰器模式,在不改变对象自身的基础上,动态增加额外的职责。属于结构型模式的一种。
使用装饰者模式的优点:把对象核心职责和要装饰的功能分开了。非侵入式的行为修改。
举个例子来说,原本长相一般的女孩,借助美颜功能,也能拍出逆天的颜值。只要善于运用辅助的装饰功能,开启瘦脸,增大眼睛,来点磨皮后,咔嚓一拍,惊艳无比。
经过这一系列叠加的装饰,你还是你,长相不增不减,却能在镜头前增加了多重美。如果你愿意,还可以尝试不同的装饰风格,只要装饰功能做的好,你就能成为“百变星君”。
可以用代码表示,把每个功能抽象成一个类:
// 女孩子
class Girl {
faceValue() {
console.log('我原本的脸')
}
}
class ThinFace {
constructor(girl) {
this.girl = girl;
}
faceValue() {
this.girl.faceValue();
console.log('开启瘦脸')
}
}
class IncreasingEyes {
constructor(girl) {
this.girl = girl;
}
faceValue() {
this.girl.faceValue();
console.log('增大眼睛')
}
}
let girl = new Girl();
girl = new ThinFace(girl);
girl = new IncreasingEyes(girl);
// 闪瞎你的眼
girl.faceValue(); //
从代码的表现来看,将一个对象嵌入到另一个对象中,相当于通过一个对象对另一个对象进行包装,形成一条包装链。调用后,随着包装的链条传递给每一个对象,让每个对象都有处理的机会。
这种方式在增加删除装饰功能上都有极大的灵活性,假如你有勇气展示真实的脸,去掉瘦脸的包装即可,这对其他功能毫无影响;假如要增加磨皮,再来个功能类,继续装饰下去,对其他功能也无影响,可以并存运行。
在 javascript 中增加小功能使用类,显的有点笨重,JavaScript 的优点是灵活,可以使用对象来表示:
let girl = {
faceValue() {
console.log('我原本的脸')
}
}
function thinFace() {
console.log('开启瘦脸')
}
function IncreasingEyes() {
console.log('增大眼睛')
}
girl.faceValue = function(){
const originalFaveValue = girl.faceValue; // 原来的功能
return function() {
originalFaveValue.call(girl);
thinFace.call(girl);
}
}()
girl.faceValue = function(){
const originalFaveValue = girl.faceValue; // 原来的功能
return function() {
originalFaveValue.call(girl);
IncreasingEyes.call(girl);
}
}()
girl.faceValue();
在不改变原来代码的基础上,通过先保留原来函数,重新改写,在重写的代码中调用原来保留的函数。
用一张图来表示装饰者模式的原理:
从图中可以看出来,通过一层层的包装,增加了原先对象的功能。
TypeScript 中的装饰器使用 @expression 这种形式,expression 求值后为一个函数,它在运行时被调用,被装饰的声明信息会被做为参数传入。
Javascript规范里的装饰器目前处在 建议征集的第二阶段,也就意味着不能在原生代码中直接使用,浏览器暂不支持。
可以通过 babel 或 TypeScript 工具在编译阶段,把装饰器语法转换成浏览器可执行的代码。(最后会有编译后的源码分析)
以下主要讨论 TypeScript 中装饰器的使用。
TypeScript 中的装饰器可以被附加到类声明、方法、 访问符(getter/setter)、属性和参数上。
开启对装饰器的支持,命令行 编译文件时:
tsc --target ES5 --experimentalDecorators test.ts
配置文件 tsconfig.json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器实际上就是一个函数,在使用时前面加上 @ 符号,写在要装饰的声明之前,多个装饰器同时作用在一个声明时,可以写一行或换行写:
// 换行写
@test1
@test2
declaration
//写一行
@test1 @test2 ...
declaration
定义 face.ts 文件:
function thinFace() {
console.log('开启瘦脸')
}
@thinFace
class Girl {
}
编译成 js 代码,在运行时,会直接调用 thinFace 函数。这个装饰器作用在类上,称之为类装饰器。
如果需要附加多个功能,可以组合多个装饰器一起使用:
function thinFace() {
console.log('开启瘦脸')
}
function IncreasingEyes() {
console.log('增大眼睛')
}
@thinFace
@IncreasingEyes
class Girl {
}
多个装饰器组合在一起,在运行时,要注意,调用顺序是 从下至上 依次调用,正好和书写的顺序相反。例子中给出的运行结果是:
'增大眼睛'
'开启瘦脸'
如果你要在一个装饰器中给类添加属性,在其他的装饰器中使用,那就要写在最后一个装饰器中,因为最后写的装饰器最先调用。
有时需要给装饰器传递一些参数,这要借助于装饰器工厂函数。装饰器工厂函数实际上就是一个高阶函数,在调用后返回一个函数,返回的函数作为装饰器函数。
function thinFace(value: string){
console.log('1-瘦脸工厂方法')
return function(){
console.log(`4-我是瘦脸的装饰器,要瘦脸${value}`)
}
}
function IncreasingEyes(value: string) {
console.log('2-增大眼睛工厂方法')
return function(){
console.log(`3-我是增大眼睛的装饰器,要${value}`)
}
}
@thinFace('50%')
@IncreasingEyes('增大一倍')
class Girl {
}
@ 符号后为调用工厂函数,依次从上到下执行,目的是求得装饰器函数。装饰器函数的运行顺序依然是从下到上依次执行。
运行的结果为:
1-瘦脸工厂方法
2-增大眼睛工厂方法
3-我是增大眼睛的装饰器,要增大一倍
4-我是瘦脸的装饰器,要瘦脸50%
总结一下:
作用在类声明上的装饰器,可以给我们改变类的机会。在执行装饰器函数时,会把类构造函数传递给装饰器函数。
function classDecorator(value: string){
return function(constructor){
console.log('接收一个构造函数')
}
}
function thinFace(constructor){
constructor.prototype.thinFaceFeature = function() {
console.log('瘦脸功能')
}
}
@thinFace
@classDecorator('类装饰器')
class Girl {}
let g = new Girl();
g.thinFaceFeature(); // '瘦脸功能'
上面的例子中,拿到传递构造函数后,就可以给构造函数原型上增加新的方法,甚至也可以继承别的类。
作用在类的方法上,有静态方法和原型方法。作用在静态方法上,装饰器函数接收的是类构造函数;作用在原型方法上,装饰器函数接收的是原型对象。
这里拿作用在原型方法上举例。
function methodDecorator(value: string, Girl){
return function(prototype, key, descriptor){
console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype)
}
}
function thinFace(prototype, key, descriptor){
// 保留原来的方法逻辑
let originalMethod = descriptor.value;
// 改写,增加逻辑,并执行原有逻辑
descriptor.value = function(){
originalMethod.call(this); // 注意修改this的指向
console.log('开启瘦脸模式')
}
}
class Girl {
@thinFace
@methodDecorator('方式装饰器', Girl)
faceValue(){
console.log('我是原本的面目')
}
}
let g = new Girl();
g.faceValue();
从代码中可以看出,装饰器函数接收三个参数,原型对象、方法名、描述对象。对描述对象陌生的,可以参考 这里;
要增强功能,可以先保留原来的函数,改写描述对象的 value 为另一函数。
当使用 g.faceValue() 访问方法时,访问的就是描述对象 value 对应的值。
在改写的函数中增加逻辑,并执行原来保留的原函数。注意原函数要用 call 或 apply 将 this 指向原型对象。
作用在类中定义的属性上,这些属性不是原型上的属性,而是通过类实例化得到的实例对象上的属性。
装饰器同样会接受两个参数,原型对象,和属性名。而没有属性描述对象,为什么呢?这与TypeScript是如何初始化属性装饰器的有关。 目前没有办法在定义一个原型对象的成员时描述一个实例属性。
function propertyDecorator(value: string, Girl){
return function(prototype, key){
console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype)
}
}
function thinFace(prototype, key){
console.log(prototype, key)
}
class Girl {
@thinFace
@propertyDecorator('属性装饰器', Girl)
public age: number = 18;
}
let g = new Girl();
console.log(g.age); // 18
下面组合多个装饰器写在一起,出了上面提到的三种,还有 访问符装饰器、参数装饰器。这些装饰器在一起时,会有执行顺序。
function classDecorator(value: string){
console.log(value)
return function(){}
}
function propertyDecorator(value: string) {
console.log(value)
return function(){
console.log('propertyDecorator')
}
}
function methodDecorator(value: string) {
console.log(value)
return function(){
console.log('methodDecorator')
}
}
function paramDecorator(value: string) {
console.log(value)
return function(){
console.log('paramDecorator')
}
}
function AccessDecorator(value: string) {
console.log(value)
return function(){
console.log('AccessDecorator')
}
}
function thinFace(){
console.log('瘦脸')
}
function IncreasingEyes() {
console.log('增大眼睛')
}
@thinFace
@classDecorator('类装饰器')
class Girl {
@propertyDecorator('属性装饰器')
age: number = 18;
@AccessDecorator('访问符装饰器')
get city(){}
@methodDecorator('方法装饰器')
@IncreasingEyes
faceValue(){
console.log('原本的脸')
}
getAge(@paramDecorator('参数装饰器') name: string){}
}
运行了这段编译后的代码,会发现这些访问器的顺序是,属性装饰器 -> 访问符装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。
更详细的用法可以参考官网文档:https://www.tslang.cn/docs/handbook/decorators.html#decorator-factories
装饰器在浏览器中不支持,没办法直接使用,需要经过工具编译成浏览器可执行的代码。
分析一下通过工具编译后的代码。
生成 face.js 文件:
tsc --target ES5 --experimentalDecorators face.ts
打开 face.js 文件,会看到一段被压缩后的代码,可以格式化一下。
先看这段代码:
__decorate([
propertyDecorator('属性装饰器')
], Girl.prototype, "age", void 0);
__decorate([
AccessDecorator('访问符装饰器')
], Girl.prototype, "city", null);
__decorate([
methodDecorator('方法装饰器'),
IncreasingEyes
], Girl.prototype, "faceValue", null);
__decorate([
__param(0, paramDecorator('参数装饰器'))
], Girl.prototype, "getAge", null);
Girl = __decorate([
thinFace,
classDecorator('类装饰器')
], Girl);
__decorate 的作用就是执行装饰器函数,从这段代码中能够看出很多信息,印证上面得到的结论。
通过**__decorate**调用顺序,可以看出来,多个类型的装饰器一起使用时,顺序是,属性装饰器 -> 访问符装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。
调用了 __decorate 函数,根据使用的装饰器类型不同,传入的参数也不相同。
第一个参数传入的都一样,为数组,这样确保和我们书写的顺序一致,每一项是求值后的装饰器函数,如果写的是 @propertyDecorator() 则一上来就执行,得到装饰器函数,这跟上面分析的一致。
类装饰器会把类作为第二个参数,其他的装饰器,把原型对象作为第二个参数,属性名作为第三个,第四个是 null 或 void 0。void 0的值为undefined,也就等于没传参数
要记住传给 __decorate 函数参数的个数和值,在深入到 __decorate 源码中, 会根据这些值来决定执行装饰器函数时,传入参数的多少。
好,来看 __decorate 函数实现:
// 已存在此函数,直接使用,否则自己定义
var __decorate = (this && this.__decorate) ||
// 接收四个参数:
//decorators存放装饰器函数的数组、target原型对象|类,
//key属性名、desc描述(undefined或null)
function(decorators, target, key, desc) {
var c = arguments.length,
// 拿到参数的个数
r = c < 3 // 参数小于三个,说明是类装饰器,直接拿到类
? target
: desc === null // 第四个参数为 null,则需要描述对象;属性装饰器传入是 void 0,没有描述对象。
? desc = Object.getOwnPropertyDescriptor(target, key)
: desc,
d;
// 如果提供了Reflect.decorate方法,直接调用;否则自己实现
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
// 装饰器函数执行顺序和书写的顺序相反,从下至上 执行
for (var i = decorators.length - 1; i >= 0; i--)
if (d = decorators[i]) // 拿到装饰器函数
r = (c < 3 // 参数小于3个,说明是类装饰器,执行装饰器函数,直接传入类
? d(r)
: c > 3 // 参数大于三个,是方法装饰器、访问符装饰器、参数装饰器,则执行传入描述对象
? d(target, key, r)
: d(target, key) // 为属性装饰器,不传入描述对象
) || r;
// 给被装饰的属性,设置得到的描述对象,主要是针对,方法、属性来说的
/***
* r 的值分两种情况,
* 一种是通过上面的 Object.getOwnPropertyDescriptor 得到的值
* 另一种,是装饰器函数执行后的返回值,作为描述对象。
* 一般不给装饰器函数返回值。
*/
return c > 3 && r && Object.defineProperty(target, key, r),r;
};
上面的参数装饰器,调用了一个函数为 __params,
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
目的是,要给装饰器函数传入参数的位置 paramIndex。
看了编译后的源码,相信会对装饰器的理解更深刻。
以上如有偏差欢迎指正学习,谢谢。~~~~
如果对你有帮助,请关注【前端技能解锁】:
使用过一段时间 class 来定义组件,要用 vue-property-decorator 提供定义好的装饰器,辅助完成所需功能,对这个过程好奇,就研究了源码。内部主要依靠 vue-class-component 实现,所以将重点放在对 vue-class-component 的解读上。
本文主要内容有:
没有使用 class 方式定义组件时,通常导出一个选项对象:
<script>
export default {
props: {
name: String
},
data() {
return {
message: '新消息'
}
},
watch: {
message(){
console.log('message改变触发')
}
},
computed:{
hello: {
get(){
return this.message + 'hello';
},
set(newValue){}
}
},
methods:{
clickHandler(){}
}
mounted(){
console.log('挂载完毕');
}
}
</script>
这个对象告诉 Vue 你要做什么事情,需要哪些功能。 根据字段的不同作用,把需要添加的属性和方法,写在指定的位置,例如,需要响应式数据写在 data 中、计算属性写在 computed 中、事件函数写在 methods中、直接写生命周期函数等 。Vue 内部会调用 Vue.extend() 创建组件的构造函数,以便在模板中使用时,通过构造函数初始化此组件。
如果使用了 class 来定义组件,上面的字段可省略,但要符合 Vue 内部使用数据的规则,就需要重组这些数据。
定义 class 组件:
<script lang="ts">
class Home extends Vue {
message = '新数据';
get hello(){
return this.message + 'hello';
}
set hello(newValue){}
clickHandler(){}
mounted(){}
}
Home.prototype.age = '年龄'
</script>
message 作为响应式的数据,应该放在 data 中,但问题是 message 写在类中,为初始化后实例上的属性,就要想办法在初始化后拿到 message,放在 data 中。
age 直接写在原型上,值不是函数,也应该放在 data 中。
hello 写了访问器,作为计算属性,写在 computed 中;clickHandler作为方法,写在 methods 中;mounted 是生命周期函数,挂载原型上就可以,不需要动。这三个都是方法,定义在原型上,需要拿到原型对象,找到这三类方法,按照特性放在指定位置。
这就引发一个问题,怎么把这些定义的属性放在 Vue 需要解析的数据中,“上帝的归上帝,凯撒的归凯撒”。
最终处理成这样:
{
data:{
message: '新数据',
age: '年龄'
},
methods:{
clickHandler(){}
},
computed:{
hello:{
get(){
return this.message + 'hello';
}
}
},
mounted(){}
}
最好是无入侵式的添加功能,开发者无感知,正常写业务代码,提供封装好功能来完成归类数据这件事。
装饰器模式,在不改变自身对象的基础上,动态增加额外的功能,这个模式的思路符合上述内容的要求。具体可参考一篇文章详细了解,装饰者模式和TypeScript装饰器
vue-class-component 的代码使用 ts 书写,如果对 ts 语法不熟悉,可以忽略定义的类型,直接看函数体内的逻辑,不影响阅读。或者直接看打包后,没有压缩的代码,也不多,大约200行左右。
本文分析的代码主要文件在:仓库地址
先来看大致结构和如何使用:
function Component(options) {
// options 是 function类型,是要装饰的类
if (typeof options === 'function') {
return componentFactory(options);
}
// 执行后,这个函数作为装饰器函数,接收要装饰的类
// options 为传入的选项数据。
return function (Component) {
return componentFactory(Component, options);
};
}
// 使用1
@Component
class Home Extend Vue {}
// 使用2
@Component({
components:{}
data:{newMessage: '增加的消息'},
methods:{
moveHandler(){}
},
computed:{
reveserMessage(){
return this.newMessage + '翻转'
}
}
// ... vue中选项对象其他值
})
class Home Extend Vue {}
Component 作为装饰器函数,接受的 options 就是要装饰的类 Home, js 中类不过是一种语法糖,typeof Home 得到为 function 类型。
Component 函数作为工厂函数,执行并传入参数 options(为了称呼方便,后面把这个参数叫做 装饰器选项数据),工厂函数执行后,返回装饰器函数,同样是接受要装饰的类 Home。
从代码中可以看出来,都调用了 componentFactory ,第一个参数为要装饰的类,第二参数可选,传入的话就是装饰器选项数据。
从名字上可以看出来,componentFactory 用来产生组件的工厂,经过一系列的执行后,返回新的组件函数。省略其他,先看关键代码 代码地址:
function componentFactory(Component) {
// 省略其他代码...
// 参数为两个,说明第二个是传入的部分选项数据;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// 得到继承的父类,不出意外为 Vue
var superProto = Object.getPrototypeOf(Component.prototype);
// 如果原型链上确实有 Vue,则得到构造函数;不为 Vue,则直接使用 Vue;
// 目的是为了找到 extend 函数。
var Super = superProto instanceof Vue ? superProto.constructor : Vue;
// 根据选项对象,新建一个组件的构造函数
var Extended = Super.extend(options);
// 返回新的构造函数
return Extended;
}
验证了上面的猜测,调用了 Vue.extend 返回新的组件函数。但在返回之前,要处理原来组件上的属性,和原型上的方法。
首先对选项上的方法归类,方法归 methods;非方法归 data;有访问器归 computed。
// 需要忽略的属性
const $internalHooks = [
'data',
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeDestroy',
'destroyed',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'render',
'errorCaptured', // 2.5
'serverPrefetch' // 2.6
]
function componentFactory(Component) {
// 其他代码省略...
// 拿到原型对象
const proto = Component.prototype
// 返回对象上所有自身属性,包括不可枚举的属性
Object.getOwnPropertyNames(proto).forEach(function (key) {
// 构造函数,不做处理
if (key === 'constructor') {
return
}
// 钩子函数之类的属性,直接赋值到 options对象上,不需要归类
if ($internalHooks.indexOf(key) > -1) {
options[key] = proto[key]
return
}
// 拿到对应属性的描述对象,用这个方法能避免继续查找原型链上的属性
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
// 如果此属性的值不为 undefined,说明有值
if (descriptor.value !== void 0) {
// methods
// 如果为函数,则直接归为 methods
if (typeof descriptor.value === 'function') {
(options.methods || (options.methods = {}))[key] = descriptor.value
} else {
// 如果值不为函数,则归为data,这里采用 mixins,混合数据的方式来做
(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return { [key]: descriptor.value }
}
})
}
} else if (descriptor.get || descriptor.set) {
// value 为空,但是有 get或set的访问器,则归为computed
(options.computed || (options.computed = {}))[key] = {
get: descriptor.get,
set: descriptor.set
}
}
})
}
从上述代码可以看出来,拿到属性对应的描述对象,根据属性对应的值,进行类型判断,来决定归为哪一类。
值得注意的是这段代码,目的是把非函数的属性,混合在 data 中:
if(typeof descriptor.value === 'function'){/*省略*/}
else{// 处理原型上不是函数的情况
(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return { [key]: descriptor.value }
}
})
}
一般写在类中的只有是函数才能放在原型上,但有别的方式可以把非函数的值添加到原型上:
// 第一种,直接给原型添加属性
Home.prototype.age = 18;
// 第二种,用属性装饰器
function ageDecorator(prototype, key){
return { // 装饰器返回描述对象,会在 prototype增加key这个属性
enumerable: false,
value: 18
}
}
class Home extends Vue {
@ageDecorator
age: number = 18;
}
如果用了 ts 的属性装饰器,并返回描述对象,就会在 prototype 增加这个属性,所以在上面 componentFactory 源码中要处理这种情况,一般在项目中比较少见。
写在类中的属性,不添加在原型上,只有通过得到实例后拿到这些值,可以沿着这个思路进行分析。
先看实例上属性的情况:
class Home {
message: '新消息',
clickHandler(){}
}
let home = new Home();
console.log(home);
// 打印实例,简化后:
{
message: "新消息"
__proto__:
constructor: class Home
clickHandler: ƒ clickHandler()
__proto__: Object
}
在 componentFactory 中做了单独的处理:
function componentFactory(Component){
// 省略其他代码
;(options.mixins || (options.mixins = [])).push({
data () {
return collectDataFromConstructor(this, Component)
}
})
}
这里依然使用混合 data 的方式,混合功能很强大,敲黑板记下来。mixins 会在初始化组件时,调用 data 对应的函数,得到要混合的数据,又调用了 collectDataFromConstructor,传入 this,为组件实例,跟平时写项目在 mounted 中使用的那个 this 一样,都为渲染组件的实例;第二参数为 Component,是原来装饰的类,上面例子中就是 Home 类。
这个函数的目的是把原来装饰的类,初始之后,拿到实例上的属性组成对象返回。代码地址
来看代码:
// 用来收集被装饰类中定义的属性
// vm 为要渲染的组件实例
// Component 为原来要装饰的组件类
function collectDataFromConstructor(vm, Component) {
// 先保存原有的 _init,目的是不执行 Vue上的 _init 做其他初始化动作
var originalInit = Component.prototype._init;
// 在被装饰的类的原型上手动增加 _init,在Vue实例化事内部会调用
Component.prototype._init = function () {
var _this = this;
// 拿到渲染组件对象上的属性,包括不可枚举的属性,包含组件内定义的 $开头属性 和 _开头属性,还有自定义的一些方法
var keys = Object.getOwnPropertyNames(vm);
// 如果渲染组件含有,props,但是并没有放在原组件实例上,则添加上
if (vm.$options.props) {
for (var key in vm.$options.props) {
if (!vm.hasOwnProperty(key)) {
keys.push(key);
}
}
}
// 把给原组件实例上 Vue 内置属性设置为不可遍历。
keys.forEach(function (key) {
if (key.charAt(0) !== '_') {
Object.defineProperty(_this, key, {
get: function get() {
return vm[key];
},
set: function set(value) {
vm[key] = value;
},
configurable: true
});
}
});
};
// 手动初始化要包装的类,目的是拿到初始化后实例
var data = new Component();
// 重新还原回原来的 _init,防止一直引用原有的实例,造成内存泄漏
Component.prototype._init = originalInit;
// 重新定义对象
var plainData = {};
// Object.keys 拿到可被枚举的属性,添加到对象中
Object.keys(data).forEach(function (key) {
if (data[key] !== undefined) {
plainData[key] = data[key];
}
});
return plainData;
}
具体要做的话,通过 new Component() 得到被装饰类的实例,但要注意,Component 继承了 Vue 类,初始化后实例上有很多 Vue 内部添加上的属性,比如 $options、$parent、$attrs、$listeners、$data 等等,还有以 _ 开头的属性,_watcher、_renderProxy 等等,还有我们需要的属性。这里只是简单举几个属性,你可以手动初始化,在控制台打印输出看一下。
以 _ 开头的属性,是内置方法,不可被枚举;以 $ 开头的属性,也是内置方法,但是可被枚举。如果直接循环实例,会拿到以 $ 开头的属性,这并不是我们需要的。
那怎么办呢?代码中给了答案,在初始化一系列组件内置的属性后,组件内部会调用 Component.prototype._init 方法,可通过改写这个方法,来处理属性为不可枚举。
最后通过 Object.keys() 得到能够被遍历的属性。
上面拐的弯比较多,难免看蒙了,根据核心意思,简化如下:
原来有个组件:
class Home {
message: '新消息'
}
现在有个需要渲染的组件,要把上面定义在 Home 中的 message 写在现有组件的 data 中:
const App = Vue.extend({
// 混合功能
mixins:[{
data(){
// 初始化后拿到实例,就能拿到 message 属性
let data = new Home();
let plainData = {};
Object.keys(data).forEach(function (key) {
if (data[key] !== undefined) {
plainData[key] = data[key];
}
});
return plainData;
}
}],
data(){
return {
other: '其他data'
}
}
})
new App().$mounted('#app');
简化后,是不是清晰很多,本质就是初始类得到实例,拿属性组成对象,混合到渲染的组件中。
小的优化点,简化代码:
// 保留原有的 _init 方法
var originalInit = Component.prototype._init;
Component.prototype._init = function(){
// 其他代码省略
};
Component.prototype._init = originalInit;
这段代码,在改写的 _init 内部使用了外面的引用 vm 和 Component,就会一直在内存中,为防止内存泄漏,重新赋回原来的函数。
vue-property-decorator 依赖 vue-class-component 实现,主要用了内部提供的 createDecorator 方法。
如果你想增加更多装饰器,也可以通过调用 createDecorator 方法,原理很简单,就是向选项对象上增加所需数据。
在 vue-class-component 中提供了工具函数 createDecorator 允许添加其他额外的装饰函数,统一挂载在 Component.decorators 上,并把 options 传过去,对 options 增加需要的属性,实际上会调用这些装饰函数,让这些函数有机会处理 options。
function componentFactory(Component) {
// 省略其他代码....
var decorators = Component.__decorators__;
if (decorators) {
decorators.forEach(function (fn) {
return fn(options);
});
delete Component.__decorators__;
}
}
我们可以利用 createDecorator,扩展其他的装饰器,vue-property-decorator 内部就是利用这个函数扩展了 @prop、@watch 等装饰器。
function createDecorator(factory) {
return (target, key, index) => {
// 是函数类型,则为装饰的类;
// 否则,为原型,通过constructor拿到构造函数
const Ctor = typeof target === 'function'
? target
: target.constructor;
if (!Ctor.__decorators__) {
Ctor.__decorators__ = [];
}
// 当为参数装饰器时,index为number
if (typeof index !== 'number') {
index = undefined;
}
Ctor.__decorators__.push(options => factory(options, key, index));
};
}s
从源码中可以看出来,createDecorator 调用后会返回一个函数,这个函数可以作为装饰器函数,接收的 target 如果是函数类型,说明作为类装饰器,target 就是被装饰的类;否则,得到的是原型,通过 constructor 拿到构造函数。
向要装饰的类上添加静态属性 decorators,存入一个函数,获得 options。
现在来看 vue-property-decorator 中 watch 装饰器的源码,代码地址
function Watch(path, options) {
if (options === void 0) { options = {}; }
return createDecorator(function (componentOptions, handler) {
if (typeof componentOptions.watch !== 'object') {
componentOptions.watch = Object.create(null);
}
var watch = componentOptions.watch;
if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) {
watch[path] = [watch[path]];
}
else if (typeof watch[path] === 'undefined') {
watch[path] = [];
}
watch[path].push({ handler: handler});
});
}
传入 createDecorator 的回调函数,会接受两个参数,componentOptions 为一个对象,就是在上面 componentFactory 中调用 Component.decorators,传入的对象,目的是向这个对象添加或增加 watch 属性,给要装饰的类使用;handler 是函数名字;
这样使用:
@Component
class Home extend Vue {
message='新消息'
@watch('message')
messageHandler(){
console.log('当message改变后,执行这里')
}
}
经过 @watch 装饰器处理后,选项对象上会增加一段数据:
{
watch: {
message: 'messageHandler'
},
methods:{
messageHandler(){
console.log('当message改变后,执行这里')
}
}
}
以上便是 vue-property-decorator 增加装饰器的实现方式,对其他装饰器感兴趣,可以看仓库源码,做进一步了解,思路都大同小异。
以上如有偏差欢迎指正学习,谢谢。~~~~
github博客地址:https://github.com/WYseven/blog,欢迎star。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.