这是我参与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’,可是实际情况并非如此,回调函数无法执行,因为浏览器再次因为死循环假死。
据上面两个例子,总结如下:
- JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.
- 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()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。还要注意两问题:
- 某些间隔会被跳过;
- 多个定时器的代码执行之间的间隔可能会比预期小。此时可采取 setTimeout和setsetInterval的区别 的例子方法。
8, 总结
所有的异步请求,都利用了浏览器定时器的工作原理