Ramda中的柯理化

_curry1

内置接口,保证了参数的个数,无需判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Optimized internal one-arity curry function.
*
* @private
* @category Function
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/

export default function _curry1(fn) {
return function f1(a) {
if (arguments.length === 0 || _isPlaceholder(a)) {
return f1;
} else {
return fn.apply(this, arguments);
}
};
}

_curry2

如果以值的真假为纬度分支会比较复杂,而且不能正确参数的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const _curry2 = function (fn) {
return function f2(a, b) {
return a !== undefined && b !== undefined
? _isPlaceholder(a) && _isPlaceholder(b)
? f2
: isPlaceholder(a)
? _curry1(function (_a) { return fn(_a, b) })
: _isPlaceholder(b)
? _curry1(function (_b) { return fn(a, _b) })
: fn.apply(this, arguments)
: a !== undefined
? _curry1(function (_b) { return fn(a, _b) })
: f2
}
}

作者以参数的个数为纬度考虑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Optimized internal two-arity curry function.
*
* @private
* @category Function
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/
export default function _curry2(fn) {
return function f2(a, b) {
switch (arguments.length) {
case 0:
return f2;
case 1:
// 如果a传入的是undefined也是一个参数
return _isPlaceholder(a)
? f2
: _curry1(function(_b) { return fn(a, _b); });
default:
return _isPlaceholder(a) && _isPlaceholder(b)
? f2
: _isPlaceholder(a)
? _curry1(function(_a) { return fn(_a, b); })
: _isPlaceholder(b)
? _curry1(function(_b) { return fn(a, _b); })
: fn(a, b);
}
};
}

_curry3

同理增加分支判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Optimized internal three-arity curry function.
*
* @private
* @category Function
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/
export default function _curry3(fn) {
return function f3(a, b, c) {
switch (arguments.length) {
case 0:
return f3;
case 1:
return _isPlaceholder(a)
? f3
: _curry2(function(_b, _c) { return fn(a, _b, _c); });
case 2:
return _isPlaceholder(a) && _isPlaceholder(b)
? f3
: _isPlaceholder(a)
? _curry2(function(_a, _c) { return fn(_a, b, _c); })
: _isPlaceholder(b)
? _curry2(function(_b, _c) { return fn(a, _b, _c); })
: _curry1(function(_c) { return fn(a, b, _c); });
default:
return _isPlaceholder(a) && _isPlaceholder(b) && _isPlaceholder(c)
? f3
// 其中两个是占位符的情况
: _isPlaceholder(a) && _isPlaceholder(b)
? _curry2(function(_a, _b) { return fn(_a, _b, c); })
: _isPlaceholder(a) && _isPlaceholder(c)
? _curry2(function(_a, _c) { return fn(_a, b, _c); })
: _isPlaceholder(b) && _isPlaceholder(c)
? _curry2(function(_b, _c) { return fn(a, _b, _c); })
// 其中一个是占位符的情况
: _isPlaceholder(a)
? _curry1(function(_a) { return fn(_a, b, c); })
: _isPlaceholder(b)
? _curry1(function(_b) { return fn(a, _b, c); })
: _isPlaceholder(c)
? _curry1(function(_c) { return fn(a, b, _c); })
: fn(a, b, c);
}
};
}

curry

共有方法curry的实现, 对共有方法curryN的封装,在不确定参数个数时候通过 fn.length 获取参数长度

1
2
3
var curry = _curry1(function curry(fn) {
return curryN(fn.length, fn);
});

curryN

需要指定参数的个数, 抽象出参数个数的判断,最多个数为10个,防止参数过多造成栈溢出

_arity 对参数个数的处理

1
2
3
4
5
6
7
8
9
10
11
12
export default function _arity(n, fn) {
/* eslint-disable no-unused-vars */
switch (n) {
case 0: return function() { return fn.apply(this, arguments); };
case 1: return function(a0) { return fn.apply(this, arguments); };
case 2: return function(a0, a1) { return fn.apply(this, arguments); };
// ...
case 10: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); };
default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
}
}

1
2
3
4
5
6
var curryN = _curry2(function curryN(length, fn) {
if (length === 1) {
return _curry1(fn);
}
return _arity(length, _curryN(length, [], fn));
});

在参数较少时使用其他的内置方法

