菜鸟的逆袭


  • Home

  • Archives

node_v8_metrics

Posted on 2019-06-10

node v8 内存一二三

基础知识

  • Node.js 进程的内存管理,都是有V8自动处理的,包括内存分配和释放。

  • 在V8内部,会为程序中的所有变量构建一个图,来表示变量间的关联关系,当变量从根节点无法触达时,就意味着这个变量不会再被使用了,即可回收。

  • 回收是一个过程性的,从快速GC到最后的Full GC,是需要一段时间的。另外,Full GC是有触发阈值的,所以可能会出现内存长期占用在一个高值,也可以算是一种内存泄漏。还有一种就是引用不释放,导致无法进入GC环节,并且一直产生新的占用,这一般会发生在Javascript业务层面。

  • 定位内存泄漏,找有引用但不被使用

V8 内存构成(以下研究基于node 8.x 版本依赖的v8版本)

一个 V8 进程的内存通常由以下几个块构成:

Heap内存分配枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// NOTE: SpaceIterator depends on AllocationSpace enumeration values being
// consecutive.
// Keep this enum in sync with the ObjectSpace enum in v8.h
enum AllocationSpace {
NEW_SPACE, // Semispaces collected with copying collector.
OLD_SPACE, // May contain pointers to new space.
CODE_SPACE, // No pointers to new space, marked executable.
MAP_SPACE, // Only and all map objects.
LO_SPACE, // Promoted large objects.

FIRST_SPACE = NEW_SPACE,
LAST_SPACE = LO_SPACE,
FIRST_PAGED_SPACE = OLD_SPACE,
LAST_PAGED_SPACE = MAP_SPACE
};

node-v8 暴露模块

  • 新生代内存区(new_space)
    • 大多数的对象都会被分配在这里,区域很小但是垃圾回收比较频繁,由两个半区域(semispace)构成
  • 老生代内存区(old_space)
    • 属于老生代,存放从新生代晋升而来的对象,可能包含对new space的引用,GC频率低,按照GC 1.4GB至少要50ms以上,非增量更是1s以上。
  • 大对象区(large_object_space)
    • 这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区
  • 代码区(code_space)
    • 代码对象,会被分配在这里,无引用。唯一拥有执行权限的内存
  • map 区(map_space)
    • 存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单

Heap内存计算

1
2
3
4
5
6
7
8
9
10
11
12
size_t Heap::Capacity() {
if (!HasBeenSetUp()) return 0;

return new_space_->Capacity() + OldGenerationCapacity();
}

size_t Heap::OldGenerationCapacity() {
if (!HasBeenSetUp()) return 0;

return old_space_->Capacity() + code_space_->Capacity() +
map_space_->Capacity() + lo_space_->SizeOfObjects();
}
  • 从上面代码可以看出v8整个heap内存容器大小即新生代+老生代+代码区+map区+大对象区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Heap::heap()
:...
// semispace_size_ should be a power of 2 and old_generation_size_ should
// be a multiple of Page::kPageSize.
max_semi_space_size_(8 * (kPointerSize / 4) * MB),
initial_semispace_size_(MB),
max_old_generation_size_(700ul * (kPointerSize / 4) * MB),
initial_max_old_generation_size_(max_old_generation_size_),
initial_old_generation_size_(max_old_generation_size_ /
kInitalOldGenerationLimitFactor),
old_generation_size_configured_(false), //是否配置老生代内存上限
...
// heap.h 头文件申明
static const int kInitalOldGenerationLimitFactor = 2;
  • 上面代码中,看到几个关键信息:
    • max_semi_space_size_,kPointerSize是指的当前系统的当前编译器的sizeof(void*)大小,即指针大小,64bits系统通常为 8,32bits 通常为 4,笔者电脑测试为16MB
    • initial_semispace_size_ 即semi_sapce_size初始为1MB
    • max_old_generation_size_ 按照上面计算方法得出1400MB
    • initial_max_old_generation_size_ 即max_old_generation_size_大小,如果后续有启动参数配置则为配置大小如 node --max-old-space-size=2000 xxx.js,表示将最大老生代内存设置为2000MB
    • initial_old_generation_size_ 同理可得到为最大老生代内存的一半
1
2
3
4
// Returns the maximum amount of memory reserved for the heap.
size_t MaxReserved() {
return 2 * max_semi_space_size_ + max_old_generation_size_;
}
  • 该函数获得当前系统当前编译器中最大的heap内存,通过计算为1432MB,笔记通过API实时调用能看到也是1432MB,可通过v8提供的getHeapStatistics()函数拿到heap_size_limit就是我们heap内存最大上限。

内存分布

内存分布

内存模块中通常分为已申请区、使用区、可使用区等。new_space则分为激活区、未激活区。

以下通过三个API获取的当前系统及程序内存环境数据,笔者已换算成MB

  • process.memoryUsage()
  • v8.getHeapStatistics()
  • v8.getHeapSpaceStatistics()
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
{
"code": 100000,
"data": {
"memoryUsage": {
"rss": 217,
"heapTotal": 181,
"heapUsed": 152,
"external": 11
},
"HeapStatistics": {
"total_heap_size": 181,
"total_heap_size_executable": 10,
"total_physical_size": 179,
"total_available_size": 1260,
"used_heap_size": 152,
"heap_size_limit": 1432,
"malloced_memory": 1,
"peak_malloced_memory": 11,
"does_zap_garbage": 0
},
"HeapSpaceStatistics": [
{
"space_name": "new_space",
"space_size": 32,
"space_used_size": 8,
"space_available_size": 9,
"physical_space_size": 32
},
{
"space_name": "old_space",
"space_size": 88,
"space_used_size": 86,
"space_available_size": 2,
"physical_space_size": 88
},
{
"space_name": "code_space",
"space_size": 9,
"space_used_size": 8,
"space_available_size": 1,
"physical_space_size": 9
},
{
"space_name": "map_space",
"space_size": 3,
"space_used_size": 3,
"space_available_size": 1,
"physical_space_size": 3
},
{
"space_name": "large_object_space",
"space_size": 50,
"space_used_size": 49,
"space_available_size": 1250,
"physical_space_size": 50
}
]
},
"msg": "metrics"
}

v8内存生命周期

假设当前有一个变量house,从创建到销毁过程大致过程如下。

  1. 这个对象被分配到了 new_space

  2. 随着程序的运行,new_space 塞满了,GC 开始清理 new_space 里的死对象,house 因为还处于活跃状态,所以没被清理出去GC

  3. 清理了两遍 new_space,发现 house 依然还活跃着,就把 house 移动到了 old_space

  4. 随着程序的运行,old_space 也塞满了,GC 开始清理 old_space,这时候发现 house 已经没有被引用了,就把 house 给清理出去了,如果一直引用,则不会被清理

第二步里,清理 new_space 的过程叫做 Scavenge(不是胡说八道,证据在下面),即空间换时间,我们把new_space分为激活和未激活两个半(semi)区域,则过程如下:

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
//heap.h

// Returns the timer used for a given GC type.
// - GCScavenger: young generation GC
// - GCCompactor: full GC
// - GCFinalzeMC: finalization of incremental full GC
// - GCFinalizeMCReduceMemory: finalization of incremental full GC with
// memory reduction
HistogramTimer* Heap::GCTypeTimer(GarbageCollector collector) {
if (IsYoungGenerationCollector(collector)) {
return isolate_->counters()->gc_scavenger();
} else {
if (!incremental_marking()->IsStopped()) {
if (ShouldReduceMemory()) {
return isolate_->counters()->gc_finalize_reduce_memory();
} else {
return isolate_->counters()->gc_finalize();
}
} else {
return isolate_->counters()->gc_compactor();
}
}
}
// 是否是新生代收集器
static inline bool IsYoungGenerationCollector(GarbageCollector collector) {
return collector == SCAVENGER || collector == MINOR_MARK_COMPACTOR;
}

static inline GarbageCollector YoungGenerationCollector() {
return (FLAG_minor_mc) ? MINOR_MARK_COMPACTOR : SCAVENGER;
}
//gc-tracer.cc
const char* GCTracer::Event::TypeName(bool short_name) const {
switch (type) {
case SCAVENGER:
return (short_name) ? "s" : "Scavenge";
case MARK_COMPACTOR:
case INCREMENTAL_MARK_COMPACTOR:
return (short_name) ? "ms" : "Mark-sweep";
case MINOR_MARK_COMPACTOR:
return (short_name) ? "mmc" : "Minor Mark-Compact";
case START:
return (short_name) ? "st" : "Start";
}
return "Unknown Event Type";
}

scavenge-cheney

scavenge-cheney算法

  1. 当活跃区满了或主动GC,from会有两个操作,且都是在经过标记后,一个清除经过标记后的非存活对象,另一个复制经过标记后存活对象到to
  2. 交换from和to
  3. 交换中如果有存活对象经过清道夫标记后标记数>1,或当前to区域占比超过25%,则直接进入old_space

mark-sweep标记清扫

