GithubHelp home page GithubHelp logo

blog's People

Contributors

wyseven avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

blog's Issues

[翻译]2020 Javascript 模块

在这篇文章中,我们将介绍标准的 JavaScript 模块,目前是如何在前端应用程序中使用的,以及未来我们可能会如何使用它们。
JavaScript 模块有时被称为 ESM,它代表 ECMAScript 模块。


image.png

什么是JavaScript模块?

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 模块通常是如何使用的?


JavaScript 模块语法是在 ES6 中引入的,通常在我们今天构建的应用程序中使用,如下所示:

import React from 'react';
...
export const HomePage = () => ...


上面的示例导入 React 模块并导出 HomePage 组件。


不过,这段代码并没有使用 JavaScript 模块。取而代之的是,Webpack 将其转换为非原生模块,而采用了 IIFE(立即调用函数表达式)来做 。
值得注意的是,Webpack 确实有一个实验性的 outputModule 特性,允许它以原生模块格式发布。希望 Webpack 5 中包含这个功能!

使用原生的 JavaScript 模块


要声明一个引用 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.jsc.jsJavaScript

// /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模块错误


让我们以前面的类似示例为例,其中有 JavaScript 模块 a,b,c。模块 a 依赖于模块 b 和 c。模块 bc没有依赖关系。


假设 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>


模块依赖于其他模块,而这些模块又依赖于其他模块。因此,在执行脚本元素之前,会下载并解析所有依赖项。


image.png

预加载模块


JavaScript 模块可以预加载使用 modulepreload 资源提示:

<link
  rel="modulepreload"
  href="https://cdn.jsdelivr.net/npm/[email protected]/intersection.js"
/>

即在其他模块下载之前先下载并解析此模块:
image.png
目前只有 ChromeEdge 支持预加载模块。**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
**

依赖项必须发布的是ES模块


重要的一点是,库必须发布为原生模块格式,以便开发者将库用作原生模块使用。不幸的是,目前这种情况并不常见。例如,React尚未发布为原生模块。

原生模块相对于非原生模块的好处


与 IIFE 模块之类的模块相比,原生模块的一个好处是需要下载到浏览器、解析然后执行,相对来说代码更少。
原生模块也可以并行地、异步地下载和解析。因此,原生模块对于大型依赖树可能执行得更快。
此外,预加载模块可能意味着用户可以更快地与页面交互,因为这些代码是从主线程中解析出来的。


除了性能上的提高之外,新的浏览器特性还可能构建在模块之上,因此使用原生模块是一种验证未来的代码。

结束语

当前最流行的浏览器都可以使用本机模块,并且可以为IE提供备份。Rollup已经可以以这种格式发布了,而且Webpack支持似乎正在进行中。现在,我们所需要的是更多的库开始以原生模块发布。

github博客地址:https://github.com/WYseven/blog,欢迎star。
如果对你有帮助,请关注【前端技能解锁】:
前端技能解锁

装饰者模式和TypeScript装饰器

导读

本文主要为三方面的内容:

  1. 装饰者模式的概念和使用
  2. Typescript装饰器的使用、执行顺序
  3. 编译后的源码分析

学习的目的是对装饰者模式模式有进一步的理解,并运用在自己的项目中;对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中的装饰器

TypeScript 中的装饰器使用 @expression 这种形式,expression 求值后为一个函数,它在运行时被调用,被装饰的声明信息会被做为参数传入。

Javascript规范里的装饰器目前处在 建议征集的第二阶段,也就意味着不能在原生代码中直接使用,浏览器暂不支持。

可以通过 babelTypeScript 工具在编译阶段,把装饰器语法转换成浏览器可执行的代码。(最后会有编译后的源码分析)

以下主要讨论 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%

总结一下:

  1. 写了工厂函数,从上到下依次执行,求得装饰器函数。
  2. 装饰器函数的执行顺序是 从下到上 依次执行。

类装饰器

作用在类声明上的装饰器,可以给我们改变类的机会。在执行装饰器函数时,会把类构造函数传递给装饰器函数。

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 对应的值。

在改写的函数中增加逻辑,并执行原来保留的原函数。注意原函数要用 callapplythis 指向原型对象。

属性装饰器

作用在类中定义的属性上,这些属性不是原型上的属性,而是通过类实例化得到的实例对象上的属性。

装饰器同样会接受两个参数,原型对象,和属性名。而没有属性描述对象,为什么呢?这与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() 则一上来就执行,得到装饰器函数,这跟上面分析的一致。

类装饰器会把类作为第二个参数,其他的装饰器,把原型对象作为第二个参数,属性名作为第三个,第四个是 nullvoid 0void 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

看了编译后的源码,相信会对装饰器的理解更深刻。

以上如有偏差欢迎指正学习,谢谢。~~~~

如果对你有帮助,请关注【前端技能解锁】:

前端技能解锁

解读 vue-class-component 源码实现原理

导读

使用过一段时间 class 来定义组件,要用 vue-property-decorator 提供定义好的装饰器,辅助完成所需功能,对这个过程好奇,就研究了源码。内部主要依靠 vue-class-component 实现,所以将重点放在对 vue-class-component 的解读上。

本文主要内容有:

  • 装饰器作用在 class 定义的组件,发生了什么
  • 解读 Component 装饰器实现过程
  • vue-property-decorator 中如何扩展装饰器

装饰器作用在 class 定义的组件,发生了什么

没有使用 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行左右。

本文分析的代码主要文件在:仓库地址

解读 Component 装饰器

先来看大致结构和如何使用:

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 就是要装饰的类 Homejs 中类不过是一种语法糖,typeof Home 得到为 function 类型。

Component 函数作为工厂函数,执行并传入参数 options(为了称呼方便,后面把这个参数叫做 装饰器选项数据),工厂函数执行后,返回装饰器函数,同样是接受要装饰的类 Home

从代码中可以看出来,都调用了 componentFactory ,第一个参数为要装饰的类,第二参数可选,传入的话就是装饰器选项数据。

解读 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 类。

分析 collectDataFromConstructor 函数

这个函数的目的是把原来装饰的类,初始之后,拿到实例上的属性组成对象返回。代码地址

来看代码:

// 用来收集被装饰类中定义的属性
// 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 内部使用了外面的引用 vmComponent,就会一直在内存中,为防止内存泄漏,重新赋回原来的函数。

vue-property-decorator 中如何扩展装饰器

vue-property-decorator 依赖 vue-class-component 实现,主要用了内部提供的 createDecorator 方法。

如果你想增加更多装饰器,也可以通过调用 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-decoratorwatch 装饰器的源码,代码地址

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。

如果对你有帮助,请关注【前端技能解锁】:
qrcode_for_gh_d0af9f92df46_258.jpg

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.