1
2
3
4
5
6
7
8
9
10
11
function curryN(length, fn) {
return _curry2(function (length, fn) {
return length === 1
? _curry1(fn)
: length === 2
? _curry2(fn)
: length === 3
? _curry3(fn)
: _arity(length, _curryN(length, [], fn))
})
}

_curryN 核心方法

下面是一段糟糕的代码

receive保持了源数组的引用,不符合函数式编成数据不可变的思想

每次添加新参数,都要查询可添加的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 1. 真实参数个数和length项等 则执行函数
// 2. 实时记录占位符的位置, 新参数优先放到占位符
function _curryN(length, receive, fn) {
return function fN() {
var index = 0;
var zw = 0;
function findzw(zw, receive) {
while (zw < receive.length) {
if (_isPlaceholder(receive[zw])) {
//指向占位符的前一位, 方便后面处理
return zw - 1
}
zw++
}
return zw - 1
}
// 找到占位符的位置
zw = findzw(zw, receive);
while (index < arguments.length) {
receive[zw + 1] = arguments[index];
//更新之后重新查找占位符, 条过当前的占位符
zw = findzw(zw + 1, receive);
index++;
}
// 没有占位符 则位置+1为参数长度
if (zw + 1 === length) {
return fn.apply(this, receive)
} else {
// 计算剩余参数
var j = 0;
var len = 0
while (j < receive.length) {
if (!_isPlaceholder(receive[index])) {
len++;
}
j++
}
return _arity(length - len, _curryN(length, receive, fn))
}
}
}

源码的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function _curryN(length, receive, fn) {
return function () {
// 复制已接受参数的数组
var combined = [];
// 遍历入参的索引
var argsIdx = 0;
// 剩余参数
var left = length;
// 遍历已接受参数索引
var combinedIdx = 0;

// 如果参数没有复制完成,或新如参没有添加完成则继续执行
while (combinedIdx < received.length || argsIdx < arguments.length) {
var result;
// 通过条件语句处理了占位符的问题
// 如果老参数没有复制完成,且复制的值不是占位符,或者入参已经都处理了,直接取出当前的老参数,用于复制到新的数组中
// 通过双指针解决占位符的问题
if (combinedIdx < received.length &&
(!_isPlaceholder(received[combinedIdx]) ||
argsIdx >= arguments.length)) {
result = received[combinedIdx];
} else {
// 如果老参数都复制完,或者时占位符,或者还有新参数没有复制
// 取一个新参数加到数组中
result = arguments[argsIdx];
argsIdx += 1;
}
// 添加到新数组中
combined[combinedIdx] = result;
// 如果不是占位符剩余参数 -1
if (!_isPlaceholder(result)) {
left -= 1;
}
combinedIdx += 1;
}
// 如果没有剩余参数直接执行
return left <= 0
? fn.apply(this, combined)
// 否则通过_arity固定新函数的参数个数
: _arity(left, _curryN(length, combined, fn));
};
}

TypeScript 面向对象

定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class C {
constructor(n: string) {
this.name = n;// 构造函数,为定义的属性赋值
}
name: string; // 定义属性 省略前面的public关键字

readonly age:number = 18 // 只读属性

static readonly age: number = 1;

run():void{ // 定义方法
console.log('void')
}

}

接口

定义类的结构,声明包含的属性和方法,接口也可以当作类型声明去使用

接口可以在定义类的时候限制类的结构,接口中所有属性都不应该有实际的值

接口只定义对象的结构,接口中的所有方法都是抽象方法

同名结构声明的类型会合并,如果一个对象使用了该接口,必须包含所有的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
interface MyObj {
prop1: string,
prop2: number
}
interface MyObj {
prop3: Boolean
}

const obj: MyObj = {
prop1: '1',
prop2: 2,
prop3: true,
}

接口可以描述类的行为
定义类时可以让类去实现一个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface PersonInterface {
// 只读属性
readyonly name: string,
age: number,
say(method: string): boolean
}

class JiaZhen implements PersonInterface {
name: string = '1';
age: number = 1;
say(method: string): boolean {
return true;
}
}

使用接口描述一个类的实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface PersonInterface{
name:string,
new(name:string):Person
}

class Person{
constructor(name:string) {
}
}

function createPerson(Clsss:PersonInterface,name:string) {
return new Clsss(name);
}