第四步里,清理 old_space 的过程叫做 Mark-sweep,也就是标记和清扫,这块占用内存很大,所以没有使用 Scavenge,这个回收过程包含了若干次(2-7次)标记过程和清理过程:

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
// full GC
void Heap::CollectAllAvailableGarbage(GarbageCollectionReason gc_reason) {
// Since we are ignoring the return value, the exact choice of space does
// not matter, so long as we do not specify NEW_SPACE, which would not
// cause a full GC.
// Major GC would invoke weak handle callbacks on weakly reachable
// handles, but won't collect weakly reachable objects until next
// major GC. Therefore if we collect aggressively and weak handle callback
// has been invoked, we rerun major GC to release objects which become
// garbage.
// Note: as weak callbacks can execute arbitrary code, we cannot
// hope that eventually there will be no weak callbacks invocations.
// Therefore stop recollecting after several attempts.
if (gc_reason == GarbageCollectionReason::kLastResort) {
InvokeOutOfMemoryCallback();
}
RuntimeCallTimerScope runtime_timer(
isolate(), &RuntimeCallStats::GC_Custom_AllAvailableGarbage);
if (isolate()->concurrent_recompilation_enabled()) {
// The optimizing compiler may be unnecessarily holding on to memory.
DisallowHeapAllocation no_recursive_gc;
isolate()->optimizing_compile_dispatcher()->Flush(
OptimizingCompileDispatcher::BlockingBehavior::kDontBlock);
}
isolate()->ClearSerializerData();
set_current_gc_flags(kMakeHeapIterableMask | kReduceMemoryFootprintMask);
isolate_->compilation_cache()->Clear();
const int kMaxNumberOfAttempts = 7;
const int kMinNumberOfAttempts = 2;
for (int attempt = 0; attempt < kMaxNumberOfAttempts; attempt++) {
if (!CollectGarbage(OLD_SPACE, gc_reason,
v8::kGCCallbackFlagCollectAllAvailableGarbage) &&
attempt + 1 >= kMinNumberOfAttempts) {
break;
}
}

set_current_gc_flags(kNoGCFlags);
new_space_->Shrink();
UncommitFromSpace();
}

把当前 内存数据抽象为森林,如下
mark-sweep

清理后
mark-sweep
mark-sweep

  1. 标记从根可达的对象为白色
  2. 遍历白色对象的邻接对象,直到所有可到对象都标记为白色
  3. 循环标记若干次(2-7)
  4. 清理掉非白色的对象。

简单来说,Mark-sweep 就是把从根节点无法获取到的对象清理掉,与scavenge-cheney相比,scavenge-cheney只会复制存活对象,而新内存中本身就小,且存活对象不多,所以高效。mark-sweep则只会清除没被标记的对象,而老生代死对象少,这也就是mark-sweep针对老生代区域高效GC的原因。

mark-compact 标记整理和压缩

由于经过mark-sweep算法GC后,会出现不连续的空间,导致空间碎片,当下次需要移动大对象或对象晋升,但没有足够的空间使用,将会再次导致GC,但往往这个时候GC没必要的,因为很可能是刚刚GC过,所以怎么合理利用空间碎片就成了性能关键。于是mark-compact算法出现了。

  1. 将存活对象移动到old_space的一端
  2. 将另一端直接清空

整理之前(黑色为死对象)
整理之前

整理之后
整理之后
整理之后

我们对比下三种GC算法

GC算法对比

incremental-marking

由于GC期间,需要执行stop-the-world来保证应用程序逻辑和GC看到的一致性,所以v8中引入了incremental-marking增量标记策略,清理一会儿,执行一会儿应用程序。

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
void Heap::ReportExternalMemoryPressure() {
const GCCallbackFlags kGCCallbackFlagsForExternalMemory =
static_cast<GCCallbackFlags>(
kGCCallbackFlagSynchronousPhantomCallbackProcessing |
kGCCallbackFlagCollectAllExternalMemory);
if (external_memory_ >
(external_memory_at_last_mark_compact_ + external_memory_hard_limit())) {
CollectAllGarbage(
kReduceMemoryFootprintMask | kFinalizeIncrementalMarkingMask,
GarbageCollectionReason::kExternalMemoryPressure,
static_cast<GCCallbackFlags>(kGCCallbackFlagCollectAllAvailableGarbage |
kGCCallbackFlagsForExternalMemory));
return;
}
if (incremental_marking()->IsStopped()) {
if (incremental_marking()->CanBeActivated()) {
StartIncrementalMarking(i::Heap::kNoGCFlags,
GarbageCollectionReason::kExternalMemoryPressure,
kGCCallbackFlagsForExternalMemory);
} else {
CollectAllGarbage(i::Heap::kNoGCFlags,
GarbageCollectionReason::kExternalMemoryPressure,
kGCCallbackFlagsForExternalMemory);
}
} else {
// Incremental marking is turned on an has already been started.
const double kMinStepSize = 5;
const double kMaxStepSize = 10;
const double ms_step =
Min(kMaxStepSize,
Max(kMinStepSize, static_cast<double>(external_memory_) /
external_memory_limit_ * kMinStepSize));
const double deadline = MonotonicallyIncreasingTimeInMs() + ms_step;
// Extend the gc callback flags with external memory flags.
current_gc_callback_flags_ = static_cast<GCCallbackFlags>(
current_gc_callback_flags_ | kGCCallbackFlagsForExternalMemory);
incremental_marking()->AdvanceIncrementalMarking(
deadline, IncrementalMarking::GC_VIA_STACK_GUARD,
IncrementalMarking::FORCE_COMPLETION, StepOrigin::kV8);
}
}

理论于实践的意义

注:本文Chrome调试部分基于版本(Version 74.0.3729.169 (Official Build) (64-bit))

首先认识下Chrome DevTools内存模块(当前静态内存分布,时刻)

  • 准备好 Chrome,然后执行下面的代码
1
2
3
4
5
6
7
8
9
10
class BeikeClass {
constructor(){} //没有构造函数效果一样
}
class BeikeFangClass {
constructor(){
this.fang = new BeikeClass()
}
}

let array = new Array(100000).fill('').map(item => new BeikeFangClass())
  • 打开Chrome devtools,进入memory

内存堆快照

介绍下几个tab:

  • Constructor为构造函数
  • Distance为对象到根层级
  • Shallow Size为对象自己内存大小(不包含内部引用)
  • Retained Size为对象内存总大小且包含内部引用对象大小

上面经过过滤后会看到申明的两个类 BeikeClass、BeikeFangClass,可以看到 BeikeFangClass(6)下一层级有 BeikeClass(7),而且 2400000+3200000=5600000 也符合上面对 Shallow Size和 Retained Size的解释(瞎解释?,官网走你)

注:Retained Size 是性能调优阶段重要指标(主动GC)

怎么样才能看到内存在涨呢?看个动态的Performance之前叫Timeline

`Performance`

看,这里的波动图就能看到内存在涨。怎么操作?执行下面的代码

1
2
3
4
5
6
7
8
9
10
var x = [];

function grow() {
for (var i = 0; i < 10000; i++) {
document.body.appendChild(document.createElement('div'));
}
x.push(new Array(1000000).join('x'));
}

document.getElementById('grow').addEventListener('click', grow);

然后点击开始记录当前时刻开始一段时间内的内存使用情况,下面仔细看下这20s左右内存使用情况:

  • 首先主动GC一次,能看到内存有所下降,也就是图中标注的第一次(Major GC,其中可能包含一到多次Minor GC),Major GC通常是针对老生代、Minor GC通常针对新生代,那也就意味着Major通常比Minor慢,因为老生代内存比新生代内存大很多,算法也不相同。

  • 中间红色框标记的为向文档中插入10000个DOM,可以看到js heap有增长

  • 后续每次插入10000DOM,都能看到明显的js heap增长,同时还有nodes增长

  • 在随着DOM原来越多,系统会自动触发DOM GC,尝试回收无用DOM,以及Minor GC
    DOM GC
    Minor GC

  • 最后一次主动GC后内存也明显下降了

  • 我们还可以点击下方的 Call tree 来查看整个过程的调用树,我们能看到除Major GC、Minor GC、DOM GC之外的其他相关系统调用栈及具体信息
    Call Tree

  • 通过Event Log查看根据时间系统调用栈
    `Event Log`

怎么分析内存爆了?谁爆了?

那我们模拟一个泄漏的例子,模拟内存增长:

  • 打开 memory,然后执行下面的代码,每隔一段时间录制一段 HEAP SNAPSHOTS,然后做两两对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class BeikeClass {
    constructor(){
    this.fang = new ArrayBuffer(1000000)
    }
    }

    const _heap = {}

    setInterval(() => {
    _heap[Date.now()] = new BeikeClass()
    }, 100)

    `多次HEAP SNAPSHOTS`

  • 对于上面的图来说,我们的首先选中其中一个SNAPSHOTS比如SNAPSHOTS 7,然后修改Summary->Comparison,右侧选中SNAPSHOTS 7作对比,我们看到右侧红色框中new中比较多,也是我们需要关注的。

  • 重复上面的过程,选择对比SNAPSHOTS 8与SNAPSHOTS 7,同样能得到一个对比
    二次对比

  • 对比两张图,可以看到两次对比中BeikeClass、ArrayBuffer、string等几项都明显增长,我们点击三角展开就能定位变量最终引用比如这里的ArrayBuffer,点开后fang in BeikeiClass-1560244861634-_heap,及最终引用链(官网叫支配项)

  • 查看对象引用关系

    对象引用

最终我们也就能定位到是BeikeClass、ArrayBuffer、string几个可能是‘凶手’,从而破案。

node环境怎么操作?

node环境下结合node-heapdump和自己监听内存使用情况或使用node-memwatch

  • node-memwatch监听当前程序(V8实例)的GC事件,然后会触发leak和stats事件,leak是在内存泄漏时候会触发,stats是在GC后触发,上报数据

如果遇到node-memwatch编译报错

1
2
no matching constructor for initialization of 'String::Utf8Value'
candidate constructor not viable: requires 1 argument, but 2 were provided

