一文了解你不知道的JavaScript异步篇

事件循环

先通过一段伪代码了解一下事件循环这个概念

//eventLoop是一个用作队列的数组
var eventLoop = []
var event;
//永远执行
while(true){
   if(eventLoop.length>0){}
    //拿到队列中的下一个事件
    event = eventLoop.shift();
    //现在,执行下一个事件
    try{
      event()
    }
    catch(err){
     reportError(err)
    }
}

这是一段继续简化的代码,你可以看到有一个用while循环实现的持续运行的循环,循环的每一轮称为一个tick。对每个tick而言,如果在队列中有等待事件,就会从队列中摘下一个事件并执行,这些事件就是所谓的回调函数

一定要清楚,setTimeout()并没有把你的回调函数挂在事件循环队列中。他所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的tick会摘下并执行这个回调。所以这也是为什么setTimeout时间精度可能不太高,它只能确保你的回调函数不会在指定的时间间隔之前运行,但可能会在事件循环队列中20个项目后才执行。取决于你事件队列的项目与状态,毕竟JavaScript一次只能处理一个事件。这也引出了另一个概念“并发”。

当两个或多个“进程”同时执行就出现了并发,也许浏览器会发出很多请求,当发出第二个请求时,第一个请求返回响应,当发出第三个请求时,第二个请求返回响应。这里请求2与响应1并发运行,请求3与响应2并发运行,但是他们的各个事件是在事件循环队列中依次运行的。

更常见一点的情况是,并发的“进程”需要相互交流,如果出现这样的交互,就需要对他们的交互进行协作以避免竞态的出现。下面是两个并发的进程通过隐含的顺序相互影响,这个顺序有时会被破坏:

var res = [];
function response(data){
    res.push(data)
}
ajax("http://url1",response);
ajax("http://url2",response);

这里的两个ajax去调用response函数,但不确定哪一会先执行完成,这种不确定性很有可能就是一个竞态条件bug。

在es6中,有一个新的概念建立在事件循环队列之上,解决这种不确定性的执行,叫做任务队列

任务队列

对任务队列最好的理解方式就是,它是挂在事件循环队列的每个tick之后的一个队列。在事件循环的每个tick中。可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前tick的任务队列末尾添加一个任务。

任务队列意思是:哦?原来这里还有一件事情要做,但要在任何事情发生之前就完成它,立刻接着执行它。

而事件循环类似于做完这件事情,需要重新到队尾排队才能再做这件事情。

console.log("A")
setTimeout(()=>{
    console.log("B")
},0)
task(()=>{
console.log("C")
    task(()=>{
      console.log('D')
    })
})

可能你认为这里会打印出ABCD,但其实打印结果是ACDB,因为定时器触发是在所有同步事件队列清空之后才开始执行的。

回调

到目前位置,回调是编写和处理JavaScript程序异步逻辑的最常用的方式,也是最基础的异步模式。

我们的大脑可以看作类似于单线程运行的事件循环队列,就像JavaScript引擎那样。用正在写博客的我写作进行类比。此刻我心里就是计划写啊写一直写,一次完成我脑海中已经按顺序排好的一系列要点。我没有将任何终端或非线性的行为纳入到我的写作计划中。然而尽管如此,实际上我的大脑还是在不停的切换状态。即使我的大脑在以异步事件方式运行,但我的写作还是以顺序、同步的进行,“先写这里,再写那里”。

所以,如果说同步的大脑计划能够很好地映射到同步代码语句,那么我们大脑在规划异步方面又是怎样的呢?

答案是回调。即使在脑海中有许多事件出现,如果真的想到什么就做什么去那恐怕我这篇博客也无法完成。但在实际执行方面,我的大脑就是这样运作了。不是多任务,而是快速的上下文切换。

嵌套回调

listen("click",()=>{
    setTimeout(()=>{
     setTimeout(()=>{
       ajax("http:url",()=>{
          console.log("响应结果")
       })
    },1000)
},1000)
})

你可能非常熟悉这样的代码,好几个函数嵌套在一起构成的链,这种代码常常被称为回调地狱问题,在大型项目中他引起的问题要比这些严重得多。为了避免回调地狱问题,产生了伟大的promise。