createPerson(Person,'aaa');

属性的封装

类的属性如果可以随意被修改,可能导致运算时的错误

TS提供了三种属性的修饰符

public: 公有 在类,子类 类外面都可以访问

1
2
3
4
5
6
class Person {
constructor(pubilc name:string){
// 表示添加到实例上。可以通过属性访问
}
}
new Person().name

protected: 保护类型 在类里面,子类里面可以访问,在类外部没法访问

private: 私有 在类里面可以方法,子类,类外都不能访问

属性如果不加修饰符就是共有

泛型

定义函数或者类时遇到类型不确定时使用泛型,可以使用多个

1
2
3
4
5
6
7
function jiazhen<Z>(age: Z): Z {
return age;
}

jiazhen(19);

jiazhen<Number>(19) // 主动指明泛型

指明泛型Z必须是interface的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Zhen {
age: number,
love(who: string): boolean
}

function jiazhen<Z extends Zhen>(obj: Z): any {
return obj.love;
}

jiazhen<Zhen>({
age: 1,
love(a) {
return true;
}
})

指明类初始化时的泛型

1
2
3
4
5
6
7
8
class A<T>{
name: T;
constructor(a: T) {
this.name = a
}
}

new A<string>('10')

类装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function enhancer(target:any){
target.prototype.name = '11';
}

interface Person{
name:string
}

@enhancer
class Person {
constructor(){

}
}

const p = new Person();
p.name;

属性装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

// 如果装饰的是普通的属性,target指向类的原型
// 如果是static属性,target指向类的定义
function upperCase(target:any,propname:string) {
let v = target[propname];
const getter = ()=>v;
const setter = (newValue:string)=>{
v = newValue.toUpperCase();
}
delete target[propname];
Object.defineProperty(target,propname,{
set:setter,
get:getter,
enumerable:true,
configurable:true
})
}

interface Person{
name:string
}

class Person {
@upperCase
name = 'girl'
constructor(){

}
}

const p = new Person();
console.log(p.name);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function unEnumber(enumConfig:boolean) {
// 属性描述器
return function unEnumber(target:any,propname:string,propertyDescriptor:PropertyDescriptor) {
propertyDescriptor.enumerable = enumConfig;
}
}

interface Person{
name:string
}

class Person {
name = 'girl'
constructor(){}

// getname 不能枚举
@unEnumber(false)
getName(){}
}

const p = new Person();
console.log(p.name);

for(let key in p){
console.log(key)
}

可以对方法的逻辑进行包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function trans(target:any,propname:string,propertyDescriptor:PropertyDescriptor) {
const oldValue = propertyDescriptor.value;
console.log("1------------")
propertyDescriptor.value = function getNumber(...args:any[]) {
args = args.map(item=>Number(item));
console.log("2------------")
return oldValue.apply(this,args);
}
}

interface Person{
name:string
}

class Person {
name = 'girl'
constructor(){}

@trans
getNumber(...args:any[]){
console.log("3------------")
return args.reduce((sum,item)=>{ sum+=item; return sum },0);
}

}

const p = new Person();

console.log(p.getNumber('1',2,'3'));

参数修饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//              类的原型    方法名             被修饰参数的索引
function dubble(target:any,methodName:string,index:number) {
target.num = 'num'
}

class Person {
getNumber(@dubble num:number){
console.log(num);
console.log(this.num);
}
}

const p = new Person();

p.getNumber(2);

抽象类

不能当作构造函数,抽象方法必须被子类实现

1
2
3
4
5
6
7
abstract class Person {
abstract say(): void
}

class Lisa extends Person {
say() { }
}

重写和重载

重写:子类中重写继承自父类的方法

1
2
3
4
5
6
7
class Person {
say(){}
}

class Boy extends Person{
say(){}
}

重载:为一个函数提供多个类型的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数重载表示同名的函数如果参数不同函数会重载

// 但是JS中没有重载的概念,下面同名的函数会覆盖上面的函数

// TS中模拟函数重载, 通过不同的参数类型校验

function fn(a: number): number;
function fn(b: string): string;
function fn(c: any): any {
if (typeof c === 'number') {
return c + 1;
}
};

