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 基本类型

TS 是什么

是JavaScript的超集,添加了类型系统。 拟补JavaScript在开发大型系统时的不足。

相比 JS 的优势

  • 类型化的思维方式,开发更加严谨,提前发现错误,减少改bug的时间

  • 类型系统提高了代码的可读性,并使维护和重构更加容易

  • 补充了接口枚举等开发大型应用时JS缺失的功能。

注释

单行注释 // 快捷键 ctrl + /

多行注释 /* */ 快捷键 ctrl + shift + A (ubuntu)

类型注解

为变量添加类型约束的方式

声明变量必须指定type类型

1
let a:number;
变量命名规则

不能以数字开头,且只能包含 $_数字字母

数据类型

原始类型: Number/String/Bollean/undefined/null 对应类型注解为 number/string/boolean/undefined/null

undefined null类型 属于其他类型 never 的子类型

联合类型

1
2
3
const n:number | null | undefined;

n = 1;

如果直接赋值不需要声明type类型,类型推论

1
2
3
4
let a = 1;

// 如果重新覆盖不同的类型值会提示错误
a = '22'// 错误

也可通通过字面量赋值

1
2
let a:1|true;
a = 1;

数组类型 Array

1
2
// 指定数组中的类型
let arr:number[] = [1,2,3]
1
let arr:Array<number> = [1,2,3]
1
let arr:any[] = [1,2,3]

object

[propName:string]:any 表示任意类型的属性

1
2
3
4
5
let b:{
name:string,
age?:number,
[propName:string]:any
}

元组类型 tuple

数组类型的一种,可以指定数组中不同元素的类型

1
let arr:[string,number] = ['123',3.4]

类数组

1
2
3
4
5
function  fn(...args:any[]) {
const arg:IArguments = arguments;
}

fn(1,2,3)

枚举类型 enum

定义枚举类型

1
2
3
4
5
6
enum StatusEnum {
success = 0,
error = -1
}

const a: StatusEnum = StatusEnum.success

如果没有声明值,则返回枚举字段的索引, 如果其中一个有值,后面没有赋值的字段会把前面的值 +1 返回

1
2
3
4
5
enum Color { red, gray = 5, black}

const c:Color = Color.red //0
const c1:Color = Color.gray //5
const c2:Color = Color.black //6

常数枚举

1
2
3
4
5
const enum Color { red, gray}
console.log(Color.red,Color.gray);

//最终编译为
console.log(0/*Color.red*/,1/*Color.red*/)

任意类型 any

一个变量设置了any,相当于关闭了类型监测

声明变量如果没有执行类型,会默认设置为any类型

使用场景:第三方库没有定义的类型,类型转换的时候,数据结构太复杂太灵活,ts为document提供了一整套类型声明

1
2
const a:any = document.getElementById('app')
a.style.innerHTML = '<div />'
1
2
const ele:HTMLElement|null = document.getElementById('app')
ele!.style.color = '<div />'

void 类型

void 表示没有任何类型,一般用于定义方法的时候没有返回值

1
2
3
4
5
6
7
function fun:void(){
cosnole.log(1)
}

const fun = (): void => {
console.log(1)
}

never类型

包括 nullundefined 类型,代表从不会出现的值,这意意味着never类型只能被never类型所赋值

用于报错,没有值的类型,函数执行不完,比如事件循环,数据库的监听

1
const a: never = (() => { throw new Error })()
1
2
3
function fn(): never {
throw new Error();
}
1
2
3
4
5
function fn(): never {
while(true){}
// 需要never类型,永远不会到达的语句
cosnole.log(1)
}

unknow 类型

unknow 类型在复制给其他的变量时会检查,不允许把unknow类型复制给其他类型,如果使用的是any类型将不会检查错误

1
2
3
4
5
const d:any = '123'
const s:string = d;// 不会报错

const d:unknow = '123'
const s:string = d;// 回报错

通过类型判断允许赋值

1
2
3
if(typeof d === 'string'){
s = d
}

类型断言

1
2

s = d as string;

字面量类型

描述的是同类型不同值

1
2
let a: "aaa" | "bbb";
a = "aaa";

类型别名

1
2
3
4
type MyType = 1|2|3
const a:MyType = 1;
const a:MyType = 2;
const a:MyType = 3;

定义函数

函数参数和返回值类型必须被声明

1
2
3
4
5
6
7
8
9
function fn(a: number = 2, b?: number, ...c: Array<any>): void {
if (b) {
console.log(a, b, c);
} else {
console.log(a, b, c);
}
}

fn(1)

函数重载表示同名的函数如果参数不同函数会重载

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

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

1
2
3
4
5
6
7
function fn(a: number): number;
function fn(b: string): string;
function fn(c: any): any {
if (typeof c === 'number') {
return c + 1;
}
};

定义函数的结构

1
const fn:(a:number,b:number) => number

包装对象

自动在基本类型的对象类型之间切换,基本类型上没有方法,在内部迅速的完成一个装箱操作,把基本类型迅速包装成对象类型,然后调用对象类型的方法。

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;
}
};

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-2025 SunZhiqi

此时无声胜有声!

支付宝
微信