JS异步请求汇总

2,492 阅读6分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

1, 异步请求简介

js是一门为浏览器而诞生的语言,发展到现在,js已经不仅仅只在浏览器上运行了,服务端也可以运行js,像node。而js最初设计是单线程,也就是说会一行一行的执行,下面需要等待上面代码执行完毕,也就是说在特定的时刻只能做特定的事情,阻塞其他代码的执行

了解javascript为什么会出现异步,那么我们该怎么去解决异步呢?

2, 解决方案一:Ajax

既然说JavaScript是单线程运行的,那么XMLHttpRequest在连接后是否真的异步?其实请求确实是异步的,不过这请求是由浏览器新开一个线程请求(参见上图),当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到JavaScript引擎的处理队列中等待处理,当任务被处理时,JavaScript引擎始终是单线程运行回调函数,具体点即还是单线程运行onreadystatechange所设置的函数。

  Tip:理解JavaScript引擎运作非常重要,特别是在大量异步事件(连续)发生时,可以提升程序代码的效率。 普通的Ajax请求,用XHR发送一个json请求一般是这样的:

var xhr =  new XMLHttpRequest();

 xhr.open("GET", url);

 xhr.responseType =  'json';

 xhr.onload =  function(){

   console.log(xhr.response);

};

 xhr.onerror =  function(){

console.log("error")

}

xhr.send();

理论上,如果ajax异步请求,它的异步回调函数是在单独一个线程中,那么回调函数必然不被其他线程”阻挠“而顺利执行,也就是1秒后,它回调执行弹出‘ajax’,可是实际情况并非如此,回调函数无法执行,因为浏览器再次因为死循环假死。

  据上面两个例子,总结如下:

  1. JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.
  2. JavaScript引擎用单线程运行也是有意义的,单线程不必理会线程同步这些复杂的问题,问题得到简化。

3, 解决方案二:fetch

使用fetch实现的方式:

  fetch(url).then(function(response){ 
     return response.json(); 
 }).then(function(data){ 
     console.log(data) 
}).catch(function(e){ 
     console.log("error") 
}) 

fetch的主要优点是

  • 语法简洁,更加语义化
  • 基于标准的Promise实现,支持async/await
  • 同构方便

但是也有它的不足

  • fetch请求默认是不带cookie的,需要设置fetch(url, {credentials: 'include'})
  • 服务器返回400,500这样的错误码时不会reject,只有网络错误这些导致请求不能完成时,fetch才会被reject.

4, 解决方案三:Promise

我们在用Ajax写异步的时候,很容易掉入回调地狱(callback),代码的可读性会大大的下降。Promise可以让代码变得更优雅。

封装基于Promise的XHR

function ajax ( method,url ) { // 返回一个Promise对象
      return new Promise(function (resolve) { 
      var xmlhttp = new XMLHttpRequest() // 创建异步请求 // 异步请求状态发生改变时会执行这个函数 
      xmlhttp.onreadystatechange = function () { // status == 200 用来判断当前HTTP请求完成 
       if ( xmlhttp.readyState == 4 && xmlhttp.status == 200 ) {  
             resolve(JSON.parse(xmlhttp.responseText)) 
    // 标记已完成 
   } } 
      xmlhttp.open(method,url) // 使用GET方法获取 
      xmlhttp.send() // 发送异步请求 
      }) 
    }

5, 解决方案四:async/await

用了await后,写异步代码感觉像同步代码一样爽。await后面可以跟Promise对象,表示等待Promise resolve()才会继续下去执行,如果Promise被reject()或抛出异常则会被外面的try...catch捕获。

ES7的asnyc/await号称是异步的终极解决方案,让我们以同步的方式来书写异步代码,这样看起来更简洁,逻辑更清晰。

try{ 
 let response = await fetch(url); 
 let data = await response.json(); 
 console.log(data); 
} catch(e){ 
 console.log("error") 
}

6, 浏览器的三个线程

浏览器是多线程的,它们在内核制控下相互配合以保持同步。一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程(UI线程)。

1.  javascript引擎是基于事件驱动单线程执行的。JS引擎一直等待着event loop中任务的到来,然后加以处理(只
有当前函数执行栈执行完毕,才会去任务队列中取任务执行)。浏览器无论什么时候都只有一个JS线程在运行JS程序。

2.  UI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会
执行。但是 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,JS对页面的操作即GUI的更新也会被
保存在一个队列中,等到JS引擎空闲时才有机会被执行。这就是JS阻塞页面加载。

3.  事件触发线程,当一个事件被触发时该线程会把事件添加到任务队列的队尾,等待JS引擎的处理。这些事件可以来
自JavaScript引擎当前执行的代码块调用setTimeout/ajax添加一个任务,也可以来自浏览器其他线程如鼠标点击添加
的任务。但由于JS的单线程关系,所有这些事件都得排队等待JS引擎处理。

javascript要等主线程空了才会去查看子线程有没有回调内容。异步的任务执行的顺序是不固定的,主要看返回的速度。

7, 两个异步API,setTimeout和 setInterval

setTimeout(function(){
    /* Some long block of code ... */
    setTimout(arguments.callee,10);
},10);

setInterval(function(){
    /* Some long block of code ... */
},10);

这两个程序段第一眼看上去是一样的,但并不是这样。setTimeout代码至少每隔10ms以上才执行一次;然而setInterval固定每隔10ms将尝试执行,不管它的回调函数的执行状态。

我们来总结下:

-   JavaScript引擎只有一个线程,强制异步事件排队等待执行。
-   setTimeout和setInterval在异步执行时,有着根本性不同。
-   如果一个计时器被阻塞执行,它将会延迟,直到下一个可执行点(这可能比期望的时间更长)
-   setInterval的回调可能被不停的执行,中间没间隔(如果回调执行的时间超过预定等待的值)

针对setInterval说法如下:

当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。还要注意两问题:

  1. 某些间隔会被跳过;
  2. 多个定时器的代码执行之间的间隔可能会比预期小。此时可采取 setTimeout和setsetInterval的区别 的例子方法。

8, 总结

所有的异步请求,都利用了浏览器定时器的工作原理