TypeScript 基本概念

  • Q1: TypeScript 可以直接在浏览器或 Node.js 中运行吗?
  • Q2: 使用 TypeScript 会影响代码在生产环境的运行性能吗?
  • Q3: 现有的 JavaScript 项目必须重写才能切换到 TypeScript 吗?
  • Q4: 既然最终运行的是 JS,为什么不直接写 JS?

TypeScript 概述

TypeScript 是由微软开发并维护的开源编程语言,旨在解决 JavaScript 在大型应用开发中的局限性。

技术背景与语言特性

TypeScript 由 Anders Hejlsberg(C# 与 Delphi 的核心设计者)主导架构。其设计深受 C# 与 Java 等强类型语言的影响:

  1. 语法特性:引入了接口(Interface)、泛型(Generics)、枚举(Enum)及访问修饰符(Access Modifiers)等特性,这些设计与 C# 和 Java 保持了高度一致。
  2. 静态类型检查:提供了严格的类型推断与检查机制,使开发者能利用 IDE 的智能提示(IntelliSense)与重构工具,降低了开发时的出错率。
  3. 面向对象编程:增强了 JavaScript 的面向对象(OOP)能力,支持类、继承、多态等特性,规范了代码结构。

设计初衷

JavaScript 原生设计的动态弱类型特性,在构建复杂 Web 应用(如大型 SPA)时,常导致代码维护成本高、重构困难以及运行时类型错误频发。TypeScript 通过引入静态类型系统,在编译阶段即可进行类型检查,显著提升了代码的健壮性与可维护性,使其更适用于企业级应用开发。

编译器演进:Project Corsa (Go 重写计划)

2025 年初,微软公开了由 Anders Hejlsberg 亲自带队的 Project Corsa,旨在用 Go 语言对 TypeScript 编译器进行原生重写。

  1. 技术选型逻辑 (Why Go?)

    • Go 的优势:官方评估认为 Go 的并发模型(Goroutines)和内存管理(GC)与现有的 TypeScript 编译器架构高度契合,代码平移(Porting)成本远低于 Rust。
    • Rust 的挑战:尽管 Rust 性能卓越,但其严格的内存所有权模型与 TS 编译器中复杂的 AST(抽象语法树)循环引用结构存在冲突,重写成本过高且不利于快速迭代。
  2. 性能预期
    官方测试数据显示,使用 Go 重写后的原生编译器(Native tsc)在大型项目上的编译速度预计提升 10 倍左右。

  3. 当前生态格局

    • 官方路线:全力推进 Go 原生编译器,预计将在 TypeScript 7.0 版本左右成为稳定选项。
    • 社区路线:现有的 SWC (Rust) 和 esbuild (Go) 主要解决“转译/类型擦除”问题。社区中尝试用 Rust 复刻完整类型检查器(如 stc 项目)的难度极大,官方方案有望填补这一高性能类型检查的空白。

TypeScript 在保留 JavaScript 灵活性的基础上,融合了静态类型语言的工程化优势。

TypeScript 核心优势

  1. 静态类型系统与编译期验证
    通过静态类型检查,TypeScript 能够在代码编译阶段识别类型不匹配及潜在的逻辑错误。相比 JavaScript 的运行时错误机制,这种提前规避风险的能力显著提升了代码交付质量。

  2. 卓越的工具链支持
    类型系统赋予了 IDE 强大的代码分析能力。开发者能获得精准的智能补全、参数提示、即时错误反馈以及安全的自动重构功能,大幅提升了开发体验与效率。

  3. 增强的可维护性与架构能力
    显式的类型定义起到了文档的作用,降低了代码阅读与理解的门槛。同时,TypeScript 提供的接口、泛型及访问控制等特性,为构建高内聚、低耦合的大型应用提供了必要的语言层面支持。

  4. 拥抱未来标准与向下兼容
    TypeScript 支持最新的 ECMAScript 语法特性,并通过编译器将其转换为兼容主流浏览器的 JavaScript 代码(如 ES5/ES6)。这使得开发者可以提前使用新特性,而无需担心环境兼容性问题。

问题解答 (FAQ)

为了更深入地理解 TypeScript,我们可以先思考以下几个关键问题:

Q1: TypeScript 可以直接在浏览器或 Node.js 中运行吗?
不能。 浏览器和 Node.js 运行时仅能原生执行 JavaScript 代码。TypeScript 代码(.ts)必须经过编译(Compilation) —— 或更准确地说是转译(Transpilation) —— 转换为标准的 JavaScript 代码(.js)后才能运行。这也就是为什么在工程中通常包含 build 流程的原因。

Q2: 使用 TypeScript 会影响代码在生产环境的运行性能吗?
不会。 TypeScript 的类型检查仅发生在编译阶段。一旦编译完成,生成的 JavaScript 代码中不再包含任何类型信息(类型擦除)。因此,TypeScript 既不会拖慢运行速度,也不会增加运行时包的体积。

Q3: 现有的 JavaScript 项目必须重写才能切换到 TypeScript 吗?
不需要。 由于 TypeScript 是 JavaScript 的超集,所有合法的 JS 代码天然就是合法的 TS 代码。你可以选择从新模块开始逐步引入 TS,或者通过配置 allowJs 选项让 TS 和 JS 文件共存,实现渐进式迁移。

Q4: 既然最终运行的是 JS,为什么不直接写 JS?
这类似于“为什么有了汇编语言还需要高级语言”。虽然最终执行的是机器码(类比 JS),但 TypeScript 提供了更高维度的抽象(接口、泛型)和安全保障(静态检查),是用来管理开发复杂度的工具,而非改变运行机制的工具。

co模块

简介

基于生成器的nodej和浏览器控制流,使用promises,让您以一种很好的方式编写非阻塞代码。

可以让Generator函数的自动执行。不用编写Generator函数的执行器。

API

co(fn*).then( val => )

通过解析一个Generator,Generator函数,或任何返回Generator的函数,并返回一个Promise

1
2
3
4
5
6
7
8
co(function* () {
return yield Promise.resolve(true);
}).then(function (val) {
console.log(val);
}, function (err) {
console.error(err.stack);
});

var fn = co.wrap(fn*)

generator转换为一个普通函数并返回Promise

1
2
3
4
5
6
7
var fn = co.wrap(function* (val) {
return yield Promise.resolve(val);
});

fn(true).then(function (val) {

});

执行逻辑

  • co(fn) 执行传入co模块的 generator 函数 fn

  • 把 fn 执行的结果 res,包装成 promise 对象

const gen = fn(); const res = gen.next() const rp = toPromise(res.value)

  • 如果是合法的 promise 这继续调用 fn 的 next 方法

rp.then((value)=> res.next())

co其实是一个返回promise对象的函数, 内部通过递归的方式调用 generator 的 next 方法

源码分析

工具方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// sllice 引用
var slice = Array.prototype.slice;

// isObject
function isObject(val) {
return Object == val.constructor;
}

//导出方法
module.exports = co['default'] = co.co = co;

//判断是否是promise对象,只要是实现了thenable的对象都可以视为promise对象
function isPromise(obj) {
return 'function' == typeof obj.then;
}

function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}