修复方法两个:node升级到9以上或者修改源码,上面笔者说本文基于8.x,所以这里只能改源码了

1
String::Utf8Value utfString(isolate, str->ToString());

删除utfString第一个参数即可(别问为什么,问就是看源码)

笔者这里有一段代码示例,(我们在启动的时候可以加上 --trace_gc 参数来观察运行过程中的详细GC信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class BeikeFang {
constructor(){
this.stamp = Date.now()
}
}

class BeikeINF {
constructor(){
this.list = new BeikeFang()
}
}
// 每个10ms向GC_VARS数组push一个{class: new BeikeINF()}
global.GC_VARS = []
const timer = setInterval(() => {
var GC_VAR = {class: new BeikeINF()}
global.GC_VARS.push(GC_VAR)
}, 10);
setTimeout(() => {
clearInterval(timer)
console.log('clear')
}, 100000);

在clear之前然后每隔一段时间生成heapsnapshot

1
2
3
4
5
let filename = './' + Date.now() + '.heapsnapshot'
heapdump.writeSnapshot(filename, function(a, b){
console.log('succ filename', b)
filename = b
})

下面生成了6个快照

6个heapsnapshot

导入Chrome->memory分析

6个heapsnapshot diff
上图中能明显看到内存是增长趋势,按照之前描述的方法进行分析对比就能知道是那块变量出现问题

我们可以使用memwatch提供的diff方法进行对应两个时间点的snapshot的diff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const hd = new memwatch.HeapDiff();
let diff = null
function sleep() {
return new Promise((resolve, reject) => {
setTimeout(() => {
diff = hd.end()
resolve(diff)
}, 5000);
})
}
diff = await sleep()
ctx.ajax(diff, {
error: false,
message: 'heap diff'
})

下面是利用HeapDiff类生成的diff结果

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
{
before: {
nodes: 447998,
size_bytes: 52876240,
size: "50.43 mb"
},
after: {
nodes: 447670,
size_bytes: 52568136,
size: "50.13 mb"
},
change: {
size_bytes: -308104,
size: "-300.88 kb",
freed_nodes: 1975,
allocated_nodes: 1543,
details: [{
what: "Arguments",
size_bytes: -64,
size: "-64 bytes",
+: 0,
-: 2
},
{
what: "Array",
size_bytes: -41432,
size: "-40.46 kb",
+: 102,
-: 631
},
{
what: "BeikeFang",
size_bytes: 14112,
size: "13.78 kb",
+: 441,
-: 0
},
{
what: "BeikeINF",
size_bytes: 14112,
size: "13.78 kb",
+: 441,
-: 0
},
{
what: "Buffer",
size_bytes: -80,
size: "-80 bytes",
+: 0,
-: 1
},
{
what: "BufferList",
size_bytes: 48,
size: "48 bytes",
+: 1,
-: 0
},
{
what: "Closure",
size_bytes: 720,
size: "720 bytes",
+: 16,
-: 3
},
{
what: "Code",
size_bytes: -282656,
size: "-276.03 kb",
+: 15,
-: 392
},
{
what: "FSReqWrap",
size_bytes: -32,
size: "-32 bytes",
+: 0,
-: 1
},
{
what: "Native",
size_bytes: 344,
size: "344 bytes",
+: 6,
-: 1
},
{
what: "Number",
size_bytes: -16,
size: "-16 bytes",
+: 0,
-: 1
},
{
what: "Object",
size_bytes: 14288,
size: "13.95 kb",
+: 445,
-: 2
},
{
what: "Promise",
size_bytes: 384,
size: "384 bytes",
+: 4,
-: 0
},
{
what: "PromiseWrap",
size_bytes: 192,
size: "192 bytes",
+: 4,
-: 0
},
{
what: "ReadableState",
size_bytes: 192,
size: "192 bytes",
+: 1,
-: 0
},
{
what: "Socket",
size_bytes: 248,
size: "248 bytes",
+: 1,
-: 0
},
{
what: "String",
size_bytes: -8856,
size: "-8.65 kb",
+: 1,
-: 212
},
{
what: "TCP",
size_bytes: 32,
size: "32 bytes",
+: 1,
-: 0
},
{
what: "TickObject",
size_bytes: -128,
size: "-128 bytes",
+: 0,
-: 2
},
{
what: "Timeout",
size_bytes: 176,
size: "176 bytes",
+: 1,
-: 0
},
{
what: "Timer",
size_bytes: 32,
size: "32 bytes",
+: 1,
-: 0
},
{
what: "TimersList",
size_bytes: 72,
size: "72 bytes",
+: 1,
-: 0
},
{
what: "WritableState",
size_bytes: 224,
size: "224 bytes",
+: 1,
-: 0
},
{
what: "system / Context",
size_bytes: 56,
size: "56 bytes",
+: 3,
-: 2
}]
}
}

从diff的结果看,明显看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
what: "BeikeFang",
size_bytes: 14112,
size: "13.78 kb",
+: 441,
-: 0
},
{
what: "BeikeINF",
size_bytes: 14112,
size: "13.78 kb",
+: 441,
-: 0
},

那我们还可以进行进一步的diff,继续采用这种方式进行选择diff,最终确认内存爆掉的凶手

在node中查看GC数据

上面一节中说到了通过添加--trace_gc来查看运行过程中详细的GC数据

node-trace_gc

  • 我们重点关注这里的红框部分,左边的是通过memwatch.stats函数监听得到的数据(后续会讲),右边的是通过启动参数得到的GC算法。能看到前面说的Mark-sweep、scavenge两个算法,针对老生代和新生代。

内存使用

  • 作用域

    1
    2
    3
    const foo = function(){
    let inner = {}
    }

    上面的代码foo函数没执行一次,都会生成一个foo的函数作用域,同时foo的局部变量也在函数作用域中,执行结束函数作用域也随之销毁,局部变量亦然。局部变量存活周期很短,会首先分配到新生代的From区域,函数执行结束后,也就被GC掉了。

  • 作用域链

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const foo1 = function(){
    const foo2 = function(){
    let inner_var = 1
    return (function(){
    return inner_var
    }())
    }
    foo2()
    }
    foo1()

    上面的代码foo1在执行的时候,首先生成foo1的Function Scope,然后进入foo2的Function Scope,到里面的闭包中,return inner_var时,闭包的Function Scope里没有inner_var,然后找foo2的Function Scope,找到了其中的局部变量,然后返回。这里的如果foo2里作用域没有inner_var则再向上找,直到global Scope。

  • 主动释放变量

    根据前面讲到的GC原则,我们在编码的时候要注意主动释放不用的内存变量。全局上的变量是整个APP生命周期可访问,所以这部分的变量会很快放到老生代,所以如果有未使用的或用过后不再使用的变量,及时释放。对于局部变量而言,v8本身的GC就够用了,除非手抖搞成了全局的。释放变量可通过delete或重新赋值。

查看内存数据

1
2
3
4
5
6
7
8
9
// 显示内存
const showMem = function() {
const mem = process.memoryUsage();
const format = function(bytes) {
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};
console.log('Process: heapTotal ' + format(mem.heapTotal) + ' heapUsed ' + format(mem.heapUsed) + ' rss ' + format(mem.rss));
console.log('---------------------------------------------------------------------------');
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不停分配内存但不释放
const useMem = function() {
const size = 20 * 1024 * 1024;
const arr = new Array(size);
for (let i = 0; i < size; i++) {
arr[i] = 0;
}
return arr;
};
const total = [];
for (let j = 0; j < 15; j++) {
showMem();
total.push(useMem());
}
showMem();

程序down

第一个红框内:我们看到上面的分配内存只执行了9次,v8内存就爆了,heapTotal 1437.03 MB heapUsed 1352.93 MB rss 1377.70 MB第九次中,申请的heap内存总共1437.03MB,已使用1352.93MB,常驻内存1377.70MB,在第十次分配中就爆了。
第二个红框内:我们看到相关的GC数据,在第九次后尝试GC老生代内存失败。
第三个红框内:v8内存爆掉,进程down,给出了相关的js stack trace,我们可以明确看到useMem就是凶手。

当然这个时候再访问系统也就GG了。

堆外存

在上面我们用Array发现其分配内存是在v8的heap中,Buffer则不会通过v8来分配,是Node自己处理分配的,我们把useMem换成Buffer再看一遍

1
2
3
4
5
6
7
8
const useMem = function () {
const size = 200 * 1024 * 1024;
const arr = Buffer.alloc(size);
for (let i = 0; i < size; i++) {
arr[i] = 0;
}
return arr;
};

分配后的增量GC
分配中常驻内存

这里我们执行了20次,执行后有多次的full_gc,还有多次的inc_gc,分配很频繁,前面也说到了,v8的GC原则中,在很频繁触发GC的时候会采用inc_gc也就是增量的,这样保证程序能及时响应我们请求。而且我们看到总堆内存和使用堆内存变化并不大,最大的就是常驻内存变化一直增加,这也就说明Buffer在node环境中不是通过v8的分规则进行分配的,我们在适当的时候也可以用这种方法突破v8的限制,当然我们也可以使用前面说的通过--max-old-space-size参数启动的时候指定。

我们可以看下对应的leak事件中的数据,其中包含事件开始时间和结束时间(NODE_UNIXTIME_V8类型),在五次GC过程中内存增长了(字节),这里只有内存(疑似)泄漏,没有详细的原因说明

1
2
3
4
5
6
{
start: 2019-06-28T06:06:53.000Z,
end: 2019-06-28T06:07:05.000Z,
growth: 584568,
reason: 'heap growth over 5 consecutive GCs (12s) - 167.25 mb/hr'
}

我们可以按照上面讲的Comparison方式来对比我们泄漏前和泄漏后的堆内存变化,看那些增长明显

不过需要注意:我们在leak事件里不能主动结束HeapDiff()的end(),在leak会提前结束,所以我们还是在leak里手动生成heapsnapshot比较靠谱。

我是怎样搞定webpack4静态资源的

Posted on 2019-01-08

这篇文章说说webpack的静态资源

前端工程化

提到前端工程化,现在大家避免不开的可能就是webpack了,那webpack经历了多个版本,不同版本之间差异也是比较折磨人的。

笔者经历过(使用)三个大版本,webpack2.x webpack3.x webpack4.x,现在大家用得最多的也就是4.x了,因为4.x支持按需加载了,这就很厉害了呀,既然能按需加载,那肯定涉及到代码的拆分,当然拆分是webpack自己做的,是否启用按需加载这是个也是我们自己配置决定的,

未完(没空写)

自定义了一个工具 hpack直通车,有兴趣的可以先看

XHR原生对象到底怎么仿AJAX?

Posted on 2019-01-08

XHR(XMLHttpRequest)

— MDN文档

XHR是浏览器提供的能与服务端进行信息交互的对象,支持多种协议。

此接口继承了 XMLHttpRequestEventTarget 和 EventTarget 的属性。

属性

MLHttpRequest.onreadystatechange

  • 当readyState属性发生变化时调用的EventHandler。

XMLHttpRequest.readyState 只读

  • 返回 一个unsigned short 即无符号短整型,请求的状态码。

XMLHttpRequest.response 只读

  • 返回ArrayBuffer、Blob、Document、DOMString},具体是哪种类型取决于
    XMLHttpRequest.responseType的值。其中包含响应体body。

