[TOC]
# 什么是事件循环(Event loop)
Event loop是什么?
<br>
WIKI定义:
> In computer science, the**event loop, message dispatcher, message loop, message pump, or run loop**is a programming construct that waits for and dispatches events or messages in a program.
Event loop是一种程序结构,是实现异步的一种机制。Event loop可以简单理解为:
1. 所有任务都在主线程上执行,形成一个执行栈(execution context stack)。
2. 主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后主线程继续执行后续的任务。
3. 一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。
4. 主线程不断重复上面的第三步。
对JavaScript而言,Javascript引擎/虚拟机(如V8)之外,JavaScript的运行环境(runtime,如浏览器,node)维护了任务队列,每当JS执行异步操作时,运行环境把异步任务放入任务队列。当执行引擎的线程执行完毕(空闲)时,运行环境就会把任务队列里的(执行完的)任务(的数据和回调函数)交给引擎继续执行,这个过程是一个**不断循环**的过程,称为**事件循环**。
<br>
**注意:JavaScript(引擎)是单线程的,Event loop并不属于JavaScript本身,但JavaScript的运行环境是多线程/多进程的,运行环境实现了Event loop。**
<br>
<br>
## Node.js的Event loop
在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。
Node.js的运行机制如下:
* V8引擎解析JavaScript脚本。
* 解析后的代码,调用Node API。
* libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
* V8引擎再将结果返回给用户。
![](https://box.kancloud.cn/b70bb4a1a0746b75115fb35b7e130d37_543x223.png)
<br>
下面是node启动的部分相关代码:
~~~c
// node.cc
{
SealHandleScope seal(isolate);
bool more;
do {
v8_platform.PumpMessageLoop(isolate);
more = uv_run(env.event_loop(), UV_RUN_ONCE);
if (more == false) {
v8_platform.PumpMessageLoop(isolate);
EmitBeforeExit(&env);
// Emit `beforeExit` if the loop became alive either after emitting
// event, or after running some callbacks.
more = uv_loop_alive(env.event_loop());
if (uv_run(env.event_loop(), UV_RUN_NOWAIT) != 0)
more = true;
}
} while (more == true);
}
~~~
<br>
<br>
# libuv
Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现。
[libuv/src/win/core.c](https://github.com/libuv/libuv/blob/v1.x/src/win/core.c)
<br>
<br>
# 事件循环模型
~~~
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
~~~
注:模型中的每一个方块代表事件循环的一个阶段
<br />
每个阶段都有一个**FIFO**的回调队列(queue)要执行。而每个阶段有自己的特殊之处,简单说,就是当event loop进入某个阶段后,会执行该阶段特定的(任意)操作,然后才会执行这个阶段的队列里的回调。当队列被执行完,或者执行的回调数量达到上限后,event loop会进入下个阶段。
<br />
## 事件循环各阶段
只要任意两个阶段中有`process.nextTick`及`Promise`,就会优先执行它的回调
* timers:定时器阶段。setTimeout、setInterval的回调函数被执行。对应源码的`uv__run_timers`
* I/O callbacks:执行被推迟到下一个iteration的 I/O 回调。对应源码的`uv__run_pending`
* idle, prepare: 仅内部使用。
* poll:获取新的I/O事件;node会在适当条件下阻塞在这里。进入poll阶段,如果poll queue是空的,并且没有`setImmediate`添加的回调,event loop会在这里等待I/O callbacks被添加到poll queue,并立即执行。
* check:执行`setImmediate`回调
* close callbacks:执行比如`socket.on('close', ...)`的回调。
**大部分的I/O回调会在poll阶段被执行,但某些系统操作(比如TCP类型错误)执行回调会安排在pending callbacks阶段。**
## 阶段详情
### timers
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
>
> 注意:技术上来说,**poll**阶段控制 timers 什么时候执行。
>
> <br />
>
> 注意:这个下限时间有个范围:`[1, 2147483647]`,如果设定的时间不在这个范围,将被设置为1。
以下是官网文档解释的例子:
~~~js
var fs = require('fs');
function someAsyncOperation (callback) {
// 假设这个任务要消耗 95ms
fs.readFile('/path/to/file', callback);
}
var timeoutScheduled = Date.now();
setTimeout(function () {
var delay = Date.now() - timeoutScheduled;
console.log(delay + "ms have passed since I was scheduled");
}, 100);
// someAsyncOperation要消耗 95 ms 才能完成
someAsyncOperation(function () {
var startCallback = Date.now();
// 消耗 10ms...
while (Date.now() - startCallback < 10) {
; // do nothing
}
});
~~~
当event loop进入**poll**阶段,它有个空队列(`fs.readFile()`尚未结束)。所以它会等待剩下的毫秒, 直到最近的timer的下限时间到了。当它等了95ms,`fs.readFile()`首先结束了,然后它的回调被加到**poll** 的队列并执行——这个回调耗时10ms。之后由于没有其它回调在队列里,所以event loop会查看最近达到的timer的 下限时间,然后回到**timers**阶段,执行timer的回调。
<br />
所以在示例里,回调被设定 和 回调执行间的间隔是105ms。
<br />
### I/O callbacks
这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到`ECONNREFUSED`,
类unix系统会等待以报告错误,这就会放到**I/O callbacks**阶段的队列执行。
<br />
### poll
**poll**阶段有两个主要功能:
1. 执行下限时间已经达到的timers的回调,然后
2. 处理**poll**队列里的事件。
* 当event loop进入**poll**阶段,并且**没有设定的timers(there are no timers scheduled)**,会发生下面两件事之一:
* 如果**poll**队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;
* 如果**poll**队列为空,则发生以下两件事之一:
* 如果代码已经被`setImmediate()`设定了回调, event loop将结束**poll**阶段进入**check**阶段来执行**check**队列(里的回调)。
* 如果代码没有被`setImmediate()`设定回调,event loop将阻塞在该阶段等待回调被加入**poll**队列,并立即执行。
* 当event loop进入**poll**阶段,并且**有设定的timers**,一旦**poll**队列为空(**poll**阶段空闲状态):
* event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 **timers** 阶段,并执行 **timer** 队列。
<br />
### check
这个阶段允许在**poll**阶段结束后立即执行回调。如果**poll**阶段空闲,并且有被`setImmediate()`设定的回调,event loop会转到**check**阶段而不是继续等待。
<br />
`setImmediate()`实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用`libuv`的API 来设定在**poll**阶段结束后立即执行回调。
<br />
通常上来讲,随着代码执行,event loop终将进入**poll**阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被`setImmediate()`设定了回调,一旦**poll**阶段空闲,那么程序将结束**poll**阶段并进入**check**阶段,而不是继续等待**poll**事件们 (**poll**events)。
<br />
### close callbacks
如果一个 socket 或 handle 被突然关掉(比如`socket.destroy()`),close事件将在这个阶段被触发,否则将通过`process.nextTick()`触发。
<br />
<br />
# `setImmediate()`vs`setTimeout()`
`setImmediate()`和`setTimeout()`是相似的,区别在于什么时候执行回调:
1. `setImmediate()`被设计在**poll**阶段结束后立即执行回调;
2. `setTimeout()`被设计在指定下限时间到达后执行回调。
## 外部执行setTimeout和setimmediate
下面看一个例子:
~~~js
// timeout_vs_immediate.js
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
~~~
代码的输出结果是:
~~~shell
timeout
immediate
// 或
immediate
timeout
~~~
是的,你没有看错,输出结果是**不确定**的!
从直觉上来说,`setImmediate()`的回调应该先执行,但为什么结果随机呢?
<br />
## readFile中执行setTimeout和setImmediate
~~~js
// timeout_vs_immediate.js
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
~~~
结果是:
~~~shell
immediate
timeout
~~~
很好,`setImmediate`在这里永远先执行!
<br>
## 延时执行setTimeout和setImmediate
~~~
const now = Date.now();
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
// 延迟1秒
while (Date.now() - now < 1000) {
}
// 改为添加一个nextTick函数也可以让setTimeout先执行
// process.nextTick(function(){
// console.log('nextTick');
// });
~~~
~~~
setTimeout
setImmediate
~~~
因为延迟了1秒,进入timer阶段时,已过去1秒,setTimeout会先执行
<br>
## setImmediate中执行setTimeout和setImmediate
~~~
setImmediate(() => {
console.log('setImmediate')
setTimeout(() => {
console.log('setImmediate 里面的 setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate 里面的 setImmediate')
})
});
// setImmediate
// setImmediate 里面的 setTimeout
// setImmediate 里面的 setImmediate
~~~
## setTimeout中执行setTimeout和setImmediate
~~~
setTimeout(() => {
console.log('setTimeout')
setTimeout(() => {
console.log('setTimeout 里面的 setTimeout')
}, 0)
setImmediate(() => {
console.log('setTimeout 里面的 setImmediate')
})
}, 0);
// setTimeout
// setTimeout 里面的 setImmediate
// setTimeout 里面的 setTimeout
~~~
<br>
所以,结论是:
1. 如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,即随机。
2. 如果两者都不在主模块调用(即在一个 IO circle 中调用),那么`setImmediate`的回调永远先执行。
<br>
看`int uv_run(uv_loop_t* loop, uv_run_mode mode)`源码(deps/uv/src/unix/core.c#332):
~~~
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
//// 1. timer 阶段
uv__run_timers(loop);
//// 2. I/O callbacks 阶段
ran_pending = uv__run_pending(loop);
//// 3. idle/prepare 阶段
uv__run_idle(loop);
uv__run_prepare(loop);
// 重新更新timeout,使得 uv__io_poll 有机会跳出
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
//// 4. poll 阶段
uv__io_poll(loop, timeout);
//// 5. check 阶段
uv__run_check(loop);
//// 6. close 阶段
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
uv__update_time(loop);
// 7. UV_RUN_ONCE 模式下会再次检查timer
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
~~~
<br>
第一个参数为指向 `uv_loop_t` 的指针,是事件循环的结构。每次执行uv_run就是进行事件循环的迭代。
1. 首先进入timer阶段,如果我们的机器性能一般,那么进入timer阶段时,1毫秒可能已经过去了(`setTimeout(fn, 0)`等价于`setTimeout(fn, 1)`),那么`setTimeout`的回调会首先执行。
2. 如果没到一毫秒,那么我们可以知道,在check阶段,`setImmediate`的回调会先执行。
3. 为什么`fs.readFile`回调里设置的,`setImmediate`始终先执行?因为`fs.readFile`的回调执行是在**poll**阶段,所以,接下来的**check**阶段会先执行`setImmediate`的回调。
4. 我们可以注意到,`UV_RUN_ONCE`模式下,event loop会在开始和结束都去执行timer。
<br>
<br>
# 理解`process.nextTick()`
直到现在,我们才开始解释`process.nextTick()`。因为从技术上来说,它并不是event loop的一部分。相反的,`process.nextTick()`会把回调塞入`nextTickQueue`,`nextTickQueue`将在当前操作完成后处理,不管目前处于event loop的哪个阶段。
看看我们最初给的示意图,`process.nextTick()`不管在任何时候调用,都会在所处的这个阶段最后,在event loop进入下个阶段前,处理完所有`nextTickQueue`里的回调。
## `process.nextTick()`vs`setImmediate()`
两者看起来也类似,区别如下:
1. `process.nextTick()`立即在本阶段执行回调;
2. `setImmediate()`只能在**check**阶段执行回调。
## 为什么它会被允许?
为什么node中会有这种东西呢?其中的一部分是设计理念,即 API应该始终是异步操作,即使没必要是。下面是个例子:
~~~js
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(callback,
new TypeError('argument should be string'));
}
~~~
这个小片段做了一个参数校验,如果不正确就会将错误对象传给回调,API最近才更新的允许传参数给process.nextTick()使他能够接收任何在回调函数扩散之后作为回调函数的参数传过来的参数,所以你就用不着进行函数嵌套了。
<br>
我们正在做的就是在允许用户的剩下的代码能继续执行的条件下,传递错误对象回去给用户。通过使用process.nextTick()我们保证那个`APICall()`会一直在用户剩下的代码之后和事件循环被允许之前执行它的回调。为了实现这个目的,JS调用栈被允许来解开,然后立即执行被提供的回调,这允许用户做出递归调用来使用process.nextTick(),而不会报`RangeError: Maximum call stack size exceeded from v8`的错。
<br>
这个理念会导致可能的有问题的情况,就拿下面这段代码来说:
~~~
let bar;
// 异步特征的却被同步调用
function someAsyncApiCall(callback) { callback(); }
// 在someAsyncApiCall完成之前回调
someAsyncApiCall(() => {
// someAsyncApiCall 完成调用,bar 还没赋上值
console.log('bar', bar); // undefined
});
bar = 1;
~~~
用户定了someAsyncApiCall()来获取异步签名,但是实际上却是同步操作的,当被调用时,提供给函数的回调,在同一个事件循环的同一阶段被调用,因为函数没有做任何异步操作。结果,回调试图引用 bar这个变量,在作用域中甚至还没有那个变量,因为脚本还没运行完。
<br>
通过把回调函数放在一个`process.nextTick()`中,代码还有能力继续运行完,允许所有变量,函数,等等在函数回调调用之前进行初始化。它还具有不允许事件循环继续的优点。可以在让用户在事件循环被允许继续之前报出错误帮上忙。
<br>
这里先前的例子使用 process.nextTick:
~~~
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
~~~
这里是现实中另一个例子:
~~~
const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});
~~~
当仅传一个端口时,端口立即被绑定。所以,“listening”回调将会被立即调用。问题就是.on('listening')回调那会儿还没设置。
<br>
为了克服这个问题,listening事件被在`nextTick()`中插入队列,这样就允许脚本能运行完。这允许用户设置任何他们想设置的方法。
<br>
## 为什么使用process.nextTick()?
有两个主要原因:
* 允许用户解决报错,清理任何将来不需要的资源(垃圾清理)或者是在事件循环继续之前再次发送请求。
* 有时,允许回调函数在调用栈解开之后但是在事件循环继续之前运行时必要的。
<br>
一个满足用户期待的简单例子:
~~~
const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });
~~~
listen() 运行在生命周期的开始,但是监听回调被放在一个setImmediate()立即执行函数里,除非端口名被传进去,否则会立即绑定到端口。为了事件循环执行,他必须进入轮询阶段,也就是意味着有非零的几率,可能会接收到一个在监听事件开始之前就触发的连接事件
另一个例子就是运行一个函数的构造器,也就是说继承一个EventEmitter并且想在构造器里调用事件
~~~
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
~~~
你不能在构造器里立即释放一个事件,以为你脚本还没运行到用户为事件定义回调函数的地方。所以在构造函数内部,你可以使用process.nextTick()来在构造器完成之后设置一个回调来释放这个事件,也就是下面这个例子所期望的结果:
~~~
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
~~~
<br>
<br>
<br>
<br>
# readFile执行顺序
~~~
const fs = require('fs')
const now = Date.now();
setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {}
~~~
在同一上下文,readFile执行顺序在setImmediate、setTimeout后
~~~
// 结果
timer
immediate
readfile
~~~
分析见
https://github.com/creeperyang/blog/issues/26#issuecomment-370144475
<br>
<br>
# Node11 改变了宏任务的执行
## setTimeout
~~~
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(function() {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(function() {
console.log('promise2');
});
}, 0);
~~~
Node 10以下版本的结果:
~~~
timer1
timer2
promise1
promise2
~~~
Node 11的结果
~~~
timer1
promise1
timer2
promise2
~~~
## setImmediate
1. 多次调用`setImmediate()`则把回调都放入队列,在 check 阶段都会执行;
2. 但`setImmediate()`回调里调用`setImmediate()`,则放到下次 event loop。
~~~
setImmediate(function(){
console.log("setImmediate");
setImmediate(function(){
console.log("嵌套setImmediate");
});
process.nextTick(function(){
console.log("nextTick")
})
})
setImmediate(function(){
console.log("setImmediate2");
});
// Node 10以下
// setImmediate
// setImmediate2(Node 10以下会输出到这里)
// nextTick
// 嵌套setImmediate
// Node 11
// setImmediate
// nextTick
// setImmediate2(Node 11会输出到这里)
// 嵌套setImmediate
~~~
<br>
在node 11.0 的修改日志里面发现了这个:
* Timers
* Interval timers will be rescheduled even if previous interval threw an error. #20002
* nextTick queue will be run after each immediate and timer. #22842
<br>
然后分别看了20002和22842的PR,发现在[ #22842](https://github.com/nodejs/node/pull/22842) 在lib/timers.js里面有以下增加:
![](https://box.kancloud.cn/7165ee58c3aa9ce4b5fdee1bdb66b1eb_566x289.png)
![](https://box.kancloud.cn/3f450d6877905fa734ed165f0e979607_504x172.png)
<br>
runNextTicks()就是process.\_tickCallback()。用过的可能知道这个就是除了处理一些异步钩子,然后就是执行微任务队列的。于是我增加了两行process.\_tickCallback()在setTimeout方法尾部,再使用node10运行,效果果然和node11一致,代码如下:
~~~
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(function() {
console.log('promise1');
});
process._tickCallback(); // 这行是增加的!
}, 0);
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(function() {
console.log('promise2');
});
process._tickCallback(); // 这行是增加的!
}, 0);
~~~
## 那么为什么要这么做呢?
当然是为了和浏览器更加趋同。
<br>
了解浏览器的eventloop可能就知道,浏览器的宏任务队列执行了一个,就会执行微任务。
<br>
简单的说,可以把浏览器的宏任务和node10的timers比较,就是node10只有全部执行了timers阶段队列的全部任务才执行微任务队列,而浏览器只要执行了一个宏任务就会执行微任务队列。
<br>
现在node11在timer阶段的setTimeout,setInterval...和在check阶段的immediate都在node11里面都修改为一旦执行一个阶段里的一个任务就立刻执行微任务队列。
# 参考资料
* [Node.js的event loop及timer/setImmediate/nextTick](https://github.com/creeperyang/blog/issues/26))
* [又被node的eventloop坑了,这次是node的锅](https://juejin.im/post/5c3e8d90f265da614274218a)
* [nodejs的eventloop,timers和process.nextTick()【译】](https://www.jianshu.com/p/ac64af22d775)
* [详解JavaScript中的Event Loop(事件循环)机制](https://zhuanlan.zhihu.com/p/33058983)
* [The Node.js Event Loop, Timers, and process.nextTick()](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)
- 第一部分 HTML
- meta
- meta标签
- HTML5
- 2.1 语义
- 2.2 通信
- 2.3 离线&存储
- 2.4 多媒体
- 2.5 3D,图像&效果
- 2.6 性能&集成
- 2.7 设备访问
- SEO
- Canvas
- 压缩图片
- 制作圆角矩形
- 全局属性
- 第二部分 CSS
- CSS原理
- 层叠上下文(stacking context)
- 外边距合并
- 块状格式化上下文(BFC)
- 盒模型
- important
- 样式继承
- 层叠
- 属性值处理流程
- 分辨率
- 视口
- CSS API
- grid(未完成)
- flex
- 选择器
- 3D
- Matrix
- AT规则
- line-height 和 vertical-align
- CSS技术
- 居中
- 响应式布局
- 兼容性
- 移动端适配方案
- CSS应用
- CSS Modules(未完成)
- 分层
- 面向对象CSS(未完成)
- 布局
- 三列布局
- 单列等宽,其他多列自适应均匀
- 多列等高
- 圣杯布局
- 双飞翼布局
- 瀑布流
- 1px问题
- 适配iPhoneX
- 横屏适配
- 图片模糊问题
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 内存空间
- 作用域
- 执行上下文栈
- 变量对象
- 作用域链
- this
- 类型转换
- 闭包(未完成)
- 原型、面向对象
- class和extend
- 继承
- new
- DOM
- Event Loop
- 垃圾回收机制
- 内存泄漏
- 数值存储
- 连等赋值
- 基本类型
- 堆栈溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍历对象属性
- 宽度、高度
- performance
- 位运算
- tostring( ) 与 valueOf( )方法
- JavaScript技术
- 错误
- 异常处理
- 存储
- Cookie与Session
- ES6(未完成)
- Babel转码
- let和const命令
- 变量的解构赋值
- 字符串的扩展
- 正则的扩展
- 数值的扩展
- 数组的扩展
- 函数的扩展
- 对象的扩展
- Symbol
- Set 和 Map 数据结构
- proxy
- Reflect
- module
- AJAX
- ES5
- 严格模式
- JSON
- 数组方法
- 对象方法
- 函数方法
- 服务端推送(未完成)
- JavaScript应用
- 复杂判断
- 3D 全景图
- 重载
- 上传(未完成)
- 上传方式
- 文件格式
- 渲染大量数据
- 图片裁剪
- 斐波那契数列
- 编码
- 数组去重
- 浅拷贝、深拷贝
- instanceof
- 模拟 new
- 防抖
- 节流
- 数组扁平化
- sleep函数
- 模拟bind
- 柯里化
- 零碎知识点
- 第四部分 进阶
- 计算机原理
- 数据结构(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- 搜索算法
- 动态规划
- 二叉树
- 浏览器
- 浏览器结构
- 浏览器工作原理
- HTML解析
- CSS解析
- 渲染树构建
- 布局(Layout)
- 渲染
- 浏览器输入 URL 后发生了什么
- 跨域
- 缓存机制
- reflow(回流)和repaint(重绘)
- 渲染层合并
- 编译(未完成)
- Babel
- 设计模式(未完成)
- 函数式编程(未完成)
- 正则表达式(未完成)
- 性能
- 性能分析
- 性能指标
- 首屏加载
- 优化
- 浏览器层面
- HTTP层面
- 代码层面
- 构建层面
- 移动端首屏优化
- 服务器层面
- bigpipe
- 构建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack优化
- Webpack原理
- 实现loader
- 实现plugin
- tapable
- Webpack打包后代码
- rollup.js
- parcel
- 模块化
- ESM
- 安全
- XSS
- CSRF
- 点击劫持
- 中间人攻击
- 密码存储
- 测试(未完成)
- 单元测试
- E2E测试
- 框架测试
- 样式回归测试
- 异步测试
- 自动化测试
- PWA
- PWA官网
- web app manifest
- service worker
- app install banners
- 调试PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 饿了么整理
- 样式
- 技巧
- Vue音乐播放器
- Vue源码
- Virtual Dom
- computed原理
- 数组绑定原理
- 双向绑定
- nextTick
- keep-alive
- 导航守卫
- 组件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 动画(未完成)
- 异常监控、收集(未完成)
- 数据采集
- Sentry
- 贝塞尔曲线
- 视频
- 服务端渲染
- 服务端渲染的利与弊
- Vue SSR
- React SSR
- 客户端
- 离线包
- 第五部分 网络
- 五层协议
- TCP
- UDP
- HTTP
- 方法
- 首部
- 状态码
- 持久连接
- TLS
- content-type
- Redirect
- CSP
- 请求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服务端
- Linux
- Linux命令
- 权限
- XAMPP
- Node.js
- 安装
- Node模块化
- 设置环境变量
- Node的event loop
- 进程
- 全局对象
- 异步IO与事件驱动
- 文件系统
- Node错误处理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服务
- 负载均衡
- 获取用户IP
- 解决跨域
- 适配PC与移动环境
- 简单的访问限制
- 页面内容修改
- 图片处理
- 合并请求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自动化(未完成)
- docker
- 创建CLI
- 持续集成
- 持续交付
- 持续部署
- Jenkins
- 部署与发布
- 远程登录服务器
- 增强服务器安全等级
- 搭建 Nodejs 生产环境
- 配置 Nginx 实现反向代理
- 管理域名解析
- 配置 PM2 一键部署
- 发布上线
- 部署HTTPS
- Node 应用
- 爬虫(未完成)
- 例子
- 反爬虫
- 中间件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源码
- app.js
- config.js
- 消息队列
- RPC
- 性能优化
- 第七部分 总结
- Web服务器
- 目录结构
- 依赖
- 功能
- 代码片段
- 整理
- 知识清单、博客
- 项目、组件、库
- Node代码
- 面试必考
- 91算法
- 第八部分 工作代码总结
- 样式代码
- 框架代码
- 组件代码
- 功能代码
- 通用代码