// 是否是generator函数
function isGeneratorFunction(obj) {
// 一定是GeneratorFunction构造函数的实例
// (function(){}).constructor === Function => true
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorF方法
function thunkToPromise(fn) {
var ctx = this;
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}

// arrayToPromise yeildable数组转换为promise
function arrayToPromise(obj) {
// 每一项都转换为promise
// 第二个参数为toPromise执行时的this值
return Promise.all(obj.map(toPromise, this));
}

// objectToPromise yieldables对象转为promise
function objectToPromise(obj){
// 借用 function Object(){}
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// 每一项转为promise
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
// 其他情况直接返回key值
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});

function defer(promise, key) {
// 提前在results中定义key
results[key] = undefined;
promises.push(promise.then(function (res) {
// 提前一步拿到结果
// 通常自己的写法为这里直接返回promise
// 在promise.all 中在遍历结果生成result,这样写法更简洁一点
results[key] = res;
}));
}
}

//把yeild转为Promise对象
function toPromise(obj) {
// 如果为假直接返回
if (!obj) return obj;
//如果已经是promise直接返回
if (isPromise(obj)) return obj;
// 如果是generator函数 通过co函数栈为promise
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
// this通过next方法中toPromise.call(ctx, ret.value)来绑定
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}

co 核心方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)

// 把任何东西包装成promise, 从而避免promise调用链

// 内存泄漏的问题
// https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {

// 执行Generator函数,返回Generator
if (typeof gen === 'function') gen = gen.apply(ctx, args);

