在现代JavaScript开发中,异步编程已成为不可或缺的一部分,它使我们能够处理耗时操作,如网络请求、文件读写,而不阻塞主线程。在最近的学习中了解到了Promise和Event Loop,本文将深入探讨他们的工作原理、如何优雅地使用Promise控制异步流程,以及其在JavaScript事件循环机制中的角色。
异步编程的需求
在Web开发或其他涉及I/O操作的场景中,经常遇到需要等待某个操作完成才能继续执行的情况。传统的解决方案是使用回调函数,但随着业务复杂度上升,回调函数层层嵌套,形成了所谓的“回调地狱”,这不仅使得代码难以阅读和维护,还可能引发调试难题。
如下面代码,要求需要执行完a再执行b再执行c执行d(假设每个方法中都存在setTimeout),当嵌套无限多时,简直就是地狱。
function a(cbB,cbC,abD){ cbB(cbC,cbD) } function b(cb,cbD){ cb(cbD) } function c(cb){ cb() } function d(){ } a(b,c,d)
Promise的诞生
Promise,正如其名,代表了一个未来的值,它或者成功(resolved)并携带结果数据,或者失败(rejected)并携带错误信息。Promise的设计初衷就是为了解决异步编程中的复杂性问题,提供一种更优雅、链式调用的方式来组织异步操作。
Promise的基本使用
通过new Promise((resolve, reject) => {...})
构造函数创建,其中resolve
和reject
是两个函数,分别用于改变Promise的状态。
我以一个案例来给大家讲解一下。当getup()
状态为resolve()
时.then()
中的computer()
开始执行。
function getup() { //pending resloved rejected return new Promise((resolve, reject) => { setTimeout(() => { console.log('小帅起床了'); resolve() }, 2000) }) } function computer() { return new Promise((resolve, reject) => { setTimeout(() => { console.log('小帅打开了电脑'); resolve() }, 1000) }) } function study(params) { console.log('小帅开始敲代码了'); } getup().then(() => { return computer() }) .then(() => { study() })
执行结果
.then方法
是promise原型上的一个函数,x.then函数会在x这个promise实例对象状态变更为resolved之后才执行内部逻辑,由此借助这个机制可以将异步捋成同步。then方法支持链式调用,因为then默认也会返回一个promise对象,但状态默认是pending,这就会导致后面的then用不上前面then的状态,从而继续往前查找。我们在then中返回一个promise对象,会覆盖掉then自带的返回。
还是以上面代码为例,删除computer前的return。为什么执行结果是先敲代码再打开电脑呢?
因为第一个then没有返回值,但是它会默认返回一个Promise
,但这个Promise
的状态默认是pending
(进行中),第二个then无法使用到第一个then的状态,那么第二个then就会继续往前查找,发现getup()
状态为resolve
,此时它就会立即执行,而第一个then则在2s后执行。
不删除return时,第一个then返回的是computer()
的Promise
,此时第二个then可以识别到第一个then中的状态为resolve
,所以会等待computer()
执行完后再执行
getup().then(() => { computer() }) .then(() => { study() })
执行结果
.catch方法
用于捕获Promise链中任何环节抛出的错误,保证异常处理的一致性。
如一下代码,如果Promise的状态为reject时,会导致程序报错,增加.catach()方法捕捉错误,就会将错误处理不会再导致程序报错。
function a(){ return new Promise(function(resloved,reject){ setTimeout(function(){ console.log('a is ok'); // resloved('请求到的数据') reject('错误') },1000) }) } function b() { console.log(1); } a().then((res)=>{ console.log(res); b() }) .catch((err)=>{ console.log(err,'xxxx'); })
执行结果
有catch时
无catch
浏览器引擎
JavaScript引擎和渲染引擎都是现代浏览器的重要组成部分。
JavaScript引擎负责解析和执行JavaScript代码。它让网页具有交互性,处理用户事件,执行异步操作,以及操纵网页文档对象模型(DOM)等。知名的JavaScript引擎例如Google Chrome的V8、Mozilla Firefox的SpiderMonkey、Apple Safari的JavaScriptCore(也被称作Nitro或SquirrelFish)等。
渲染引擎(又称为布局引擎或呈现引擎)则负责解析HTML和CSS,构建网页的可视化表示,并对其进行布局和绘制。它确保网页内容按照CSS样式和网页标准进行正确显示。常见的渲染引擎有Blink(用于Chrome和Opera)、Gecko(用于Firefox)、WebKit(用于Safari)以及历史上Internet Explorer的Trident(也称MSHTML)和Microsoft Edge早期版本的EdgeHTML。
事件循环(Event Loop)
事件循环是JavaScript处理异步操作的核心机制,确保了即使在单线程环境下也能高效地管理任务执行顺序。
任务类型
宏任务(Macro Task) :包括
setTimeout
、setInterval
、setImmediate
(Node.js环境)、I/O操作、UI渲染等,它们在当前执行栈完成后执行。微任务(Micro Task) :如
Promise
的回调、process.nextTick
(Node.js)、MutationObserver
等,它们在当前执行栈的末尾执行,优先级高于宏任务。
执行流程
初始化阶段:程序开始执行时,调用栈是空的,但底部隐含了全局执行上下文。同时,微任务队列和宏任务队列也是初始状态,宏任务队列中通常有一个代表整个脚本的宏任务。
执行脚本:全局执行上下文被推入调用栈,开始同步执行脚本中的代码。执行过程中,任何产生的宏任务或微任务会被分别放入对应的队列中。
脚本执行完毕:当当前宏任务(通常是整个脚本)执行结束,调用栈清空至全局上下文,此时检查微任务队列。
执行微任务:如果微任务队列不为空,则从队首开始执行微任务,直至队列为空,期间新产生的微任务也会立即执行。此过程会持续进行,直到没有更多的微任务加入队列。
宏任务执行:微任务全部执行完毕后,从宏任务队列中取出下一个任务(如果有),将其推入调用栈执行。执行过程中,可能继续产生新的宏任务和微任务。
渲染操作:在每次宏任务执行后,如果事件循环检测到渲染时机(即调用栈为空且没有待执行的微任务),渲染引擎会尝试更新界面。
循环继续:事件循环不断重复上述过程,检查队列,执行任务,直到所有任务(包括宏任务和微任务)都被处理完。
通过这样的机制,JavaScript能够高效地管理同步和异步代码,确保在单线程环境中既能够执行复杂的异步操作,又不阻塞UI渲染或其他任务的执行。
结语
Event Loop机制支撑起了JavaScript的异步执行模型,而Promise作为一种高级的异步编程抽象,利用事件循环的特性,提供了清晰、灵活的异步解决方案,二者共同构成了现代JavaScript异步编程的核心基础。