XMLHttpRequest.responseText 只读

  • 返回一个DOMString},该DOMString}包含对请求的响应,如果请求未成功或尚未发送,则返回null。

XMLHttpRequest.responseType

  • 定义响应类型的枚举值。

XMLHttpRequest.responseURL 只读

  • 返回响应的序列化URL,如果URL为空,则返回空字符串。

XMLHttpRequest.responseXML 只读 Not available to workers

  • 返回一个Document,其中包含该请求的响应,如果请求未成功、尚未发送或不能解析为XML或HTML,则返回null。

XMLHttpRequest.status 只读

  • 返回 unsigned short 即无符号短整型请求响应状态。

XMLHttpRequest.statusText 只读

  • 返回一个DOMString},其中包含HTTP服务器返回的响应状态。与 XMLHTTPRequest.status不同的是,它包括响应状态的整个文本(例如,“200 OK”)。

注意:根据HTTP/2规范(8.1.2.4响应伪标头字段),HTTP/2没有定义一种方式来携带HTTP/1.1状态行中包含的版本或原因短语。

XMLHttpRequest.timeout

  • unsigned long 即无符号长整型,表示该请求的最大请求时间(毫秒),超过该时间请求会自动结束。

XMLHttpRequestEventTarget.ontimeout

  • 当请求超时调用的EventHandler。{ { gecko_minversion_inline(“ 12.0 “)} }

XMLHttpRequest.upload 只读

  • XMLHttpRequestUpload,表示上传过程。

XMLHttpRequest.withCredentials

  • Boolean,用来指定跨域的请求是否应该使用证书(如cookie或授权header头)。

以下为非标准

XMLHttpRequest.channel只读

  • nsIChannel,对象在执行请求时使用的通道。

XMLHttpRequest.mozAnon只读

  • 一个布尔值,如果为真,请求将在没有cookie和身份验证header头的情况下发送。

XMLHttpRequest.mozSystem只读

  • 一个布尔值,如果为真,则在请求时不会强制执行同源策略。

XMLHttpRequest.mozBackgroundRequest

  • 一个布尔值,它指示对象是否是后台服务器端的请求

XMLHttpRequest.mozResponseArrayBuffer 已废弃 Gecko 6 只读

  • 一个ArrayBuffer类型,把请求的响应作为一个TypedArrays。

XMLHttpRequest.multipart已废弃 Gecko 22

  • 这个Gecko的独有属性,是一个布尔值,在Firefox/Gecko 22中被删除了。请使用Server-Sent Events, Web Sockets, 或来自进度事件的responseText代替

方法

XMLHttpRequest.abort()

  • 如果请求已经被发送,则立刻中止请求.

XMLHttpRequest.getAllResponseHeaders()

  • 以字符串的形式返回所有用CRLF分隔的响应头,如果没有收到响应,则返回null。

XMLHttpRequest.getResponseHeader()

  • 返回包含指定响应头的字符串,如果响应尚未收到或响应中不存在该报头,则返回null。

XMLHttpRequest.open()

  • 初始化一个请求。该方法只能JavaScript代码中使用,若要在native code中初始化请求,请使用openRequest()。

XMLHttpRequest.overrideMimeType()

  • 重写由服务器返回的MIME type。

XMLHttpRequest.send()

  • 发送请求。如果请求是异步的(默认),那么该方法将在请求发送后立即返回。

XMLHttpRequest.setRequestHeader()

  • 设置HTTP请求头的值。您必须在open()之后、send()之前调用setRequestHeader()这个方法。

说完属性和方法,到底怎么封装?