// 如果没有next方法,则直接返回
if (!gen || typeof gen.next !== 'function') return resolve(gen);


onFulfilled();

/**
* @param {Mixed} res
* @return {Promise}
* @api private
* 通过 try catch 捕获执行时错误
*/

function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* @param {Error} err
* @return {Promise}
* @api private
*/

function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/


function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
// 如果可以包装成promise, 通过thenable方法继续调用onFulfilled,执行下一个next, 并把promise的值传入,当作上一个next的结果
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 抛出错误
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}

co.wrap(fn*)

1
2
3
4
5
6
7
8
9
// 通过柯理化函数,返回一个包含co执行结果的正常函数

co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};

Koa Router

中间件

中间件容器 负责不同组件和不同服务之间的交互,需要一个中间件负责统一的对服务使用

一个简单的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const koa = require('koa');

const app = new koa();

const m1 = async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}

const m2 = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
}
const m3 = async (ctx, next) => {
console.log(5);
await next();
console.log(6);
}

app
.use(m1)
.use(m2)
.use(m3)

app.listen(3000)

最终返回的结果为 1 3 5 2 4 6

也就是洋葱模型,由koa-compose模块来实现

实现洋葱模型的几个关键:

  • 统一的上下文 ctx
  • 操作先进后出 通过next控制
  • 有提前结束的机制

中间件类型

  • 应用级中间件 vue全局导航守卫
  • 路由级中间件 独享路由守卫
  • 错误处理中间件
  • 第三方中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const koa = require('koa');
const Router = require('koa-router')

const app = new koa();
const router = new Router();

const m1 = async (ctx, next) => {
// 应用级中间件最先被访问
console.log('应用级中间件');
//通过next进入路由级中间件
await next();
if (ctx.status == 404) {
ctx.body = '404'
}
}
// 路由级中间件会按照顺序访问
router.get('/', async (ctx, next) => {
console.log('路由级中间件1');
await next()
})

router.get('/', async (ctx, next) => {
console.log('路由级中间件2');
ctx.body = '路由'
})

app.use(router.routes());

app
.use(m1)

app.listen(3000)

koa 和 express 比较

express 通过connect添加中间件 封装了路由,视图, 异步处理使用callback (深层次的错误不能捕获)

koa 依赖于co模块,不包含任何中间件, 处理了回调 (使用了async await) 和错误处理(使用了try catch),

处理get post 请求参数

koa-body

静态资源中间件

koa-static

性能优化基础

  • prompt for unload

为卸载页面作准备, 释放js资源

  • DNS domainLookupStart domainLookupEnd

DNS 服务器会影响解析时间,DNS基于UDP协议

DNS (Domain Name System) 域名系统,用于将域名转换为IP

顶级域名: .com .org .club

域名资源记录: 域名服务商的配置记录 (3A,4A)

域名解析流程:

  • TCP

可能会产生ssl的握手(secureConnetionStart),需要放在nigix上处理

服务器的连接数会影响响应的速度,也受到物理距离的影响

  • request response

请求开始到响应开始包括,数据传输时间,服务器处理时间,服务器请求时间,服务器渲染时间

服务端优化: 服务端缓存,sql查询时间,服务端渲染,生成数据大小(使用压缩)

三次握手四次挥手

序列号seq:占4个字节,用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生;给字节编上序号后,就给每一个报文段指派一个序号;序列号seq就是这个报文段中的第一个字节的数据编号。

确认号ack:占4个字节,期待收到对方下一个报文段的第一个数据字节的序号;序列号表示报文段携带数据的第一个字节的编号;而确认号指的是期望接收到下一个字节的编号;因此当前报文段最后一个字节的编号+1即为确认号。

确认ACK:占1位,仅当ACK=1时,确认号字段才有效。ACK=0时,确认号无效

同步SYN:连接建立时用于同步序号。当SYN=1,ACK=0时表示:这是一个连接请求报文段。若同意连接,则在响应报文段中使得SYN=1,ACK=1。因此,SYN=1表示这是一个连接请求,或连接接受报文。SYN这个标志位只有在TCP建产连接时才会被置1,握手完成后SYN标志位被置0。

终止FIN:用来释放一个连接。FIN=1表示:此报文段的发送方的数据已经发送完毕,并要求释放运输连接