promise

在promise中,传入的函数会立刻执行,它有两个参数,在本例中我们将其分别称为resolve和reject。前者代表完成,后者代表拒绝

new Promise((resolve,reject)=>{
   //最终调用resolve还是reject
}).then(
  function(){
   console.log("then")
  }
)

promise调度技巧

如果两个promise都已经决议,那么p1.then和p2.then应该最终会先调用p1的回调,然后是p2的哪些,但还有一些可能微妙的场景:

p.then(function(){
  p.then(function(){
    console.log("C")
  })
  console.log("A")
})
p.then(function(){
   console.log("B")
})

这里的输出结果是 A B C

一个promise决议后,这个promise上所有的通过then注册的回调都会在下一个异步时机点上一次调用。所以在这里'C'无法抢占或打断‘B’,因为这是promise的运作方式。

错误处理

对于大多数开发者来说,最自然的错误处理就是try...catch结构,遗憾的是它只能是同步的,无法用于异步代码模式。

function(){
  setTimeout(()=>{
    bar()
  },1000)
}
try{
  foo()
}catch(err){
   //永远不会到达这里
}

try...catch当然很好,但是无法跨越异步操作工作,所以catch无法拦截定时器内异步的错误。

方法1. 可以在then(resolve,reject)中第二个回调内处理错误,可以throw传递一个error。

方法2. finally捕获

不管promise最后的状态,在执行完then或catch指定的回调函数之后,都会执行finally方法指定的回调函数。

function fn(val){
    return new Promise((resolve,reject)=>{
        if(val){
          resolve({name:"111"})
        }else{
          reject("404")
        }
    })
}
//执行函数
fn(true)
   .then(data=>{
     console.log(data) //打印name:111键值对
     return fn(false)
   })
   .catch(e=>{
     console.log(e) //打印404
     return fn(false)
   })
   .finally(()=>{
     console.log("finally") //会打印的!
   })

promise.all([...])

假如你想同时发送两个请求,等他们不管以什么顺序完成之后再发送第三个请求

Promise.all([p1,p2])
.then(function(){
    return request("http:url3")
})
.then(function(msg){
  console.log(msg)
})

Promise.all需要一个参数,是一个数组,通常由promise实例组成,从promise.all调用返回的promise会收到一个完整消息(msg)。这是一个由数组完成后传入的消息,与顺序无关。

另外,当数组内有且仅有所有成员promise都完成后才算完成。如果这些promise有任何一个被拒绝那all就会立刻被拒绝,并丢弃已经成功的来自其他数组成员的promise结果。

promise.race([...])

尽管promise.all协调多个并发promise的运行,并假定所有都需要完成,但有时候你会只想响应第一个完成promise的结果,并直接抛弃其他promise。

这种在promise中被称之为竞态

promise.race()也接受一个数组做参数。这个数组有一个/多个promise组成。一旦有任何一个promise为成功resolve,promise.race()就会完成;一旦有任何一个为reject被拒绝,它就会拒绝。

(如果你传入一个空数组,那race永远不会resolve,永远不要传递空数组)

Promise.race([p1,p2])
.then(function(){
//p1和p2其中之一会完成这场竞赛,突破重围
    return request("http:url3")
})
.then(function(msg){
  console.log(msg)
})

因为只有一个promise能够取胜,所以完成值是单个消息,而不是像all一样是一个数组。

all和race的变体

· none([...])

这个模式类似于all([...]),不过完成和拒绝的情况互换了,而是所有的promise都要被拒绝,即拒绝转化为完成值。

· any([...])

这个模式与all([...])类似,但是会忽略拒绝,所以只需要完成一个而不是全部。

· first([...])

这个模式类似于与any([...])的竞争,即只要第一个promise完成,就会忽略后续的任何完成和拒绝。

· last([...])

这个模式类似于first([...]),但却是只有最后一个完成胜出。

无法取消的promise

一旦创建了一个promise并为其注册了完成/拒绝的处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部停止它的进程。

以上就是一文了解你不知道的JavaScript异步篇的详细内容,更多关于JavaScript异步的资料请关注其它相关文章!

原文地址:https://juejin.cn/post/7163591357234151461