JS的异步机制由事件循环和任务队列构成.JS本身是单线程语言,所谓异步依赖于浏览器或者操作系统等完成. JavaScript 主线程拥有一个执行栈以及一个任务队列,主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。
JavaScript是单线程执行的,无法同时执行多段代码。当某一段代码正在执行的时候,所有后续的任务都必须等待,形成一个队列。一旦当前任务执行完毕,再从队列中取出下一个任务,这也常被称为 “阻塞式执行”。所以一次鼠标点击,或是计时器到达时间点,或是Ajax请求完成触发了回调函数,这些事件处理程序或回调函数都不会立即运行,而是立即排队,一旦线程有空闲就执行。假如当前 JavaScript线程正在执行一段很耗时的代码,此时发生了一次鼠标点击,那么事件处理程序就被阻塞,用户也无法立即看到反馈,事件处理程序会被放入任务队列,直到前面的代码结束以后才会开始执行。如果代码中设定了一个 setTimeout,那么浏览器便会在合适的时间,将代码插入任务队列,如果这个时间设为 0,就代表立即插入队列,但不是立即执行,仍然要等待前面代码执行完毕。所以 setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。
简单异步问题
在JavaScript的世界中,所有代码都是单线程执行的,由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现
window.setTimeout(function(){
console.log(123)
}, 1000)
同样异步问题会出现在 Ajax 请求当中
$.ajax({
url: '',
success: function(res){
generateTable(res.data);
}
})
function generateTable(data){
console.log(data);
}
复杂异步问题
在很多开发场景当中会有比较多的请求需要按顺序串行执行,比如有 var apis = ['api1', 'api2', 'api3']
,需求是需要按顺序执行,先完成请求一再发起请求二然后再是请求三,这个时候用 Ajax 请求就只能是回调嵌套回调
$.ajax({
url: 'api1',
success: function(res){
$.ajax({
url: 'api2',
success: function(res){
$.ajax({
url: 'api3',
success: function(res){
generateTable(res.data);
}
})
}
})
}
})
function generateTable(data){
console.log(data);
}
以上的解决方案可行,但如何 api 有 n 个,那这种无多层嵌套的方式我们称之为回调地狱
Promise
为了解决上述回调地狱问题,便推出了 Promise 并在 ES6 中被统一规范,由浏览器直接支持。
Promise 简单使用
Promise 是一个构造函数,接收一个 function 为参数,该回调函数有会被传两个形参 resolve 和 reject,resolve 用于执行正常结果,并用 then 来接收
var p1 = new Promise(function(resolve, reject){
window.setTimeout(function(){
resolve(123)
}, 1000);
})
p1.then(function(res){
console.log(res)
})
用 reject 用来执行异常结果。用 catch 来获取异常信息
var p1 = new Promise(function(resolve, reject){
window.setTimeout(function(){
reject('error')
}, 1000);
})
p1.then(function(res){
console.log(res)
}).catch(function(error){
console.log(error)
})
状态不可逆性、链式调用(串行调用)
如上述如何有多个异步需要串行执行,用 Promise 实现会简单很多
var p = new Promise(function(resolve, reject){
window.setTimeout(function(){
resolve(1);
}, 1000)
});
p.then(function(value){ //第一个then
console.log(value);
return value*2;
}).then(function(value){ //第二个then
console.log(value);
}).then(function(value){ //第三个then
console.log(value);
return Promise.resolve('resolve');
}).then(function(value){ //第四个then
console.log(value);
return Promise.reject('reject');
}).then(function(value){ //第五个then
console.log('resolve: '+ value);
}, function(err){
console.log('reject: ' + err);
})
小结
- 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态
- pending: 初始状态,不是成功或失败状态
- fulfilled: 意味着操作成功完成
- rejected: 意味着操作失败
- Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了。
new Promise(function(resolve, reject){
window.setTimeout(function(){
reject('error')
}, 2000)
window.setTimeout(function(){
resolve('ok')
}, 3000)
}).then(function(res){
console.log(res)
}).catch(function(error){
console.log(error)
})
-
优点:有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
-
缺点:
- 首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消.
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
- 当处于 Pending 状态时,无法得知目前进展到哪一个阶段
-
规范
- Pending – Promise对象的初始状态,等到任务的完成或者被拒绝;Resolved – 任务执行完成并且成功的状态;Rejected – 任务执行完成并且失败的状态;
- Promise的状态只可能从Pending状态转到Resolved状态或者Rejected状态,而且不能逆向转换,同时Resolved状态和Rejected状态也不能相互转换;
- Promise对象必须实现then方法,then是promise规范的核心,而且then方法也必须返回一个Promise对象,同一个Promise对象可以注册多个then方法,并且回调的执行顺序跟它们的注册顺序一致;
- then方法接受两个回调函数,它们分别为:成功时的回调和失败时的回调;并且它们分别在:Promise由Pending状态转换到Resolved状态时被调用和在Promise由Pending状态转换到Rejected状态时被调用。
Promise.all
有时候需要使用并行执行时可以用 Promise.all,then 返回的时间以最后一个异步执行的完成时间为准。
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 2000);
});
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});