[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引擎再將結果返回給用戶。

<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里面有以下增加:


<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算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