深入探究JS中的异步编程和事件循环机制
异步编程
js是单线程事件循环模型,同步操作与异步操作时代码所依赖的核心机制。异步行为是为了优化因计算量大而时间长的操作。
同步:当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等下去,直到请求返回。
异步:当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会直接向下执行,不需要等待,当消息返回时再通知进程处理。
Promise
Promise的出现主要时解决之间异步编程使用回调函数的回调地狱的问题。
Promise的状态
Promise有三种状态:
- pending:待定
- fulfilled:成功
- rejected:失败
两种状态变化:
- pending -> fulfilled:在promise中调用resolve()函数会产生此状态转变
- pending ->rejected:在promise中调用reject()函数会产生此状态转变
promise的基本流程
Promise构造函数
用于创建Promise对象,需要传入一个执行器函数,函数中需要带两个参数:resolved,rejected;用于修改Promise的状态
let p = new Promise((resolved,rejected)=>{ console.log(1); resolved(); }) // 1 console.log(p); //Promise {<fulfilled>: undefined}
Promise.prototype.then()
用于定义成功或失败状态的回调函数,并返回一个新的Promise。
let p = new Promise((resolved,rejected)=>{ setTimeout(()=>{ resolved("ok"); },1000); }); p.then(res=>{ console.log(res); },err=>{ console.log(err); }) // ok let p = new Promise((resolved,rejected)=>{ setTimeout(()=>{ rejected("err"); },1000); }); p.then(res=>{ console.log(res); },err=>{ console.log(err); }) // err
Promise.prototypr.catch()
用于定义失败的回调,then方法的语法糖,相当于.then(undefined,onRejected);
let p = new Promise((resolved,rejected)=>{ setTimeout(()=>{ rejected("err"); },1000); }); p.then(res=>{ console.log(res); }).catch(err=>{ console.log(err); }) // err
Promise.resolve()
返回一个成功或者失败的Promise;
let p = Promise.resolve("ok"); //Promise {<fulfilled>: ok} let p = Promise.resolve(Promise.reject('err')); //Promise {<rejected>: 'err'}
Promise.all()
返回一个新的Promise,只有所有的Promise都成功才成功,否则有一个失败就返回失败的promise
let p = Promise.all([ Promise.resolve(1), Promise.resolve(2), Promise.resolve() ]) // Promise {<fulfilled>: [1,2,undefined]} let p = Promise.all([ Promise.reject('err'), Promise.resolve(1), Promise.resolve(2) ]) // Promise {<rejected>: 'err'}
Promise.race()
返回一个新的Promise,第一个完成的Promise的结果状态就是最终的结果状态。
let p = Promise.race([ Promise.resolve(1), Promise.resolve(2), Promise.resolve() ]) // Promise {<fulfilled>: 1} let p = Promise.all([ Promise.reject('err'), Promise.resolve(1), Promise.resolve(2) ]) // Promise {<rejected>: 'err'}
Promise原理实现
function Promise(excutor){ const self = this; self.status = "pending"; self.data = undefined; self.callbacks = [] // 用来保存所有待调用的回调函数 function resolve(value){ if(self.status !== "pending"){ return; } // 改变状态 self.status = "fulfilled"; self.data = value; // 异步调用回调函数 if(self.callbacks.length > 0){ setTimeout(()=>{ self.callbacks.forEach((obj)=>{ obj.onResolved(); }) }) } } function reject(reason){ if(self.status !== "pending"){ return; } // 改变状态 self.status = "rejected"; self.data = value; // 异步调用回调函数 if(self.callbacks.length > 0){ setTimeout(()=>{ self.callbacks.forEach((obj)=>{ obj.onRejected(); }) }) } } // 立即同步调用执行器函数 try{ excutor(resolve,reject); }catch(error){ reject(error); } } Promise.prototype.then = function(onResolved,onRejected){ const self = this; onResolved = typeof onResolved === 'function'?onResolved:value=>value; onRejected = typeof onResolved === 'function'?onRejected:reason=>{throw reason}; // 返回promise对象 return new Promise((resolve,reject)=>{ // 执行回调函数 function handle(callback){ try{ const x = callback(self.data); // 如果返回对象为promise if(x instanceof Promise){ x.then(resolve,reject); }else{ resolve(x); } }catch(err){ reject(err); } } // 定义回调之前,状态已经改变 if(self.status == 'fulfilled'){ setTimeout(()=>{ handle(onResolved); }) }else if(self.status == 'rejected'){ setTimeout(()=>{ handle(onRejected); }) }else{ // 定义回调之前状态还未改变 self.callbacks.push({ onResolved(){ handle(onResolved); }, onRejected(){ handle(onRejected); } }) } }) } Promise.prototype.catch = function(onRejected){ return this.then(undefined,onRejected) } Promise.resolve = function(value){ return new Promise((resolve,reject)=>{ if(value instanceof Promise){ value.then(resolve,reject); }else{ resolve(value); } }) } Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason); }) } Promise.all = function(promises){ return new Promise((resolve,reject)=>{ let count = 0; let plen = promises.length; let values = new Array(plen); for(let i = 0;i < plen;i++){ Promise.resolve(promises[i]).then(value=>{ count++; value[i] = value; if(count == plen) resolve(values); },err=>{ reject(err); }) } }) } Promise.race = function(promises){ return new Promise((resolve,reject)=>{ for(let i = 0;i < promises.length;i++){ Promise.resolve(promises[i]).then(value=>{ resolve(value); },err=>{ reject(err); }) } }) }
异步函数async/await
ES8提出使用async和await解决异步结构组织代码的问题
async
async关键字用于声明异步函数,使用async关键字可以让函数具有异步特征,但总体上其代码仍然时同步求值。
async function fn1(){ console.log(1); } fn1(); console.log(2); // 1 // 2
async的返回值是一个Promise对象,如果函数中返回一个值value,则async则返回Promise.resolve(value);
await
因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。
await关键字必须在异步函数中使用。
async/await中真正起作用的是await。async关键字,无论从哪个方面来看,都是一个标识符。异步函数中不包含await关键字,其执行基本上跟普通函数没有区别。
async function fn(){ console.log(1); let p = await Promise.resolve('ok'); console.log(p); console.log(2); } fn(); console.log(3); // 1 // 3 // ok // 2
js事件循环机制
js运行机制
因为js时单线程的,在执行代码时,将不同函数的执行上下文压入栈中来保证代码的有序执行,在执行同步代码的时候,如果遇到异步代码,js并不会等待其返回结果,而是将异步代码交给对应的异步代码处理线程处理,继续执行栈中的任务。
当异步事件执行完毕之后,再将异步事件对应的对调加入异步执行队列,当主栈中的函数执行完毕后,会从异步执行队列中选择任务来执行。
宏任务和微任务
在任务队列中异步回调分为2中:微任务和宏任务。也就是说任务队列可以分为两种:微任务队列和宏任务队列。
宏任务:全局代码,settimeout/setinterval,UI渲染,
微任务:promise.then,catch,finally,process.nextTick
宏任务和微任务的执行顺序
代码从开始执行调用全局执行栈,script标签内的代码做为宏任务执行。
执行过程中同步代码立即执行,异步代码由异步处理线程处理然后放入任务队列中。
执行过程中同步代码立即执行,异步代码放入任务队列中。
先看任务队列中的微任务队列是否存在微任务
有微任务:执行微任务队列中的所有微任务
微任务执行过程中所产生的微任务放到微任务队列中,继续执行。
如果没微任务,查看是否具有宏任务,有的话执行,没有的话事件轮询结束。
执行过程中所产生的微任务放到微任务队列中。
完成单个宏任务之后,执行微任务队列中的任务。
常见面试题
异步编程的实现方式有哪些
- 回调函数:使用回调函数的方式的缺点是,是个回调函数嵌套的时候,会照成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码维护。
- promise:使用promise的方式可以嵌套的函数调用做为链式调用,但是使用这种方法,有时会找出多个then的链式调用,可能造成代码的语义不够明确。
- generator:配合yield实现,可以在函数内实现中断。
- async/await:async函数是generator和promise实现的一个自动执行的语法糖,他内部自带执行器,当函数内部执行到await语句的时候,如果语句返回promise对象,那么函数将会等待promise对象的状态变为resolve后再基于向下执行。因此可以将异步逻辑转化为同步的顺序来写,并且这个函数可以自动执行。
如何改变promise的状态
- 调用resolve(value),pending ->fulfilled
- 调用reject(reason),pending ->rejected
- 抛出异常:如果当前时pending就会变成rejected
改变promise状态和指定回调函数谁先谁后
都有可能,正常情况下是先指定回调再改变状态,但也可以先改变状态再指定回调。
什么时候能得到数据?
- 先指定回调,当状态发生改变时,回调函数就会调用,得到数据。
- 先改变状态,当指定回调函数时,回调函数就会调用,得到数据。
promise中改变状态和指定回调函数时做了什么事情
在promise内部定义了一个回调队列。
在状态改变时,也就是调用onresolved或者onrejected函数时,会执行回调队列中的函数。
当指定回调函数时,首先会判断当前的状态是否是pending,如果是则将回调函数加入回调队列,否则直接执行根据状态执行onresolved或者onrejected函数。
这就是为什么无论是先改变状态还是先指定回调函数,都能得到最终的结果的原因。
promise.then() 返回的新promise的结果由什么决定
如果抛出异常,新的promise变为reject,reason为抛出的异常
如果返回的时非promise的任意值,新promise变为fulfilled,value为返回的值
如果返回promise的时另一个promise对象,此peomise的结果就会成为新promise的结果。
promise异常穿透
- 当使用promise的then链式调用时,可以在最后指定失败的回调
- 前面任何操作处理异常都会传到最后的回调中处理
中断promise链
当使用promise的then链式调用,在中间中断,不在调用后面的回调函数。
办法:在回调函数中返回一个pending状态的promise对象
async/await对比promise的优势
- 代码读起来更像同步,Promise虽然摆脱了回调地狱,但是then方法的链式调用也会带来额外的阅读负担
- Promise传递中间值非常麻烦,而async、await几乎是同步的写法
Promist.catch后面的.then还会执行吗
.then回执行,因为catch方法返回的还是一个promise对象,依然支持链式调用。
Promise中,resolve后面的语句是否还会执行?
会被执行。如果不需要执行,需要在 resolve 语句前加上 return。
JS为什么是单线程语言
JavaScript的诞生就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等), 设计成单线程的原因就是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果(两个线程修改了同一个DOM节点就会产生不必要的麻烦),这对于一种网页脚本语言来说这就太复杂了。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
JS是怎么实现异步异常运行的
JavaScript是单线程的,但它所运行的宿主环境—浏览器是多线程,浏览器提供了各种线程供Event Loop调度来协调JS单线程运行时不会阻塞
谈一谈setInterval的问题
setTimeout的作用是每隔一段指定事件执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。
所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。
正对这种现象,我们可以使用setTimeout递归调用来模拟实现setInterval,这样就能确保了只有一个事件结束,我们才会触发下一个定时器事件。
function mySetInterval(fn,delay){ var timer = { flag = true; }; function interval(){ if(timer.flag){ fn(); setTimeout(interval,delay); } } setTimeout(intetval,delay); return timer; } function myClearInterval(timer){ timer.flag = false; }
以上就是深入探究JS中的异步编程和事件循环机制的详细内容,更多关于JS异步编程和事件循环机制的资料请关注脚本之家其它相关文章!
相关文章
JavaScript与Image加载事件(onload)、加载状态(complete)
以前写过一个图片等比缩放的Js函数,缺陷是要等到所有图片都加载完毕了,才能进行等比缩放。2011-02-02微信小程序:报错(in promise) MiniProgramError
这篇文章主要介绍了微信小程序:报错(in promise) MiniProgramError,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2020-10-10javaScript canvas实现(画笔大小 颜色 橡皮的实例)
下面小编就为大家分享一篇javaScript canvas实现(画笔大小 颜色 橡皮的实例),具有很好的参考价值,希望对大家有所帮助2017-11-11Express无法通过req.body获取请求传递的数据解决方法
这篇文章主要为大家介绍了Express无法通过req.body获取请求传递的数据解决方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-12-12
最新评论