PS:ACK、SYN和FIN这些大写的单词表示标志位,其值要么是1,要么是0;ack、seq小写的单词表示序号。

第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

  • 为什么连接的时候是三次握手,关闭的时候却是四次握手?

因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

  • 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

  • 为什么不能用两次握手进行连接?

3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

  • 如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

缓存策略

强制缓存

浏览器不会像服务器发送任何请求,直接从本地缓存中读取文件并返回Status Code: 200 OK

200 from disk cache: 不访问服务器,已经在之前的某个时间加载过该资源,直接从硬盘中读取缓存,关闭浏览器后,数据依然存在,此资源不会随着该页面的关闭而释放掉下次打开仍然会是from disk cache。

200 form memory cache : 不访问服务器,一般已经加载过该资源且缓存在了内存当中,直接从内存中读取缓存。浏览器关闭后,数据将不存在(资源被释放掉了),再次打开相同的页面时,不会出现from memory cache。

header参数

Expires:过期时间,如果设置了时间,则浏览器会在设置的时间内直接读取缓存,不再请求

Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。

(1) max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒;
(2) s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言;
(3)public:指示响应可被任何缓存区缓存;
(4)private:只能针对个人用户,而不能被代理服务器缓存;
(5)no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收到 请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。这个很容易让人产生误解,使人误 以为是响应不被缓存。实际上Cache-Control: no-cache是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。
(6)no-store:禁止一切缓存(这个才是响应不被缓存的意思)。

cache-control是http1.1的头字段,expires是http1.0的头字段,如果expires和cache-control同时存在,cache-control会覆盖expires,建议两个都写。

协商缓存

向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;

header参数

Etag/If-None-Match:

Etag是属于HTTP 1.1属性,它是由服务器(Apache或者其他工具)生成返回给前端,用来帮助服务器控制Web端的缓存验证。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。

当资源过期时,浏览器发现响应头里有Etag,则再次像服务器请求时带上请求头if-none-match(值是Etag的值)。服务器收到请求进行比对,决定返回200或304

Last-Modifed/If-Modified-Since:

Last-Modified 浏览器向服务器发送资源最后的修改时间

If-Modified-Since 当资源过期时(浏览器判断Cache-Control标识的max-age过期),发现响应头具有Last-Modified声明,则再次向服务器请求时带上头if-modified-since,表示请求时间。服务器收到请求后发现有if-modified-since则与被请求资源的最后修改时间进行对比(Last-Modified),若最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;若最后修改时间较旧(小),说明资源无新修改,响应HTTP 304 走缓存。

Last-Modifed/If-Modified-Since的时间精度是秒,而Etag可以更精确。
Etag优先级是高于Last-Modifed的,所以服务器会优先验证Etag
Last-Modifed/If-Modified-Since是http1.0的头字段

开发环境处理

处理html

html-webpack-plugin

1
npm i --save-dev html-webpack-plugin@next //webpack5
1
2
3
4
5
plugins: [
new HtmlWebpackPlugin({
template:'**.html'
})
]

处理样式

1
yarn add -D sass-node less-loader css-loader style-loader

css-loader style-loader sass-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
minimize: false, //压缩css代码, 默认false
import: true, //禁止或启用@import, 默认true
url: true, //控制url/image-set的解析,会处理成require引入
}
}
, 'sass-loader']
}
]
},
}

处理图片资源

1
yarn add -D url-loader file-laoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 小图片以bash64的方式输出
// 可以处理通过import引入的样式中的图片,js文件中引入的图片文件
// 如果图片大于指定大小会默认交给file-loader处理
// file-loader 会使用options中的配置
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 5 * 1024,
outputPath: 'assets',
name: '[hash:8].[ext]',
}
}
]
},
// 处理html中的图片
// 把html字符串中的图片引变成require的形式,
// 最终还是需要使用file-loader 和 url-loader来处理
// 如果生成的src是[object Module]需要关闭,url-loader的模块化规范
{
test: /\.html$/i,
use: [{
loader: 'html-loader',
}]
},

处理字体资源

1
2
3
4
5
6
7
8
9
{
// exclude:/\.xx$/
test: /\.(ttf|eot|svg|woff|woff2)$/,
loader:'file-loader',
options:{
name:'[path][name].[ext]'
}

}

简单的devServer搭建

1
yarn add -D webpack-dev-server

