一、同步 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!");
}