- 本文也参考了一篇不错的文章: https://zhuanlan.zhihu.com/p/138140285
- 读懂本文需要你对JS和ES6有一定的使用经验( feihua )
回调函数和异步编程是什么鬼?JS为什么需要他们?
- 首先需要了解, JS是单线程的, 这意味着同一时刻JS引擎只能干一件事情
- 将下面这段代码放到浏览器的控制台console中运行试试
console.log(1);
alert(2);
console.log(3);
- 运行结果显而易见, 先输出了1, 紧接着弹层显示2, 当点击弹层的按钮使弹层消失后, 输出了3
- 这里有一个概念, 基于JS单线程的特点, 当我们执行的一段JS代码中含有( 网络请求, 定时任务 )等不能立即获得结果的代码, JS代码的执行就会卡住
- 就比如当上面代码执行到第二行, 当你没有点击弹层的按钮时, alert(2)这行代码就没有获得响应, JS的执行就会卡住, 此时CPU就是空闲的, JS既不继续执行, 页面也不会接着渲染, 这时候就出现了页面卡顿的"白屏"效果, 会给用户造成很差的使用体验
- 好了, 现在需求出现了, 我们想改善用户的使用体验, 也就是说我们希望当遇到不能立即获得结果的代码时, JS代码的执行不会卡住, 聪明的前辈们想出了如下一招
- 当遇到这种不能立即获得结果的代码, 就将这些代码放入一个函数中, 然后, 将这个函数暂存在其他地方并继续执行JS代码, 当暂存的函数"时机到了", 再回过头来执行它( 所以这种函数叫回调函数 ), 这种编程方式就被称为异步编程
- 那行, 使用基于回调函数的异步方式改写如上代码
console.log(1);
setTimeout(function cb() {
alert(2);
});
console.log(3);
-
再放到浏览器console中运行一下试试? 成功地先输出1和3, 然后弹层显示2
- 注意, 这里用定时器可以实现回调函数, 原因在后面谈到event-loop事件循环实现回调的流程的时候会说明
- 异步( 定时器, ajax )要基于回调来实现, DOM事件也基于回调来实现
-
我个人非常佩服前人的这种处理方式, 但只有我们再深入一些了解这种基于回调函数的异步写法背后的实现方式, 才能让我们看得透彻, 用的顺手, 讲的明白, 对面试, 工作都大有裨益, 接下来开始讲述其实现方式—eventLoop事件循环
Event Loop 事件循环实现回调的流程介绍
- 依旧是这段代码, 以小看大
console.log(1);
setTimeout(function cb() {
alert(2);
});
console.log(3);
- 浅尝这段代码的执行过程
- 执行第 1 行代码,
console.log(1)
被压入callStack调用栈, 执行输出1, 然后该行代码从调用栈顶弹出 - 执行2~4行代码, 先被压入call stack调用栈, 发现是异步代码, 然后在Web API专用的存储位置记录一个包含函数cb的定时器, 然后代码从栈顶弹出
- 执行第 5 行代码,
console.log(3)
被压入callStack调用栈, 执行输出3, 然后该行代码从调用栈顶弹出 - 此时同步代码全部执行完毕, 启动event-loop事件循环机制轮询callbackQueue回调队列, 由于当定时器"时机到了", 就会将函数cb放入callbackQueue回调队列, 所以callbackQueue中有事件cb, 事件cb被压入callStack调用栈开始执行
alert(2)
被压入callStack调用栈, 执行后该行代码从调用栈顶弹出, 然后事件cb从调用栈顶弹出
- 执行第 1 行代码,
- 换一段代码, 依旧以小见大
console.log(1);
setTimeout(function cb1() {
alert(2);
});
Promise.resolve().then(function cb2() {
alert(3);
})
console.log(4);
-
运行这段代码, 执行顺序是: 1, 4, 3, 2
-
最开始我也有疑问, 如果cb1和cb2是被暂存在了同一个地方,那么callbackQueue肯定符合先入先出的方式, 怎么说也应该先alert(2), 很显然, 暂存cb1和cb2的时候他们被放在了不同的地方
-
这里先给出结论
- 微任务: 由ES6语法规定, 包括
Promise, async/await
- 宏任务: 由浏览器规定, 包括
setTimeout, setInterval, Ajax, DOM事件
- 微任务执行时机比宏任务早 !!!
- 微任务: 由ES6语法规定, 包括
-
接下来我从事件循环的层面解释一下, 为什么微任务执行时机更早
-
第一步, 同步代码, 一行一行放在 callStack 执行
-
遇到异步宏任务, 记录到 WebAPI, 等待时机( 定时, 网络请求等 ), 时机到了, 就移动到 callbackQueue
-
遇到异步微任务, 记录到 micro-task-queue
-
如果 callStack 为空( 即同步代码执行完 ), 先执行micro-task-queue中的微任务
-
微任务执行完毕, 尝试进行DOM渲染
-
然后 Event Loop 开始工作
-
轮询查找 callbackQueue, 如果有则移动到 callStack 执行
-
然后继续轮询查找( 永动机一样 )
-
- promise 和 async/await 单独写博客吧…也太多了(理不直气也壮!)