一、同步 vs 異步
1.?什么是同步(Synchronous)
同步代碼就是一行一行、按順序執行的。當前行沒有執行完,下一行不能動。
示例:
console.log("A");
console.log("B");
console.log("C");
輸出:
A
B
C
-
每一行都在主線程中立即執行。
-
執行過程是“阻塞式”的,一步接一步。
特點:
-
代碼執行邏輯清晰;
-
會阻塞后續代碼執行(如文件讀取、網絡請求都在等);
-
無法應對高延遲操作(如 I/O、Ajax)。
2.?什么是異步(Asynchronous)
異步指部分代碼注冊后,延遲執行,而不會阻塞后面的代碼。
這種設計來源于瀏覽器環境需要執行大量耗時操作(網絡、動畫、計時器、用戶事件)但又不能卡住主線程。
示例:
console.log("A");setTimeout(() => {console.log("B"); // 異步:定時回調
}, 1000);console.log("C");
輸出順序:
A
C
B
發生了什么:
-
打印 A;
-
setTimeout
注冊了一個回調函數,交給瀏覽器處理(計時中); -
繼續執行 C;
-
1 秒后瀏覽器將回調推入任務隊列,事件循環機制決定它被執行。
場景 | 原因 | 異步的表現 |
---|---|---|
網絡請求 | 不確定響應時間 | fetch , XHR 是異步 |
動畫行為收集 | 用戶行為可能隨時發生 | 通過事件監聽異步觸發 |
逆向處理參數 | 有些參數晚一點才生成 | 通過 setTimeout , Promise 拖延生成 |
3.?同步與異步代碼混用的真實執行順序
console.log("A");setTimeout(() => {console.log("B");
}, 0);Promise.resolve().then(() => {console.log("C");
});console.log("D");
輸出結果:
A
D
C
B
執行流程:
階段 | 內容 |
---|---|
同步 | 執行 console.log("A") → 輸出 A |
同步 | 注冊 setTimeout 回調到宏任務隊列 |
同步 | 注冊 Promise.then 回調到微任務隊列 |
同步 | 執行 console.log("D") → 輸出 D |
微任務階段 | 執行 console.log("C") → 輸出 C |
宏任務階段 | 執行 console.log("B") → 輸出 B |
總結順序:
-
同步代碼(立即)
-
微任務(如 Promise.then)
-
宏任務(如 setTimeout)
關鍵點:
在 JS 中,異步任務分為兩類:
類型 | 叫法 | 舉例 |
---|---|---|
微任務(Microtask) | 優先級高 | Promise.then , queueMicrotask , MutationObserver |
宏任務(Macrotask) | 優先級低 | setTimeout , setInterval , setImmediate (Node), I/O |
關于?setTimeout(fn, 0):
即使寫的是 setTimeout(..., 0)
,它也不會立即執行,而是被安排到宏任務隊列的下一輪事件循環中執行。也就是說最快也得等當前執行棧 + 所有微任務清空之后,才輪到宏任務執行。
4.?結合自動化和逆向理解
自動化 / 爬蟲:
很多網站 JS 會這樣寫:
setTimeout(() => {window.secretToken = "abc123";
}, 2000);
如果直接抓 DOM,token 還沒生成!
正確做法:
-
加
sleep
等待; -
或逆向分析
setTimeout
回調; -
或用 DevTools 打斷點觀察。
逆向分析:
某些加密代碼中這樣寫:
const result = doEncrypt(params);
setTimeout(() => {sendToServer(result);
}, 300);
這時需要明白:
-
加密結果是同步生成;
-
發送是異步的;
-
想抓包或還原,就要跟蹤異步部分。
二、回調函數
回調函數就是:被當作參數傳遞給另一個函數,并在特定時機被“回調執行”的函數。
本質上:函數作為參數傳入另一個函數,等時機到了“你來調用我”。
為什么需要回調?
因為 JS 是單線程 + 異步機制,我們不能讓耗時操作卡住主線程。
所以我們讓這些操作“干完后再告訴我”——這就需要回調!
例子:
function doSomething(cb) {console.log("處理任務中...");cb(); // 回調函數
}doSomething(function () {console.log("任務完成!");
});
輸出:
處理任務中...
任務完成!
1.?回調在異步中最常見
setTimeout(function () {console.log("3秒后執行的回調函數");
}, 3000);
上面這個函數是經典異步回調:
-
你把函數傳給
setTimeout
-
它3秒后“調用”這個函數
-
中間不會阻塞主線程
2.?經典使用場景(逆向 / 爬蟲 / 自動化)
場景 | 回調表現 | 該注意的點 |
---|---|---|
爬蟲中解析響應 | .then(res => {...}) 是回調 | token、數據提取往往藏在回調內部 |
逆向 setTimeout | 回調中包含參數生成 / 網絡請求 | 要“追進函數體”里找核心邏輯 |
自動化行為 | 事件觸發(點擊后執行) | 回調函數中可能有驗證邏輯 |
加密函數 | encrypt(data, key, callback) | 最終結果來自 callback 中 |
3.?回調的三種常見寫法
匿名函數
doSomething(function() {console.log("匿名回調");
});
命名函數(推薦調試時使用)
function myCallback() {console.log("我是命名回調");
}
doSomething(myCallback);
箭頭函數
doSomething(() => {console.log("箭頭函數回調");
});
4.?回調地獄(Callback Hell)警告
寫了一大堆嵌套回調時,會像這樣:
setTimeout(() => {getData((res) => {parseData(res, (result) => {saveToDB(result, () => {console.log("全部完成");});});});
}, 1000);
?這就是“回調地獄”:代碼難讀、難調試、難維護。
解決方法:使用 Promise 或 async/await
5. 在逆向時如何識別和處理回調?
場景判斷:
encrypt(data, (result) => {send(result);
});
分析:
-
真正加密后的數據 result,不在 encrypt 函數外部;
-
你不能直接 return,必須追蹤“回調函數”;
-
DevTools 可以對
encrypt
打斷點,在執行cb(result)
的那一行觀察 result。
三、Promise
Promise 是 JavaScript 的一種異步編程解決方案。
它代表一個未來某個時刻才會結束的操作(異步操作),可以是成功(fulfilled)或失敗(rejected)。
可以把它理解成一個“承諾”:
-
我現在還沒完成任務,
-
但我答應你完成之后會通知你(通過 then / catch)。
1.?Promise 三種狀態
┌──────────────┐│ pending │ ← 初始狀態└────┬─────────┘↓┌────────────────────┐│ fulfilled 或 rejected │ ← 只能變成這兩種之一,且一旦變了就不能再變└────────────────────┘
-
pending:初始狀態,進行中。
-
fulfilled:調用
resolve()
,表示成功。 -
rejected:調用
reject()
,表示失敗。
2.?如何創建一個 Promise?
基本語法:
const p = new Promise((resolve, reject) => {// 異步邏輯if (成功) {resolve(value); // 成功時調用} else {reject(error); // 失敗時調用}
});
new Promise(...)
- 創建一個 Promise 實例,它代表一個“未來才會返回值”的異步任務。
(resolve, reject) => { ... }
-
Promise 構造函數的執行器函數(executor)
-
resolve
:告訴外部“我異步成功了”,并傳出結果; -
reject
:告訴外部“我失敗了”,并傳出錯誤信息。 -
這兩個函數由 JS 引擎提供,不用自己定義,只需要在合適的時機調用它們。
3. 如何使用 Promise?(then / catch)
p.then(result => {console.log("成功:", result);
}).catch(err => {console.log("失敗:", err);
});
表示:
“等 p 這個異步流程完成后:
-
如果成功了,就執行 then 回調;
-
如果失敗了,就執行 catch 回調;
-
保證整個過程不阻塞主線程。”
可視化執行流程
-
創建 Promise,狀態為
pending
-
執行異步邏輯
-
如果執行成功 → 調用
resolve(value)
→ 狀態變為fulfilled
-
如果失敗 → 調用
reject(error)
→ 狀態變為rejected
-
外部通過
.then()
/.catch()
監聽這兩個結果
4.?實戰示例:模擬網絡請求
function fakeRequest() {return new Promise((resolve, reject) => {setTimeout(() => {const ok = Math.random() > 0.2;if (ok) {resolve("數據返回成功!");} else {reject("請求失敗!");}}, 1000);});
}fakeRequest().then(data => console.log("結果:", data)).catch(err => console.error("錯誤:", err));
執行過程
-
fakeRequest()
被調用; -
new Promise(...)
執行,并注冊setTimeout
任務; -
setTimeout(fn, 1000)
→ 1000ms 后,將回調fn
放入“宏任務隊列”中; -
事件循環機制(Event Loop) 每一輪都先清完主線程和微任務,再處理宏任務;
-
如果主線程還沒清空(例如你阻塞它)→ 宏任務隊列暫時不會執行;
-
一旦主線程空閑,事件循環會去執行
fn()
,也就是?resolve()
或reject()
。
5.?鏈式調用:then 可以 return 新的 Promise
Promise.resolve(5).then(x => {console.log("第一步:", x); // 5return x + 1; // 返回普通值(6)}).then(y => {console.log("第二步:", y); // 6return Promise.resolve(y * 2); // 返回 Promise(12)}).then(z => {console.log("第三步:", z); // 12});
輸出是:
第一步: 5
第二步: 6
第三步: 12
6.?Promise 的執行流程
console.log("1");const p = new Promise((resolve, reject) => {console.log("2");resolve("ok");
});p.then(res => {console.log("3");
});console.log("4");
輸出順序是:
1
2
4
3 ← 異步微任務(放入微任務隊列)
-
Promise 回調(構造器中的代碼)是同步執行的;
-
.then()
的回調是在當前同步任務執行完后,通過微任務隊列執行。
階段 | 同步還是異步 | 執行時間 |
---|---|---|
Promise 構造函數里的代碼 | ?同步 | 立即執行 |
resolve() / reject() | ?同步 | 立即標記狀態,不會立即觸發 .then() |
.then(...) 回調 | ?異步(微任務) | 主線程清空 + 微任務階段執行 |
常用方法匯總
方法 | 含義 |
---|---|
Promise.resolve(value) | 創建一個立即成功的 Promise |
Promise.reject(error) | 創建一個立即失敗的 Promise |
Promise.all([p1, p2]) | 所有 Promise 成功才成功,否則失敗(聚合并發) |
Promise.race([p1, p2]) | 誰先完成就返回誰(無論成功失敗) |
Promise.allSettled([p1, p2]) | 所有都執行完后返回狀態和結果 |
p.then() | 成功時執行 |
p.catch() | 失敗時執行 |
p.finally() | 無論成功失敗都會執行 |
四、async/await
async/await
是基于 Promise 的語法糖,讓我們可以用“同步代碼的寫法”來寫異步邏輯,代碼更簡潔、可讀性更強。
1.?async 關鍵字
用法:
async function func() {return "hello";
}
等價于:
function func() {return Promise.resolve("hello");
}
結論:
所有 async 函數,默認返回一個 Promise,無論你 return 的是值還是 Promise。
2.?await 關鍵字
用法:
const value = await somePromise();
-
await
只能在async
函數中使用; -
它會等待
somePromise()
執行完成后,把結果賦值給value
; -
本質上是:暫停當前函數執行,等 Promise 完成后再繼續往下執行。
3. 例子
async function test() {console.log("開始");const value = await new Promise(resolve => setTimeout(() => resolve("OK"), 1000));console.log("拿到結果:", value);
}test();
console.log("函數外繼續執行");
輸出:
開始
函數外繼續執行
(1秒后)
拿到結果: OK
4.?async/await 替代 Promise 鏈的優點
Promise 鏈式調用寫法:
getToken().then(token => encrypt(token)).then(result => send(result)).catch(err => console.error("出錯:", err));
async/await 寫法:
async function main() {try {const token = await getToken();const result = await encrypt(token);await send(result);} catch (err) {console.error("出錯:", err);}
}
優點:
-
邏輯更清晰;
-
更易調試;
-
避免 then 的嵌套。
5.?多個 await 并行 vs 串行
串行寫法(不好):
const a = await task1();
const b = await task2(); // 必須等 task1 完成
并行寫法(性能更好):
const [a, b] = await Promise.all([task1(), task2()]);
在爬蟲 / 自動化中,如果多個異步任務互不依賴,建議用 Promise.all
提高性能。
6. async/await 本質圖示:
async function main() {const a = await step1();const b = await step2(a);return b;
}
相當于:
function main() {return step1().then(a => {return step2(a);});
}
寫得像同步,其實底層是 Promise 鏈。
五、事件循環機制(Event Loop)
JavaScript 是單線程語言:一次只能執行一個任務。
但現實中很多任務是異步的,比如:
-
讀取文件 / 網絡請求
-
DOM 事件回調
-
定時器(
setTimeout
) -
Promise 等微任務
事件循環機制就是用來處理:
哪些代碼現在執行?
哪些稍后執行?
哪些排在下一個事件循環周期執行?
1.?執行模型整體結構圖
┌───────────────────────┐
│ Call Stack │ ← 主線程任務(同步任務)
└──────────┬────────────┘↓┌────────────┐│ Event Loop │ ← 核心調度機制└────┬───────┘↓┌─────────────────────┐│ 宏任務隊列 (task) │ ← 定時器、setTimeout、UI渲染└─────────────────────┘┌─────────────────────┐│ 微任務隊列 (microtask)│ ← Promise.then、queueMicrotask└─────────────────────┘
事件循環機制就是:
-
不斷檢查當前是否有同步代碼可執行;
-
執行完同步代碼后,清空所有微任務隊列;
-
然后從宏任務隊列中取出下一個任務執行;
-
循環往復。
2.?任務分類(宏任務 vs 微任務)
類型 | 說明 | 舉例 |
---|---|---|
?同步任務 | 立即執行的代碼 | 普通函數、for、console.log |
?微任務(microtask) | 當前宏任務執行完之后立即執行 | Promise.then 、queueMicrotask 、MutationObserver |
?宏任務(task) | 等待事件循環調度執行 | setTimeout 、setInterval 、setImmediate 、I/O 事件 |
3.?經典執行順序示例
console.log("A");setTimeout(() => {console.log("B");
}, 0);Promise.resolve().then(() => {console.log("C");
});console.log("D");
輸出順序:
A
D
C
B
解釋:
-
A → 同步,立即執行;
-
setTimeout → 宏任務,掛起等待;
-
Promise.then → 微任務,加入微任務隊列;
-
D → 同步,立即執行;
-
微任務 C 執行;
-
再執行下一個事件循環,從宏任務隊列中取出 B 執行。
事件循環的每一輪是怎樣的?
每一輪事件循環:
-
執行主線程中的同步代碼;
-
執行所有的微任務隊列(直到清空);
-
執行宏任務隊列中排隊的任務(如定時器);
-
回到第 1 步。
總結
異步任務 | 表現 | 逆向價值 |
---|---|---|
setTimeout(fn, 0) | 延遲執行加密函數 | fn 可能是參數生成主力 |
Promise.then(...) | 數據一步步解密 | then 中是核心解密邏輯 |
await fn() | 底層是微任務 | 實際運行時間可能不是你想象的 |
頁面加載過程 | 異步注入、動態構造 | 需要調試時理解何時運行哪些代碼 |
事件循環 = 同步 + 微任務 + 宏任務 + 無限循環調度,是 JavaScript 異步執行的核心調度機制。
六、任務隊列(宏任務、微任務)
JavaScript 是單線程,為了處理異步(比如網絡請求、定時器、DOM 事件等),需要一種機制去排隊等待執行,這就是任務隊列。
事件循環每次循環執行的順序是:
同步任務 → 微任務 → 宏任務(從任務隊列中取一個)→ 微任務 → 宏任務...
1.?任務隊列分類總覽
類別 | 含義 | 舉例 | 調度時機 |
---|---|---|---|
?宏任務(MacroTask) | 主流程中的任務 | setTimeout 、setInterval 、I/O 回調、主線程執行代碼 | 當前宏任務執行完 → 才輪到下一個宏任務 |
?微任務(MicroTask) | 優先級更高的小任務 | Promise.then 、MutationObserver 、queueMicrotask | 當前同步代碼執行完 → 立刻執行所有微任務 |
2.?任務隊列運行順序(經典圖)
JS 執行順序如下:同步代碼
↓
所有微任務
↓
一個宏任務(從宏隊列中取出執行)
↓
所有微任務
↓
下一個宏任務
↓
...
3.?實戰分析例子
console.log("1");setTimeout(() => {console.log("2");
}, 0);Promise.resolve().then(() => {console.log("3");
});console.log("4");
輸出順序:
1
4
3
2
執行流程拆解:
階段 | 執行內容 |
---|---|
同步 | 打印 1,注冊 setTimeout(進宏任務隊列),注冊 Promise.then(進微任務隊列),打印 4 |
微任務 | 執行 Promise.then,打印 3 |
宏任務 | 執行 setTimeout,打印 2 |
4.?常見異步API分類
類型 | 屬于 | 執行順序優先級 |
---|---|---|
setTimeout(fn, 0) | 宏任務 | 低 |
setInterval(fn) | 宏任務 | 低 |
setImmediate(fn) (Node.js) | 宏任務 | 中 |
Promise.then | 微任務 | 高 |
queueMicrotask(fn) | 微任務 | 高 |
MutationObserver | 微任務 | 高(用于監聽 DOM 變化) |
5.?queueMicrotask 與 Promise 的區別
queueMicrotask(() => console.log("A"));
Promise.resolve().then(() => console.log("B"));
它們都屬于微任務,但執行順序是:誰先注冊,誰先執行。
-
queueMicrotask
直接添加到微任務隊列; -
Promise.then
會經歷狀態轉換后才進入微任務隊列(有輕微延遲)。
總結
微任務是“立刻執行但排隊”的異步任務;宏任務是“下一輪事件循環”才執行的異步任務。
七、setTimeout、setInterval、queueMicrotask
1. setTimeout(fn, delay)
作用:
在“至少 delay 毫秒后”執行回調函數 fn
。常用于延時執行任務。
機制:
-
setTimeout
回調會被加入宏任務隊列; -
真正執行要等:
-
當前同步任務 + 所有微任務執行完;
-
然后進入下一輪事件循環;
-
才執行 setTimeout 的回調。
-
示例:
console.log("1");setTimeout(() => {console.log("2");
}, 0);console.log("3");
輸出順序:
1
3
2
即使是 0ms
,也不是立即執行,而是等所有同步和微任務跑完后。
逆向常見用途:
-
混淆代碼用
setTimeout(fn, 0)
延遲執行加密函數,迷惑調試; -
加密邏輯拆散,分多輪任務執行。
2. setInterval(fn, delay)
作用:
每隔 delay 毫秒執行一次 fn
,循環執行。
機制:
-
每次都把
fn
放入宏任務隊列; -
如果某次回調執行太久,下一次會延后執行,不會并發。
示例:
let i = 0;
let timer = setInterval(() => {console.log(++i);if (i === 3) clearInterval(timer);
}, 1000);
輸出:
1
2
3
逆向常見用途:
-
模擬用戶行為(點擊、滑動);
-
定時發起加密請求;
-
混淆邏輯每隔一段時間檢查調試器狀態(反調試機制)。
3. queueMicrotask(fn)
作用:
將一個函數添加到微任務隊列,在當前同步任務執行完后、下一次事件循環之前立刻執行。
等效于:
Promise.resolve().then(fn);
特點:
-
執行優先級高于宏任務(如 setTimeout);
-
非常適合需要在當前邏輯后立即運行的輕量異步任務;
-
不支持 delay、不可取消。
示例:
console.log("A");queueMicrotask(() => {console.log("B");
});console.log("C");
輸出順序:
A
C
B
與 Promise.then 的微妙區別:
queueMicrotask(() => console.log("Q"));
Promise.resolve().then(() => console.log("P"));
兩者都屬于微任務,誰先注冊誰先執行。
逆向常見用途:
-
立即執行重要計算邏輯,但避免阻塞主線程;
-
某些混淆代碼故意利用 queueMicrotask 拆分解密邏輯;
-
用于繞過同步斷點調試(你下斷點已經太晚了)。
4.?三者比較表
特性 | setTimeout | setInterval | queueMicrotask |
---|---|---|---|
類型 | 宏任務 | ?宏任務 | ?微任務 |
延遲時間 | ?可設置 | ?可設置 | ?無延遲 |
是否重復 | ?一次性 | ?循環 | ?一次性 |
是否立即執行 | ?等下輪 | ?等下輪 | ?微任務階段立即執行 |
取消方式 | clearTimeout | clearInterval | ?不能取消 |
常用于 | 延遲執行任務 | 定時輪詢、心跳 | ?立即執行異步邏輯 |
5. 舉例
示例代碼
console.log("A");setTimeout(() => {console.log("B");
}, 0);queueMicrotask(() => {console.log("C");
});Promise.resolve().then(() => {console.log("D");
});console.log("E");
輸出結果:
A
E
C
D
B
執行流程詳解(按順序)
階段 | 執行內容 |
---|---|
同步階段 | 輸出 A |
注冊 setTimeout(進入宏任務隊列) | |
注冊 queueMicrotask(進入微任務隊列) | |
注冊 Promise.then(進入微任務隊列) | |
同步階段繼續,輸出 E | |
同步結束,開始執行所有微任務(按注冊順序): | |
→ 輸出 C (queueMicrotask) | |
→ 輸出 D (Promise.then) | |
微任務全部執行完畢 | |
開始下一輪事件循環,執行宏任務(setTimeout) | |
→ 輸出 B |
總結規律
順序 | 代碼 | 類型 | 何時執行 |
---|---|---|---|
?1 | console.log("A") | 同步任務 | 立即執行 |
?2 | setTimeout(...) | 宏任務 | 下一輪事件循環 |
?3 | queueMicrotask(...) | 微任務 | 本輪同步任務后,立即執行 |
?4 | Promise.then(...) | 微任務 | 本輪同步任務后,立即執行(排在 queueMicrotask 后面僅因注冊順序) |
?5 | console.log("E") | 同步任務 | 立即執行 |
?6 | console.log("C") | 微任務 | 本輪微任務階段 |
?7 | console.log("D") | 微任務 | 本輪微任務階段 |
?8 | console.log("B") | 宏任務 | 下一輪事件循環 |
八、Hook 所有 setTimeout
注冊
代碼如下:
setTimeout = new Proxy(setTimeout, {apply(target, thisArg, args) {console.log("[HOOK] setTimeout callback:", args[0].toString());return Reflect.apply(target, thisArg, args);}
});
這段代碼利用 JavaScript 的 Proxy
技術來 Hook(鉤子)setTimeout
函數。
會攔截所有對 setTimeout
的調用,并打印出你傳給它的回調函數內容(也就是 args[0]
),然后繼續正常執行原來的 setTimeout
。
解釋:
setTimeout = new Proxy(setTimeout, {
意思是:用代理 Proxy
包一層原始的 setTimeout
函數,從現在開始,任何人調用 setTimeout(...)
,都會經過這個代理。
apply(target, thisArg, args) {
apply
是 Proxy
針對函數調用的攔截器(trap):
-
target
: 原始的setTimeout
函數; -
thisArg
: 調用時的上下文(通常無關緊要,因為setTimeout
沒用this
); -
args
: 實際調用時傳入的參數數組,比如:
setTimeout(() => {alert("hi");
}, 1000);// args = [() => { alert("hi") }, 1000]
console.log("[HOOK] setTimeout callback:", args[0].toString());
打印出傳入的**第一個參數(回調函數)**的源碼。
-
可以看到混淆過的、反調試的、延遲執行的核心邏輯;
-
有時還能直接把被混淆的 payload 打印出來分析。
return Reflect.apply(target, thisArg, args);
Reflect.apply
是標準方式,用來以原本的方式繼續調用原函數,不打斷原來邏輯。
?Hook 了它,但還讓它照常執行,這樣就不會破壞原網站邏輯,同時還能做分析。
舉例
setTimeout(function() {console.log("secret!");
}, 1000);
運行之后會打印:
[HOOK] setTimeout callback: function() {console.log("secret!");
}