前言
众所周知,JavaScript是“单线程”(single thread)语言,它跟异步(synchronous)应该是相互矛盾才对?不错,一种语言,要么是单线程,要么是多线程,JS在创造之初被确定为单线程,这个是无法更改的,但是它的宿主环境比如浏览器(Chrome、Firefox、IE)、Node是多线程,我们谈到的异步实际上也是宿主环境通过某种方式(比如Event Loop)调用js,所以使其看起来具有了异步的属性。
同步和异步
下面是当有ABC三个任务需要执行完成时,同步或异步执行的流程图:
同步
thread ->|----A-----||-----B-----------||-------C------|
异步
A-Start ---------------------------------------- A-End
| B-Start ----------------------------------------|--- B-End
| | C-Start -------------------- C-End | |
V V V V V V
thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--|
同步(synchronous)和异步asynchronous)的差别就在于这条流水线上各个流程的执行顺序不同。
同步任何是串行的执行各个任务,一个任何执行完成时才能执行下一个任务,如果有个很耗时的操作,比如I/O,其他任任务需要等到I/O操作执行完成才能执行接下来的任务并且在这期间不能干任何事情;异步可以“并行”的执行任务,不必等到某个任务执行完成,可以暂缓当前任务,等到某个指令后可以继续进行当前任务;
划重点:JavaScript在设计之初是为浏览器设计的GUI编程语言,GUI编程的特性之一就是保证UI线程一定不能阻塞,否则体验不佳甚至出现白板。
谈那我们日常中使用的异步是怎么回事,下面我们看下浏览器当中是如何做的
浏览器异步
这当中的核心是Event Loop(事件循环)和Task Queue(任务队列)
任务队列:
任务队列实际上就是事件队列,一个事件进入队列方式大致有三种:
1. 用户在页面进行的一些交互行为,比如鼠标点击、页面滚动等等,只要指定回调函数;
2. I/O设备进行一项任务时,此时也会把任务放进队列当中,比如FileReader、XMLHttpRequest等等
3.我们通过通过setTimeout、setInterval、
requestAnimationFrame
任务队列是一个先进先出的数据结构,排在前面的事件会优先被主线程来读取,只要当前的执行栈为空,任务队列的任务就会进入到执行栈中,被主线程来调度运行,这里面有一类特殊任务-定时器任务,它需要主线程去根据当前的执行时间去判断是否到了或者超过(由于执行栈执行结束后才会去查看,所以如果执行栈消耗时间比较长时,实际上执行会超过给定的事件才回去执行)指定时间,才会去执行该任务。
备注:需要注意alert和同步的XMLHttpRequest,这些会阻塞主线程。
事件循环:
由于主线程从人物队列中读取事件的过程是循环不断的,所以整个的运行机制又叫做Event Loop。
web端异步常用方式
1. XMLHttpRequest(LV1和LV2)
XMLHttpRequest是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。
XMLHttpRequest
在 AJAX 中被大量使用。尽管名字里有 XML,但 XMLHttpRequest
可以取回所有类型的数据资源,并不局限于 XML。而且除了 HTTP ,它还支持 file
和 ftp
协议。在LV2,支持了设置一些新的特性,比如:支持HTTP时限设置防止请求链接时间过长、文件上传、FormData、上传文件、CORS、对二进制数据的获取以及对进度的支持。
简单的用法实例:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'example.php');
xhr.send();
xhr.onreadystatechange = function(){
if ( xhr.readyState == 4 && xhr.status == 200 ) {
alert('Success');
} else {
alert('Other');
}
};
// 进度信息
// 下载进度
xhr.onprogress = progressHandler;
// 上传进度
xhr.upload.onprogress = progressHandler;
2. setTimeout、setInterval、requestAnimationFrame
setTimeOut/clearTimeOut、setInterval/clearInterval和requestAnimationFrame/cancelAnimationFrame都属于JavaScriptTimers函数,前两个大家都比较熟悉,基本上可以满足大家大部分场景,但是我们前面提到了在调用栈完成后才会对该方法进行调用,有些时序性比较高的场景有可能会无法满足需要,因此可能会需要requestAnimationFrame,该方法告诉浏览器在下一次屏幕重绘之前调用参数中的callback函数。
简单的用法实例:
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
3. jQuery.ajax、Fetch、Promise、Deferred
jQuery.ajax是对XMLHttpRequest的封装实现方式,Fetch对XMLHttpRequest的一种更好的替代方式,这里着重提一下两者的差异性:
- 当接收到一个代表错误的 HTTP 状态码时,从
fetch()
返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的ok
属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。 - 默认情况下,
fetch
不会从服务端发送或接收任何 cookies, 如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项)。
Fetch的简单的用法示例:
fetch('http://example.com/movies.json')
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(myJson);
});
Promise
对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象
Promise
有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
贴张图说明一下:

Deferred通过对调用自身的resolve或reject来变更自身的状态为执行状态或者是拒绝状态,并触发then方法的onFulfiled活onRejected方法。
给一段Deferred的简单实现:
function Deferred() {
/* 状态:默认 等待态 pending */
this.state = 'pending';
this.promise = new Promise()
}
Deferred.prototype.resolve = function (obj) {
this.state = 'fulfilled'
var handler = this.promise.handler
if (handler && handler.resolve) {
handler.resolve(obj)
}
}
Deferred.prototype.reject = function (obj) {
this.state = 'rejected'
var handler = this.promise.handler
if (handler && handler.reject) {
handler.reject(obj)
}
}
4. Generator、async/await
Generator函数是协程(coroutine,多个线程互相协作,完成异步任务)在ES6的实现,它最大的特点是可以将函数的控制权交到外部,由外部逻辑来进行函数内线程的暂停和执行;
async/await是ES7标准引入的,它实际上是Generator函数的语法糖,也是在日常项目中比较推崇的写法,它主要是将 Generator 函数的星号(
*
)替换成async
,将yield
替换成await
,仅此而已。另外,需要注意的是Generator返回的是Iterator,async函数返回的Promise对象。
一段简单的代码示例:
const getData = function () {
return new Promise(function (resolve, reject) {
// TODO something
});
};
const genFun = function* () {
const f1 = yield readFile('url1');
const f2 = yield readFile('url2');
console.log(f1.toString());
console.log(f2.toString());
};
// 或者是
genFun().next()
genFun().next()
const asyncFun = async function () {
const f1 = await readFile('url1');
const f2 = await readFile('url2');
console.log(f1.toString());
console.log(f2.toString());
};
一些参考资料:
https://www.cnblogs.com/etoah/p/5863475.html
https://www.cnblogs.com/woodyblog/p/6061671.html
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
http://es6.ruanyifeng.com/#docs/generator-async