一般情况下,我们都会面向对象的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var ajax = (function() {
if (typeof XMLHttpRequest !== 'undefined') {
return new XMLHttpRequest();
} else if (typeof ActiveXObject !== 'undefined') {
if (typeof arguments.callee.activeXString !== 'string') {
var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
for (var i = 0,len = versions.length;i < len;i++) {
try {
var xhr = new ActiveXOject(versions[i]);
arguments.callee.activeXString = versions[i];
return xhr;
} catch (ex) {
continue;
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("no xhr object available");
}
}());

上面代码其实就是获取一个大当前浏览器支持的XHR对象,如果是现代浏览器那直接使用XMLHttpRequest对象,否则就是IE系列

接着就是基于XHR对象封装对应方法

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
var AJAX = function(ops) {
var root = this;
var req = ajax; //上面生成的ajax对象
root.url = ops.url;
root.type = ops.type || 'responseText';
root.contentType = ops.contentType || 'application/x-www-form-urlencoded';
root.method = ops.method || 'GET';
root.async = ops.async || true;
root.data = ops.data || {};
root.complete = ops.complete || function() {};
root.success = ops.success || function(){};
root.error = ops.error || function (s) { alert(root.url+'->status:'+s+'error!')};
root.abort = req.abort;
root.timeout = ops.timeout;
root.setData = function (data) {
for(var d in data) {
root.data[d] = data[d];
}
}
root.send = function () {
var datastring = root.contentType === 'application/x-www-form-urlencoded'? formatData(root.data) : root.data,
sendstring,get = false,
async = root.async,
complete = root.complete,
method = root.method,
type = root.type,
contentType = root.contentType;
if(method === 'GET') {
root.url += '?'+datastring;
get = true;
}
req.open(method, root.url, async);
if(!get) {
req.setRequestHeader("Content-type",contentType);
sendstring = datastring;
}
//在send之前重置onreadystatechange方法,否则会出现新的同步请求会执行两次成功回调(chrome等在同步请求时也会执行onreadystatechange)
req.onreadystatechange = async ? function () {
if (req.readyState == 4){
complete();
if(req.status == 200) {
root.success(req[type]);
} else {
root.error(req.status);
}
}
} : null;
req.send(sendstring);
if (!async) {
complete();
root.success(req[type]);
}
}
root.url && root.send();
};

最后来一手正儿八经的封装

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
;(function (name, root, factory) {
var module = factory(root);
if (typeof define === 'function' && define.amd) { //require AMD
define(function () {
return module;
});
} else if (typeof exports === 'object') { //common CMD
module.exports = module;
} else {
root[name] = module;
}
window[name] = module
})('_ajax', this, function(){
var xhr = function() {
var ajax = (function() {
if (typeof XMLHttpRequest !== 'undefined') {
return new XMLHttpRequest();
} else if (typeof ActiveXObject !== 'undefined') {
if (typeof arguments.callee.activeXString !== 'string') {
var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
for (var i = 0,len = versions.length;i < len;i++) {
try {
var xhr = new ActiveXOject(versions[i]);
arguments.callee.activeXString = versions[i];
return xhr;
} catch (ex) {
continue;
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("no xhr object available");
}
}());
var formatData = function(fd) {
var res = '';
for (var f in fd) {
res += f+'='+fd[f]+'&';
}
return res.slice(0, -1);
};
var AJAX = function(ops) {
var root = this;
var req = ajax;
root.url = ops.url;
root.type = ops.type || 'responseText';
root.contentType = ops.contentType || 'application/x-www-form-urlencoded';
root.method = ops.method || 'GET';
root.async = ops.async || true;
root.data = ops.data || {};
root.complete = ops.complete || function() {};
root.success = ops.success || function(){};
root.error = ops.error || function (s) { alert(root.url+'->status:'+s+'error!')};
root.abort = req.abort;
root.timeout = ops.timeout;
root.setData = function (data) {
for(var d in data) {
root.data[d] = data[d];
}
}
root.send = function () {
var datastring = root.contentType==='application/x-www-form-urlencoded'? formatData(root.data) : root.data,
sendstring,get = false,
async = root.async,
complete = root.complete,
method = root.method,
type = root.type,
contentType = root.contentType;
if(method === 'GET') {
root.url += '?'+datastring;
get = true;
}
req.open(method, root.url, async);
if(!get) {
req.setRequestHeader("Content-type",contentType);
sendstring = datastring;
}
//在send之前重置onreadystatechange方法,否则会出现新的同步请求会执行两次成功回调(chrome等在同步请求时也会执行onreadystatechange)
req.onreadystatechange = async ? function () {
// console.log('async true');
if (req.readyState ==4){
complete();
if(req.status == 200) {
root.success(req[type]);
} else {
root.error(req.status);
}
}
} : null;
req.send(sendstring);
if(!async) {
//console.log('async false');
complete();
root.success(req[type]);
}
}
root.url && root.send();
};
return function(ops) {return new AJAX(ops);}
}();
return xhr
})

webpack_plugins

Posted on 2018-09-28

webpack插件

一个简单的 plugin

plugin 的实现可以是一个类,使用时传入相关配置来创建一个实例,然后放到配置的 plugins 字段中,而 plugin 实例中最重要的方法是 apply,该方法在 webpack compiler 安装插件时会被调用一次,apply 接收 webpack compiler 对象实例的引用,你可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程,以便完成更多其他的构建任务。

下边的这个例子,是一个可以创建 webpack 构建文件列表 markdown 的 plugin,实现上相对简单,但呈现了一个 webpack plugin 的基本形态。

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
class FileListPlugin {
constructor(options) {}

apply(compiler) {
// 在 compiler 的 emit hook 中注册一个方法,当 webpack 执行到该阶段时会调用这个方法
compiler.hooks.emit.tap('FileListPlugin', (compilation) => {
// 给生成的 markdown 文件创建一个简单标题
var filelist = 'In this build:\n\n'

// 遍历所有编译后的资源,每一个文件添加一行说明
for (var filename in compilation.assets) {
filelist += ('- '+ filename +'\n')
}

// 将列表作为一个新的文件资源插入到 webpack 构建结果中
compilation.assets['filelist.md'] = {
source: function() {
return filelist
},
size: function() {
return filelist.length
},
}
})
}
}

module.exports = FileListPlugin

webpack 4.0 版本之前使用的是旧版本的 tapable,和新版本插件API有不少区别,基本注册事件一致,只是注册改变了。

开发和调试 plugin

你要在本地开发和调试 webpack plugin 是很容易的一件事情,你只需要创建一个 js 代码文件,如同上述的例子一样,该文件对外暴露一个类,然后在 webpack 配置文件中引用这个文件的代码,照样运行 webpack 构建查看结果即可。大概的配置方式如下:

1
2
3
4
5
6
7
8
9
// 假设我们上述那个例子的代码是 ./plugins/FileListPlugin 这个文件
const FileListPlugin = require('./plugins/FileListPlugin.js')

module.exports = {
// ... 其他配置
plugins: [
new FileListPlugin(), // 实例化这个插件,有的时候需要传入对应的配置
],
}

webpack 中的事件钩子

重点关注的是webpack插件钩子,下面重点说下

先来一波文档充充饥 compiler事件钩子和compilation事件钩子

我们可以看到在事件钩子列表中看到,webpack 中会有相当多的事件钩子,基本覆盖了 webpack 构建流程中的每一个步骤,你可以在这些步骤都注册自己的处理函数,来添加额外的功能,这就是 webpack 提供的 plugin 扩展

1
2
3
4
5
6
7
8
9
10
11
12
this.hooks = {
// 这里的声明的事件钩子函数接收的参数是 compilation,
shouldEmit: new SyncBailHook(["compilation"]),
// 这里接收的参数是 stats,以此类推
done: new AsyncSeriesHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compilation"]),
run: new AsyncSeriesHook(["compilation"]),
emit: new AsyncSeriesHook(["compilation"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
thisCompilation: new SyncHook(["compilation", "params"]),
};

上面是webpack事件钩子申明的方式,可以看到有很多类型的事件钩子,从这里你可以看到各个事件钩子函数接收的参数是什么,你还会发现事件钩子会有不同的类型,例如 SyncBailHook,AsyncSeriesHook,SyncHook,接下来我们再介绍一下事件钩子的类型以及我们可以如何更好地利用各种事件钩子的类型来开发我们需要的 plugin。

了解事件钩子类型

上述提到的 webpack compiler 中使用了多种类型的事件钩子,根据其名称就可以区分出是同步还是异步的,对于同步的事件钩子来说,注册事件的方法只有 tap 可用,例如上述的 shouldEmit 应该这样来注册事件函数的

1
2
3
apply(compiler) {
compiler.hooks.shouldEmit.tap('PluginName', (compilation) => { /* ... */ })
}

但如果是异步的事件钩子,那么可以使用 tapPromise 或者 tapAsync 来注册事件函数,tapPromise 要求方法返回 Promise 以便处理异步,而 tapAsync 则是需要用 callback 来返回结果,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
compiler.hooks.done.tapPromise('PluginName', (stats) => {
// 返回 promise
return new Promise((resolve, reject) => {
// 这个例子是写一个记录 stats 的文件
fs.writeFile('path/to/file', stats.toJson(), (err) => err ? reject(err) : resolve())
})
})

// 或者
compiler.hooks.done.tapAsync('PluginName', (stats, callback) => {
// 使用 callback 来返回结果
fs.writeFile('path/to/file', stats.toJson(), (err) => callback(err))
})

// 如果插件处理中没有异步操作要求的话,也可以用同步的方式
compiler.hooks.done.tap('PluginName', (stats, callback) => {
callback(fs.writeFileSync('path/to/file', stats.toJson())
})

然而 tapable 这个工具库提供的钩子类型远不止这几种,多样化的钩子类型,主要是为了能够覆盖多种使用场景:

  1. 连续地执行注册的事件函数

  2. 并行地执行注册的事件函数

  3. 一个接一个地执行注册的事件函数,从前边的事件函数获取输入,即瀑布流的方式
    异步地执行注册的事件函数

  4. 在允许时停止执行注册的事件函数,一旦一个方法返回了一个非 undefined 的值,
    就跳出执行流

除了同步和异步的区别,我们再参考上述这一些使用场景,以及官方文档的 Plugin API,进一步将事件钩子类型做一个区分。

名称带有 parallel 的,注册的事件函数会并行调用,如:

AsyncParallelHook、AsyncParallelBailHook

名称带有 bail 的,注册的事件函数会被顺序调用,直至一个处理方法有返回值(ParallelBail 的事件函数则会并行调用,第一个返回值会被使用):

SyncBailHook、AsyncParallelBailHook、AsyncSeriesBailHook

名称带有 waterfall 的,每个注册的事件函数,会将上一个方法的返回结果作为输入参数,如:

SyncWaterfallHook、AsyncSeriesWaterfallHook

通过上面的名称可以看出,有一些类型是可以结合到一起的,如 AsyncParallelBailHook,这样它就具备了更加多样化的特性。

了解了 webpack 中使用的各个事件钩子的类型,才能在开发 plugin 更好地去把握注册事件的输入和输出,同步和异步,来更好地完成我们想要的构建需求。

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
// plugin 的实现可以是一个类, 使用时传入相关配置来创建一个实例, 然后放到配置的 plugins 字段中, 而 plugin 实例中最重要的方法是 apply, 
// 该方法在 webpack compiler 安装插件时会被调用一次,apply 接收 webpack compiler 对象实例的引用,
// 你可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程, 以便完成更多其他的构建任务。

const chalk = require('chalk')
const SyncHook = require('tapable').SyncHook;

const pluginName = 'FileListPlugin'
const Log = (str, obj) => {console.log(chalk.blue('\n'+str));console.log(obj, +'\n')}

class FileListPlugin {
constructor(options) {
this.options = options
}

apply(compiler) {
if (compiler.hooks.myCustomHook) throw new Error('Already in use');
compiler.hooks.myCustomHook = new SyncHook(['a', 'b', 'c']);

// 在 compiler 的 emit hook 中注册一个方法,当 webpack 执行到该阶段时会调用这个方法
compiler.hooks.emit.tap(pluginName, (compilation) => {
console.log('emit')
compiler.hooks.myCustomHook.call(1, 2, 3);
let chunkslist = 'In this build chunks:\n\n'
let chunks = compilation.chunks
// console.log('chunks', chunks)
for (let chunkid of chunks) {
// Log('chunkid', chunkid)
chunkslist += ('- ' + chunkid + '\n')
}
// 将 chunks 信息写下来
compilation.assets['chunkslist.md'] = {
source: function () {
return chunkslist
},
size: function () {
return chunkslist.length
},
}

let filelist = 'In this build:\n\n'
// 遍历所有编译后的资源,每一个文件添加一行说明
for (let filename in compilation.assets) {
filelist += ('- ' + filename + '\n')
}

// 将列表作为一个新的文件资源插入到 webpack 构建结果中
compilation.assets['filelist.md'] = {
source: function () {
return filelist
},
size: function () {
return filelist.length
},
}
})

compiler.hooks.compile.tap(pluginName, (compilation) => {
Log('compile', compilation.chunks)
})
compiler.hooks.compilation.tap(pluginName, (compilation) => {
// Log('compilation', compilation.chunks)
compilation.hooks.optimize.tap(pluginName, (coms) => {
Log('compilation.hooks', coms)
})
})
compiler.hooks.done.tap(pluginName, (compilation) => {
Log('done', compilation.chunks)
})
compiler.hooks.failed.tap(pluginName, (compilation) => {
Log('failed', compilation.chunks)
})
compiler.hooks.afterEmit.tap(pluginName, (compilation) => {
Log('afterEmit', compilation.chunks.length)
})
compiler.hooks.run.tapAsync(pluginName, (compilation,callback) => {
Log('run', compilation.chunks)
callback()
})
compiler.hooks['watchRun'].tap(pluginName, (compilation) => {
Log('watchRun', compilation.chunks)
})
}
}

module.exports = FileListPlugin

bucky-core原理及实现细节

Posted on 2018-08-16

bucky基本原理

挂载hooks文件

包含error, config, bodyparser, formparser, rewrite, log, csrf, cors, response, view, static, context, mysql, redis, api, model, service, action, socket, lift,二十个模块定义APP错误日志,监听启动错误日志

config配置

config业务配置,模块会传入APP实例,app参数配置,但这个时候 app.config 上只有appPath,获取用户config配置路径,挂载 package.json 信息
ignoreConfigFiles是否忽略配置,挂在lodash,并将_.js挂入忽略配置数组
尝试获取env.js下的配置,包含当前node环境

抓取configs/*.js文件,排除忽略的配置,获取对应文件的文件路径,必须先加载app.js,loadConfig做深度merge当前环境,再delete app路径防止重复加载,proxy设置,然后加载其他config文件,加载文件的时候有错则放入app.initErrors里。

如果存在socket文件且开启socket,启动http服务

中间件:代理请求头,重置或重写代理请求头

中间件:设置请求头uuid和sequence

bodyparser配置

获取在config加载阶段挂载到app.config上的bodyparser
主要是设置解析请求body的参数,比如上传文件大小限制等等

中间件:bodyparser

formparser配置

获取在config加载阶段挂载到app.config上的formparser

中间件:处理表单文件内容和文件内容,将表单数据挂载到ctx

rewrite配置

获取在config加载阶段挂载到app.config上的rewrite

中间件:处理用户自定义rewrite规则,这里会用到两个处理规则函数,match和makeTo,match处理匹配命中规则,from规则支持正则、函数、字符串,正则也就是整儿白净的正则,函数的话,会给函数传入请求头部信息,返回非null则认为是匹配成功,并返回匹配数组。字符串则使用minimatch规则,minimatch这里面可能有个坑,比如你想转一个路由,比如a/b=>a/b/这种,那么minimatch会认为这两个是一样的minimatch("a/b/", "a/b") // true!,makeTo处理跳转规则,to规则支持函数和字符串,函数则自行处理,字符串则替换字符串中的${index}为match规则中的数据,返回处理后数据,然后解析该url,合并request的query和替换path,也就是说路径可自定义,query参数为merge方式
获取在config加载阶段挂载到app.config上的redirect

中间件:处理用户自定义redirect规则,处理匹配规则经过matchFrom和makeTo处理,然后构造目标url,最后重定向这个目标url

log配置

写日志采用第三方库,log4js

中间件:触发access日志,并处理input/output两种情况
重写console.log和console.error,其中分别触发application、error日志

csrf配置

跨站请求伪造,简单理解攻击者盗用了你的身份,以你的名义发送恶意请求
中间件:用来生成csrf token

cors配置

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

中间件:每次请求都会重新绑定cors检测函数到本次请求的上下文上也就是·,然后在action执行的时候回检测是否有action的cors设置参数,有则检验并设置cors响应头

设置cors参数支持函数、字符串、Bool类型,启用cors后设置响应头,*/用户自定义/
如果设置并非*,则头中必须设置Vary为Origin,目地告诉客户端服务端对不同源返回不同内容

设置headers,将请求中设置headers,作为相应头的headers传回,请求的headers会作为预检请求的headers,目前告诉服务器实际请求使用的headers,同理告诉客户端实际允许的headers

设置methods,同理headers

设置expose-headers,在跨域请求中,xhr对象中getResponseHeader()只能获取基本的响应头,cache-control、content-language、content-type、expires、last-modified、pragma,访问其他头需要服务端设置响应头expose-headers白名单

设置max-age,设置改参数表示preflight(预检)请求缓存多久有效

设置credentials,表示当前浏览器的是否被允许读取response的内容,如果是预检请求中,则表示实际请求是否可以使用credentials

response配置

封装相应函数,200、302、403、404、500,同时支持自定义response

中间件:挂载上述封装方法

中间件:挂载自定义方法到当前请求上下文,自定义response中,会传入事先挂载到app.config上的response配置

view配置

获取当前用户view配置,模板类型,编译调试参数等
获取views模板匹配路由表,目前也就是让/映射到/views/下
构造render函数,renderTemplate函数,函数参数当前appPath、待匹配路由表,当前模板配置
取出当前配置参数,类型、数据、母模板、本次render类型,支持ejs/pug两种,支持缓存(内存),内部实现先查看是否有使用及有无缓存,有则返回,注意这里挂载内存上的是编译后的模板函数,不包含数据
构造真正的带数据模板,函数参数为是否string、view路径,数据,viewConfig(目前仅使用layout参数),构造view路径,数据(merge 本次data到全局data),然后拿构造的view路径去待匹配的路由表中查view文件,然后构造view文件路径,然后拿着view文件路径和数据data使用事先构造的模板函数进行编译,这个时候编译后的只是文档片段(html),尚未使用,这个时候查是否有layout母模板,有则再匹配和编译一次,本次编译使用的数据为之前的data和{body:html},最后将这些数据绑定到本次的请求上下文对象上,比如 Object.assign(this.response, { type: 'html', body: html }),这样也就完成了渲染

中间件:将构造构造render函数绑定到response和本次请求上下文,两种方式字符串和文档

static配置

首先还是merge用户和默认配置,生成source_buffer,生成Static对象,该对象提供几种获取文件方式参数,获取文件的md5值、获取文件的buffer类型数据(这里有缓存机制,即cache配置)、获取文本、获取base64类型

构造static文件路由表,同时如果有mix配置则走mix文件,如果有manifest配置,则存到Static.assets,assets下存的是文件路径和md5后的映射关系,最后暴露Static到global

中间件:在缓存和manifest同时开启情况下,存static文件的origin=>source关系,然后根据传入文件路径,返回指定类型的文件数据,这里会处理etag、lastModified,且默认是*跨域,cache-control时间默认10s,expires默认当前服务器时间+10s,在query中含nocache&request含fresh(缓存是否新鲜),走304

context配置

中间件:据说是用来追踪请求用的。。。。

MySQL、Redis就不解释了,很简单

api配置

首先还是merge用户和默认配置,这里得说明下,之前bucky是没有暴露config.api配置给用户的,三部曲中的只有action有用户配置,而且一般也不会改动,除非你的action里有共有的部分
然后拿着merge后的config创建API,参数中可设置的有queryEncode、contentType、userAgent、requestHandler、responseHandler、cache、host、proxy等,这些参数也可以在某类API中设置。

这里首先会创建一个API类,该类为全局API对象,然后获取当前设置api根目录,其实也就是apis/,然后遍历apis/文件夹,拿到文件路径,然后解析(requireAPI)该api文件配置,里面包含解析文件的配置,配置包含默认环境配置、特殊环境配置,每一个环境中配置都是以函数形式,配置中export类型有三个环境,创建配置中会生成该API的configMap,存放apiName与apiconfig映射关系,然后执行该API文件中的注册API,然后生成API配置,然后深度merge两个环境的各自的基础配置和configMap(APIName与APIConfig映射关系),最后会merge每个API的config到baseConfig。然后会遍历这个总的config,里面包含这个API的所有配置,以及每个api的配置,生成API.APIName.apiName,这种格式,如API.Test.test,然后拿着这个和文件路径生成API(createAPI)
createAPI里:首先解析每个api类的配置,这里的配置是某一个api配置和API基础配置的深度merge,最终返回的是一个api函数,也就是我们通常会调用的API.Test.test(data, options)这种形式,函数里面干了什么事情呢,首先从options中解析四个参数,query/headers/uriReplacer/ctx,拿着前面解析的parameters做接口数据类型检查,然后创建api请求数据对象,包含uri method timeout proxy displayname encoding headers,headers中会使用每个api中的contenttype、userAgent、host然后merge传入的headers,所以header可以在api中设置。

根据方法名,合并参数,包含GET、HEAD、OPTIONS三个,剩下处理body数据,multipart/form-data文件类型的表单提交,然后将formData写到上述创建的api请求数据对象中,formData中包含data里面的数据,如果是流stream类型,则设置filename和contentType;

application/json类型则直接设置body为stringify(data)后的数据;

application/x-www-form-urlencoded类型则设置form为data,其余类型则直接data挂到body。然后拼装uri,通过base、uriReplacer规则,然后将query对象转为search类型,此时通过queryEncode判断是否转码。
调用requestHandler,参数有创建的请求数据对象、ctx,这个时候需要注意,如果你的requestHandler是自定义的,那么需要注意,函数中需要返回你处理后的请求数据对象,然后bucky拿着处理后的请求数据对象,然后处理cache缓存,这里默认使用的是Redis,如果使用缓存且缓存有效则使用缓存的结果作为返回,反之发起请求doRequest

doRequest中有递增请求sequence的操作,标识请求的唯一和连续,记录请求时间,请求使用request库,自己封装为promise方式,如果出错则抛出错误,并记录到log.api日志中,正常则调用responseHandler方法,同样报错则抛出错误,如果有cache设置则写到缓存,同时会直接返回这个responseHandler的处理结果
上述创建完成后,会将api和apis分别挂载到apis上和API 上,这里会重写get方法,避免被篡改

特别注意:上述说到的requestHandler、responseHandler都提供有默认的,defaultRequestHandler简单的返回请求数据对象,defaultResponseHandler则会校验statusCode,解析codeKey、dataKey、messageKey、successCode,这里可以通过刚刚说到的config设置,然后会从返回的body中解析对应的字段,然后检查successCode,如果你的body中的codeKey对应的值和successCode不对应,则会抛出错误

model配置

model配置也是老套路,先解析挂载在app.config上的model配置,获取model的路径,然后加载自己的model,HdicAuth、Utils、S3、WebShot,然后挂载业务model

service配置

service是用于拦截请求用的,粗糙点说就是在action/model/api之前会执行这个,这里可以拦截并处理这个请求,service采用中间件,首先加载内部的三个service,appwebview、logviewer、login,然后获取业务自定义的service,这里注意到有service各自的依赖service

登录service配置

目前service中包含三个service,login、appwebview、logviewer,三个模块中需要重点说明的是login模块,目前login模块中包含三种方式,分别为cas(uc最新提供的登录方式)、general(原UC登录方式)、hdic(楼盘字典登录),目前使用率最高的为general,部分用cas,hdic就一个,下面简单说明集成方式。

首先login/index.js中export一个函数,该函数管理(校验)三种登录方式。

接着看general方式,首先有部分基础配置appWebViewLoginScript、URL_BASE

appwebview配置

logviewer配置

未完待续

avatar

简单描述下:

首先bucky作为中间层,处于业务server和UI client之间,同时作为应用服务器,里面模块主要包含上面罗列的模块

我们称client为 UI client,也就是通常说的传统意义的前端客户端,然后到bucky,首先到redirect,判断是否服务重定向规则,符合则重写浏览器url并发起请求,接着判断是否符合rewrite重写规则,重定向和重写区别,简单讲,比如你想去商店买个面包,但这个商店没面包,但老板说我帮去另外商店找一个给你,这叫重写;如果老师直接让你去哪个商店买面包,这叫重定向。好了,说回来,如果符合rewrite规则,这个时候,bucky会帮你重写浏览器uri,但不会重新发起请求,然后到services层,如果你没有自定义的service,那就是走默认的三个service,如果有自定义的service,则会在action之前执行,所以我们某些时候会用service来做请求的拦截或单独处理不走action及后续逻辑,然后action中可以调用model、api、redis、mysql这些,然后我们在action(action中可能有很复杂的业务或数据处理)中返回这个请求,也就是到了上面的response模块,最后到UI client。上面还有一个模块websocket,websocket是很灵活的,可以和client双向或单向通信,比如我们的logviewer其实就是一个websocket,当然websocket里也照样可以调用api model 等模块。

rewrite与redirect区别

Posted on 2018-07-27

谈及这两个概念,通常会想到这应该是后端考虑的事情,但现在前端coder也需要关注这个,因为现在有个大前端概念,node中有这个概念,需要我们关注。
我们搞node中间层,会涉及到重写url和重定向,所有也就有了这两个。

rewrite和redirect的区别

redirect,即重定向。

rewrite,即重写,不仅仅可以实现redirect在url规则上的重定向,还可以直接重写请求到实际的文件以及更多附加功能。

重定向和URL重写的具体区别:

(1)关于重定向

通过重定向,浏览器知道页面位置发生变化,从而改变地址栏显示的地址。

通过重定向,搜索引擎意识到页面被移动了,从而更新搜索引擎索引,将原来失效的链接从搜索结果中移除。

临时重定向(R=302)和永久重定向(R=301)都是亲搜索引擎的,是SEO的重要技术。

redirect是浏览器和服务器发生两次请求,也就是服务器命令客户端“去访问某个页面”;

redirect的URL需要传送到客户端。

redirect是从一个地址跳转到另一个地址。

(2)关于重写

rewrite的URL只是在服务器端

rewrite则是服务器内部的一个接管,在服务器内部告诉“某个页面请帮我处理这个用户的请求”,浏览器和服务器只发生一次交互,浏览器不知道是该页面做的响应,浏览器只是向服务器发出一个请求。

URL重写用于将页面映射到本站另一页面,若重写到另一网络主机(域名),则按重定向处理。

rewrite是把一个地址重写成另一个地址。地址栏不跳转。相当于给另一个地址加了一个别名一样。

上述的例子就像用户去买手机,缺货时的两种处理:让用户自己去其他地方买(redirect);公司从其他的地方调货(rewrite)。

webpack构建工具-hpack

Posted on 2018-07-01

webpack工具集成-babel polyfill runtime

这里简单说下 转义BABEL的POLYFILL和RUNTIME的区别

babel-polyfill 使用场景

Babel 默认只转换新的 JavaScript 语法,而不转换新的 API。例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转译。如果想使用这些新的对象和方法,必须使用 babel-polyfill,为当前环境提供一个垫片。

babel-runtime 使用场景

Babel 转译后的代码要实现源代码同样的功能需要借助一些帮助函数,例如,{ [name]: 'JavaScript' } 转译后的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var obj = _defineProperty({}, 'name', 'JavaScript');

类似上面的帮助函数 _defineProperty 可能会重复出现在一些模块里,导致编译后的代码体积变大。Babel 为了解决这个问题,提供了单独的包 babel-runtime 供编译模块复用工具函数。

启用插件 babel-plugin-transform-runtime 后,Babel 就会使用 babel-runtime 下的工具函数,转译代码如下:

1
2
3
4
5
6
'use strict';
// 之前的 `_defineProperty` 函数已经作为公共模块 `babel-runtime/helpers/defineProperty` 使用
var _defineProperty2 = require('babel-runtime/helpers/defineProperty');
var _defineProperty3 = _interopRequireDefault(_defineProperty2);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var obj = (0, _defineProperty3.default)({}, 'name', 'JavaScript');

除此之外,babel 还为源代码的非实例方法(Object.assign,实例方法是类似这样的 "foobar".includes("foo"))和 babel-runtime/helps 下的工具函数自动引用了 polyfill。这样可以避免污染全局命名空间,非常适合于 JavaScript 库和工具包的实现。例如 const obj = {}, Object.assign(obj, { age: 30 }); 转译后的代码如下所示:

1
2
3
4
5
6
7
8
9
'use strict';
// 使用了 core-js 提供的 assign
var _assign = require('babel-runtime/core-js/object/assign');
var _assign2 = _interopRequireDefault(_assign);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var obj = {};
(0, _assign2.default)(obj, {
age: 30
});

思考:babel-runtime 为什么适合 JavaScript 库和工具包的实现?

避免 babel 编译的工具函数在每个模块里重复出现,减小库和工具包的体积;

在没有使用 babel-runtime 之前,库和工具包一般不会直接引入 polyfill。否则像 Promise 这样的全局对象会污染全局命名空间,这就要求库的使用者自己提供 polyfill。这些 polyfill 一般在库和工具的使用说明中会提到,比如很多库都会有要求提供 es5 的 polyfill。在使用 babel-runtime 后,库和工具只要在 package.json 中增加依赖 babel-runtime,交给 babel-runtime 去引入 polyfill 就行了;

总结:

具体项目还是需要使用 babel-polyfill,只使用babel的话,实例方法不能正常工作(例如 “foobar”.includes(“foo”));

JavaScript 库和工具可以使用 babel-runtime,在实际项目中使用这些库和工具,需要该项目本身提供 polyfill

函数消抖与节流

Posted on 2018-06-24

函数防抖与节流是很相似的概念,但它们的应用场景不太一样。

我们先从概念上深刻理解它们。

消抖

先说函数防抖-debounce。其概念其实是从机械开关和继电器的“去弹跳”(debounce)衍生 出来的,基本思路就是把多个信号合并为一个信号。

单反也有相似的概念,在拍照的时候手如果拿不稳晃的时候拍照一般手机是拍不出好照片的,因此智能手机是在你按一下时连续拍许多张,通过合成手段,生成一张。翻译成JS就是,事件内的N个动作会被忽略,只有事件后由程序触发的动作有效。

实现思路如下,将目标方法(动作)包装在setTimeout里面,然后这个方法是一个事件的回调函数,如果这个回调一直执行,那么这些动作就一直不执行。为什么不执行呢,我们搞了一个clearTimeout,这样setTimeout里的方法就不会执行! 为什么要clearTimeout呢,我们就需要将事件内的连续动作删掉嘛!待到用户不触发这事件了。那么setTimeout就自然会执行这个方法。

那么这个方法用在什么地方呢,就是用于input输入框架的格式验证,假如只是验证都是字母也罢了,太简单了,不怎么耗性能,如果是验证是否身份证,这性能消耗大,你可以隔170ms才验证一次。这时就需要这个东西。或者你这个是自动完全,需要将已有的输入数据往后端拉一个列表,频繁的交互,后端肯定耗不起,这时也需要这个,如隔350ms。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function debounce(func, delay) {
var timeout;
return function(e) {
console.log("清除",timeout,e.target.value)
clearTimeout(timeout);
var context = this, args = arguments
console.log("新的",timeout, e.target.value)
timeout = setTimeout(function(){
console.log("----")
func.apply(context, args);
},delay)
};
};

var validate = debounce(function(e) {
console.log("change", e.target.value, new Date-0)
}, 380);
1
2
// 绑定监听
document.querySelector("input").addEventListener('input', validate);

这个保证了正常的用户每输入1,2个字符就能触发一次。如果用户是输入法狂魔,也可以狠制他每输入3~6个字符触发一次。

这个方法的重点是,它在用户不触发事件的时,才触发动作,并且抑制了本来在事件中要执行的动作。

其他应用场合:提交按钮的点击事件。

节流

再看节流,throttle。节流的概念可以想象一下水坝,你建了水坝在河道中,不能让水流动不了,你只能让水流慢些。换言之,你不能让用户的方法都不执行。如果这样干,就是debounce了。为了让用户的方法在某个时间段内只执行一次,我们需要保存上次执行的时间点与定时器。

1
<div id='panel' style="background:red;width:200px;height:200px"></div>
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
function throttle(fn, threshhold) {

var timeout
var start = new Date;
var threshhold = threshhold || 160

return function () {
var context = this, args = arguments, curr = new Date() - 0
clearTimeout(timeout)//总是干掉事件回调
if (curr - start >= threshhold) {
console.log("now", curr, curr - start)//注意这里相减的结果,都差不多是160左右
fn.apply(context, args) //只执行一部分方法,这些方法是在某个时间段内执行一次
start = curr
} else {
//让方法在脱离事件后也能执行一次
timeout = setTimeout(function(){
fn.apply(context, args)
}, threshhold);
}
}
}

var mousemove = throttle(function(e) {
console.log(e.pageX, e.pageY)
});

// 绑定监听
document.querySelector("#panel").addEventListener('mousemove', mousemove);

函数节流会用在比input, keyup更频繁触发的事件中,如resize, touchmove, mousemove, scroll。throttle 会强制函数以固定的速率执行。因此这个方法比较适合应用于动画相关的场景。

如果还是不能完全体会 debounce 和 throttle 的差异,可以到 这个页面 看一下两者可视化的比较。

koa2中间件原理

Posted on 2018-05-04

今天说说koa2中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

const app = {
middleware: [],
callback(ctx) { //最终请求结束调用
console.log(ctx)
},
use(fn) {
// 逻辑
this.middleware.push(fn)
},
start(ctx) {
// 逻辑
const reducer = (next, fn , i) => () => {
console.log(fn, next)
fn(ctx, next)
}
// 从右侧遍历,最后拿到最栈顶的next,并从栈顶到栈尾形成next链
const q = this.middleware.reduceRight(reducer, this.callback.bind(this, ctx)) //没处理异步
console.log(q)
// 开始执行栈顶
q()
}
}
app.use((ctx, next) => {
ctx.name = 'huangwei'
console.log('>> use name')
next()
console.log('<< use name')
})
app.use((ctx, next) => {
ctx.age = 20
console.log('>> use age')
next()
console.log('<< use age')
})

app.use((ctx, next) => {
console.log(`>> ${ctx.name} is ${ctx.age} years old`)
next()
console.log(`<< ${ctx.name} is ${ctx.age} years old`)
})

app.start({})

输出结果:

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
// q
() => {
console.log(fn, next)
fn(ctx, next)
}
(ctx, next) => {
ctx.name = 'huangwei'
console.log('>> use name')
next()
console.log('<< use name')
}
() => {
console.log(fn, next)
fn(ctx, next)
}
>> use name

(ctx, next) => {
ctx.age = 20
console.log('>> use age')
next()
console.log('<< use age')
}
() => {
console.log(fn, next)
fn(ctx, next)
}
>> use age

(ctx, next) => {
console.log(`>> ${ctx.name} is ${ctx.age} years old`)
next()
console.log(`<< ${ctx.name} is ${ctx.age} years old`)
}
ƒ callback(ctx) { //最终请求结束调用
console.log(ctx)
}
>> huangwei is 20 years old

{name: "huangwei", age: 20}

<< huangwei is 20 years old
<< use age
<< use name

分析栗子🌰

先看一张图

中间件是怎么串起来的

首先我们看到上面有黄色的模块,这个我们定义为我们刚刚使用的中间件默认执行函数callback(D),三个中间件一次为A、B、C,那么我们下面的两个条状柱体是reduceRight中reducer回调函数的前两个参数
那我们看到的应该是这样的。

第一次回调分别是D、C,因为我们后面传入了默认值D,第二次是返回第一次reducer返回的函数,第三次是返回第二次reducer返回的函数,以此类推,就生成了这个图。然后就形成了联调,那么最后拿到的q就是最后的A->B的函数,这样的话就形成链条(链表),然后我们顺着缕缕接下来的过程。A执行的时候,A里面包含next,而这个next正好是B,B执行的时候,B里面next正好是C,C执行的时候,C里面的next正好是D,好,这个时候D执行完,往上回溯,执行C的next后面的逻辑,这个时候C执行结束,到B的next后面逻辑,最后到A的next后面逻辑,最后就形成了这个神奇的next串

注:这里是没有考虑异步

直接koa2

上面是模拟koa2,下面直接使用koa2

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
const Koa = require('koa');
const app = new Koa();

const one = async (ctx, next) => {
console.log('>> one');
// return next();
await next();
console.log('<< one');
}

const two = async (ctx, next) => {
console.log('>> two');
// return next();
await next();
console.log('<< two');
}

const three = async (ctx, next) => {
console.log('>> three');
// return next();
await next();
console.log('<< three');
}

app.use(one);
app.use(two);
app.use(three);

app.listen(3000);

输出结果:

1
2
3
4
5
6
>> one
>> two
>> three
<< three
<< two
<< one

当然这和我们的next放的位置有关系 放最后就不会出现回溯,最前面的话,表面看就只有回溯

如果打开上面return
则输出结果为:

1
2
3
>> one
>> two
>> three

这样的话可以阻止会面的回溯,通常也会这样写,当然也有特殊case中需要用到这个特性,比如
记录请求的总耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.use( (ctx, next) => {

const startTime = Date.now()
const { request, response } = ctx

ctx.request.uuid = ctx.headers.UNIQID || uuid()
ctx.request.sequence = 0

return next().then(() => {
const endTime = Date.now()
process.emit('log.access', {
request, response, startTime, endTime, ctx,
uuid: ctx.request.uuid,
sequence: ctx.request.sequence,
timeCost: endTime - startTime
})
})
})

关于上面的运行机制及原理,我们分析一波
我们关注下application.js
application传送门

1
2
3
4
5
6
7
8
9
10
11
12
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

重点关注this.middleware.push(fn),向我们的middleware栈push中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
callback() {
const fn = compose(this.middleware);

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

重点关注
const fn = compose(this.middleware)
和handleRequest中的
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
那重点就在compose,compose函数返回的是一个promise

compose传送门

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
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}

我们重点关注这个dispatch,这样结合上面的看会发现形成了一个递归链,这样也就解释了为什么会出现回溯

当next为空,直接返回,所以也就解释了为什么没有next不会向下走。
假设我们执行到第一个fn了,这个时候fn是

1
2
3
4
5
6
fn = async (ctx, next) => {
console.log('>> one');
// return next();
await next();
console.log('<< one');
}

next对应 function next(){return dispatch(i+1)},所以就进入到dispatch(i+1)下一个中间件了,以此类推。
核心就是dispatch,最后结束就是next不存在,然后开始依次回溯到里层,所以执行顺序是越先注册越后执行。

macrotasks_microtasks

Posted on 2018-04-23
  • 放个面试题,抛个砖:
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
console.log('start')

const interval = setInterval(() => {
console.log('setInterval')
}, 0)

setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve()
.then(() => {
console.log('promise 3')
})
.then(() => {
console.log('promise 4')
})
.then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve()
.then(() => {
console.log('promise 5')
})
.then(() => {
console.log('promise 6')
})
.then(() => {
clearInterval(interval)
})
}, 0)
})
}, 0)

Promise.resolve()
.then(() => {
console.log('promise 1')
})
.then(() => {
console.log('promise 2')
})

不着急揭晓答案,先分析

首先知晓:

js是单线程语言

也就是说一次就只能做一件事情。多数的网站不需要大量计算,程序花费的时间主要集中在磁盘 I/O 和网络 I/O 上面,虽然SSD读取很快,但和CPU处理指令的速度比起来也不在一个数量级上,而且网络上一个数据包来回的时间更慢(注意过游戏的延迟吗)

so: 一些cpu直接执行的任务就成了优先执行主线任务,然后需要io返回数据的任务就成了等待被执行的任务,所以才会有同步任务(synchronous)和异步任务(asynchronous)之分

同步任务:

在主线程上排队执行的任务,前一个任务执行完毕,才能执行后一个任务;

异步任务:

不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

总之:

只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制

Microtasks Macrotasks
任务队列不止一个,还有 microtasks 和 macrotasks
  • microtasks:
1
2
3
4
process.nextTick
promise
Object.observe
MutationObserver
  • macrotasks:
1
2
3
4
5
setTimeout
setInterval
setImmediate
I/O
UI渲染
  • whatwg规范:https://html.spec.whatwg.org/multipage/webappapis.html#task-queue

    一个事件循环(event loop)会有一个或多个任务队列(task queue)
    task queue 就是 macrotask queue
    每一个 event loop 都有一个 microtask queue
    task queue == macrotask queue != microtask queue
    一个任务 task 可以放入 macrotask queue 也可以放入 microtask queue 中
    理解了这些定义之后,再看执行原理:

    事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。

    还要注意一点:

    包裹在一个 script 标签中的js代码也是一个 task 确切说是 macrotask。

    所以文首面试题的答案为:

1
2
3
4
5
6
7
8
9
10
11
start 
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6

简单来讲,整体的js代码这个macrotask先执行,同步代码执行完后有microtask执行microtask,没有microtask执行下一个macrotask,如此往复循环至结束

12

schacker

17 posts
2 tags
© 2019 schacker
Powered by Hexo
|
Theme — NexT.Muse v5.1.4