webpack.config.js 可能因为 webpack-dev-serverwebpack版本不匹配导致错误

1
2
3
4
5
devServer:{
compress: true,
contentBase: path.join(__dirname, 'dist'),
open:true
}

可以通过单独的server.js文件来使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');


const config = require('./webpack.config.js');

const options = {
contentBase: './dist',
publicPath: '/',
hot: true,
writeToDisk: true,
open: true
};

webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);

server.listen(9000, 'localhost', () => {
console.log('dev server listening on port 9000');
});

friendly-errors-webpack-plugin[https://www.npmjs.com/package/friendly-errors-webpack-plugin]

添加桌面通知
该插件没有桌面通知的原生支持,需要引入node-notifier,这样就可以了 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var NotifierPlugin = require('friendly-errors-webpack-plugin');
var notifier = require('node-notifier');
var ICON = path.join(__dirname, 'icon.png');

new NotifierPlugin({
onErrors: (severity, errors) => {
if (severity !== 'error') {
return;
}
const error = errors[0];
notifier.notify({
title: "Webpack error",
message: severity + ': ' + error.name,
subtitle: error.file || '',
icon: ICON
});
}
})
]

热部署代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
const fs = require("fs");
const path = require("path");
const vm = require("vm");

const handlerMap = {};
const hotsPath = path.join(__dirname, "hots");

// 加载文件代码 并 监听指定文件夹目录文件内容变动
const loadHandlers = async () => {
// 遍历出指定文件夹下的所有文件
const files = await new Promise((resolve, reject) => {
//https://nodejs.org/dist/latest-v14.x/docs/api/fs.html#fs_fs_readdir_path_options_callback
//异步读取指定目录下的文件和文件夹,返回一个数组
fs.readdir(hotsPath, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
// 初始化加载所有文件 把每个文件结果缓存到handlerMap变量当中
for (let f in files) {
handlerMap[files[f]] = await loadHandler(path.join(hotsPath, files[f]));
}

// 监听指定文件夹的文件内容变动
await watchHandlers();
};

// 监视指定文件夹下的文件变动
const watchHandlers = async () => {
// 这里建议用chokidar的npm包代替文件夹监听
// 监听所有子文件夹
fs.watch(hotsPath, { recursive: true }, async (eventType, filename) => {
// 获取到每个文件的绝对路径
// 包一层require.resolve的原因,拼接好路径以后,它会主动去帮你判断这个路径下的文件是否存在
//使用require.resolve函数查询模块文件名时并不会加载该模块。
const targetFile = require.resolve(path.join(hotsPath, filename));
// 使用require加载一个模块后,模块的数据就会缓存到require.cache中,下次再加载相同模块,就会直接走require.cache
// 所以我们热加载部署,首要做的就是清除require.cache中对应文件的缓存
const cacheModule = require.cache[targetFile];
// 去除掉在require.cache缓存中parent对当前模块的引用,否则会引起内存泄露,具体解释可以看下面的文章
// 《记录一次由一行代码引发的“血案”》https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63
// 《一行 delete require.cache 引发的内存泄漏血案》https://zhuanlan.zhihu.com/p/34702356
if (cacheModule.parent) {
cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
}
// 清除指定路径对应模块的require.cache缓存
require.cache[targetFile] = null;

// 重新加载发生变动后的模块文件,实现热加载部署效果,并将重新加载后的结果,更新到handlerMap变量当中
const code = await loadHandler(targetFile)
handlerMap[filename] = code;
console.log("热部署文件:", filename, ",执行结果:", handlerMap);
});
};

// 加载指定文件的代码
const loadHandler = filename => {
return new Promise((resolve, reject) => {
//https://nodejs.org/dist/latest-v14.x/docs/api/fs.html#fs_fs_readfile_path_options_callback
//读取文件中的内容 默认返回buffer
//当目录为文件夹时抛出错误
fs.readFile(filename, (err, data) => {
if (err) {
resolve(null);
} else {
try {
// 使用vm模块的Script方法来预编译发生变化后的文件代码,检查语法错误,提前发现是否存在语法错误等报错
new vm.Script(data);
} catch (e) {
// 语法错误,编译失败
reject(e);
return;
}
// 编译通过后,重新require加载最新的代码
resolve(require(filename));
}
});
});
};

loadHandlers()
  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信