同步模式与异步模式
JavaScript将任务执行的顺序分成了两种,同步模式和异步模式
同步模式
同步模式指代码中的任务依次执行,后一个任务等待前一个任务结束后才开始执行,程序的执行顺序与代码的编写顺序是完全相同的,在单线程的情况下,大多数任务都会以同步模式执行。
异步模式
异步模式不同于同步模式,异步模式的API不会等待这一个任务结束才开始下一个任务,对于耗时操作,他都是开启过后就立即执行下一个任务,耗时操作的后续逻辑一般通过回调函数的方式来定义,在内部耗时任务完成过后,就会自动传入我们的回调函数。 异步模式对JavaScript十分重要,如果没有异步模式,单线程的JavaScript就无法同时处理大量耗时任务。但异步模式代码执行顺序不会像同步那样通俗易懂,要充分分析理解。
js异步模式执行过程:
看图分析一下啦,假如js线程某一个时刻发起了异步调用,它会紧接着执行其他的任务,此时异步线程会单独的去执行异步任务,在执行完这个任务后,会将这个任务的回调放到消息队列,js主线程在完成了所有的任务之后,会在依次执行我们消息队列中的任务。
我们需要特别注意,JavaScript确实是单线程的,而浏览器并非单线程的,更具体一点来说,我们通过JavaScript调用的某些内部的API并不是单线程的,例如常使用的倒计时器,它内部就会有一个线程去倒数,在时间到了之后就会将我们的回调放入消息队列,这样的事件有一个单独的线程去操作,而我们所说的单线程是执行我们代码的是一个线程,也就是说我们的API会有一个单独的线程去执行这些等待的操作
异步编程的方式
回调函数
JavaScript实现异步编程的根本方式--回调函数
promise
promise概述
一种更优的异步编程统一方案--promise
如果直接使用传统的回调方式去完成复杂的异步流程,无法避免大量的回调函数的嵌套,就会形成回调地狱,promise便可以提供更加扁平的异步编程体验s。
promise实际就是一个对象,用来表示异步任务最终是成功(fulfilled)还是失败(rejected),当成功了会执行onFulilled任务,失败了执行onRejected任务
promise基本用法
首先我们构造一个promise实例,这里我们需要一个函数作为参数,在这里函数内部可以接受到两个参数,分别是resolve和rejected,resolve和rejected都是函数,resolve将我们promise对象的状态修改为成功,rejected将我们promise对象状态修改为失败,一般异步任务的结果我们通过resolve和rejected传递出去。promise状态确定便不可再更改,resolve和rejected只可调用二者其一。
当promise实例创建完成之后,我们便可以调用它的then方法分别指定onFulilled和onRejected的回调函数。then方法传入的第一个参数是便是onFulilled成功后的回调函数,第二个便是onRejected失败后的回调函数。正在根据上面promise对像的状态调用相应成功与失败的回调函数。
// Promise基本实例
const promise=new Promise(function(resolve,reject){
// 这里用于兑现承诺
// resolve(100)//承诺达成
reject(new Error('promise rejected')) //承诺失败
})
promise.then(function(value){
console.log('resolved',value);
},function(error){
console.log('rejected',error);
})
console.log('end');
promise链式调用
当ajax发起多个连续的请求时,仍然会形成回调地狱,promise就没有意义了。
// promise方式的ajax
function ajax(url){
return new Promise(function(resolve,reject){
var xhr=new XMLHttpRequest()
xhr.open('GET',url)
xhr.responseType='json'
xhr.onload=function(){
if(this.status===200){
resolve(this.response)
} else{
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
ajax('/api/users.json').then(function(res){
ajax(urls.users).then(function(users){
ajax(urls.users).then(function(users){
ajax(urls.users).then(function(users){
})
})
})
})
所以便有了链式调用,来解决这个问题
// promise方式的ajax
function ajax(url){
return new Promise(function(resolve,reject){
var xhr=new XMLHttpRequest()
xhr.open('GET',url)
xhr.responseType='json'
xhr.onload=function(){
if(this.status===200){
resolve(this.response)
} else{
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
ajax('/api/users.json')
.then(function(value){
console.log(111);
console.log(value);
return ajax('/api/urls.json')
})//=>promise
.then(function(value){
console.log(222);
console.log(value);
return ajax('/api/urls.json')
})//=>promise
.then(function(value){
console.log(333);
console.log(value);
return ajax('/api/urls.json')
})//=>promise
.then(function(value){
console.log(444);
console.log(value);
})//=>promise
注意注意重点重点:
- Promise对象的then方法会返回一个全新的Promise对象
- 后面的then方法就是在为上一个then返回的Promise注册回调
- 前面then方法中回调函数的返回值会作为后面then方法回调的参数
- 如果回调中返回的是Promise对象,那么后面then方法的回调会等待它的结束
promise异常处理
promise结果一旦失败就会调用onRejected函数,下面几种情况便会导致结果失败调用onRejected函数
- 如果ajax请求一个根本不存在的地址,会调用onRejected失败函数
- 在ajax函数里调用一个根本不存在的方法,最终也会调用onRejected失败函数
- 在ajax函数里抛出一个错误最终也会调用onRejected失败函数
关于onreject函数的注册除了then方法其实还有还可以使用promise的catch方法
其实catch方法相当于then方法的一个别名,相当于then方法第一个参数传递了undefined,第二个失败回调函数参数正常
比较两种方式的差异catch方法会更适合链式调用,根据代码分析一下
// Promise 异常处理
function ajax(url){
return new Promise(function(resolve,reject){
// 在这里调用一个根本不存在的方法,最终也会调用onRejected失败函数
// foo()
// 抛出一个错误最终也会调用onRejected失败函数
// throw new Error()
var xhr=new XMLHttpRequest()
xhr.open('GET',url)
xhr.responseType='json'
xhr.onload=function(){
if(this.status===200){
resolve(this.response)
} else{
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
// promise结果一旦失败就会调用onRejected函数
// 如果ajax请求一个根本不存在的地址,也会调用onRejected失败函数
ajax('/api/users.json').then(
function onFulfilled(value){
console.log('onFulfilled',value);
// 返回一个ajax调用,只不过是一个根本就不存在的地址,就是说这个promise一定是失败的,不过他并不会被then方法注册的失败回调给捕获到
return ajax('/error-url')
},function onRejected(error) {
console.log('onRejected',error);
})
ajax('/api/users.json')
.then(
function onFulfilled(value){
console.log('onFulfilled',value);
// 而在这里也返回一个ajax调用,一个不存在的地址,promise一定失败,但他catch方法注册的失败回调可以捕获到第一个then方法指定的promise对象的异常
return ajax('/error-url')
}) //=>promise
.catch(
function onRejected(error) {
console.log('onRejected',error);
}
)
两种方式虽然都能捕获promise执行过程的异常,但是仔细对比,then方法和catch方法还是存在许多差异
每个then方法返回的都是全新的promise对象,通过链式调用的catch实则是在给上一个then方法指定的promise对象去指定失败回调,并不是给第一个promise对象(ajax('/api/users.json'))所指定的。因为这是同一个promise链条,前面promise的异常(如ajax请求一个根本不存在的地址)会一直往后传递,所以我没能够通过catch方法捕获到第一个promise的异常。
如果我们使用then的第二个参数注册的失败回调它是捕获不到的promise链中第二个promise的异常的,他只是给第一个promise注册的失败回调,所以说catch方法更适合链式调用。
promise并行执行
1.Promise.all()方法
使用promise的all方法可以实现将多个promise对象合并为一个,promise的all方法接受的是一个数组,每个元素都是一个promise对象,把这些都看做一个异步任务,all方法会返回一个全新的promise对象,当内部所有的promise都完成之后呢,我们返回的新的promise才会完成,此时新的promise对象拿到的便是一个新的数组,数组里面包含着每一个异步任务执行之后的结果。
注意,在这个任务中,只有all方法里面promise的异步任务都成功结束了,新的promise才会成功结束。其中一个失败结束了,新的promise也会失败结束。这是一个很好同步执行多个promise的方式
function ajax(url){
return new Promise(function(resolve,reject){
var xhr=new XMLHttpRequest()
xhr.open('GET',url)
xhr.responseType='json'
xhr.onload=function(){
if(this.status===200){
resolve(this.response)
} else{
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
// ajax('/api/posts.json')
// ajax('/api/users.json')
// 数组的形式传递的
var promise=Promise.all([
ajax('/api/posts.json'),
ajax('/api/users.json')
])
promise.then(function(values){
console.log(values);
}).catch(function(err){
console.log(err);
})
2.Promise.race()
利用promise.race实行ajax请求超时控制,代码如下:
const request=ajax('/api/posts.json')
const timeout=new Promise((resolve,reject)=>{
setTimeout(()=>reject(new Error('timeout')),500)
})
Promise.race([
request,
timeout
])
.then(value=>{
console.log(value);
})
.catch(err=>{
console.log(err);
})
- promise.all()与promise.race()相同点
都可以将多个promise对象组合为一个全新的promise对象
- promise.all()与promise.race()不同点
promise.all是等待所有的任务都结束才会结束
promise.race是只会等待第一个任务的结束
promise执行时序
分析如下代码:
有一个传统的异步调用setTimeout,并将延迟时间设为0,我们可能认为因为延迟时间是零,会立即进入回调队列中排队,那它进入回调队列之后,等待下一轮的执行。那我们可能认为是setTimeout先进的队列,然后才是promise。但打印结果实则不是这样的,会先打印promise然后才是setTimeout,这是为什么呢?
其实是因为promise的异步执行时序会有一些特殊,
回调队列中的任务称为宏任务,宏任务执行过程中可以临时加上一些额外需求,对于这些临时额外的需求,可以选择作为一个新的宏任务进到队列中排队,这里的setTimeout就是作为一个新的宏任务到回调队列中排队。而微任务,就是直接在我这个当前任务结束之后立即执行,而不是到整个队伍的末尾再重新排队,这就是宏任务与微任务的差异,而promise则是作为微任务执行的,所以他会在本轮执行调用的末尾去自动执行,所以我们这里打印的先是promise然后再是setTimeout,setTimeout是以宏任务的概念进入回调函数的末尾,微任务是后来被引入js概念的,目的就是提高整体的响应能力。
目前绝大多数异步调用都是作为宏任务执行,而promise对象和MutationObserver对象,以及node中的process.nextTick它们都作为微任务在本轮调用的末尾就执行了。
// 微任务
console.log('global start');
setTimeout(()=>{
console.log('setTimeout');
},0)
Promise.resolve()
.then(()=>{
console.log('promise');
})
.then(()=>{
console.log('promise2');
})
.then(()=>{
console.log('promise3');
})
.then(()=>{
console.log('promise4');
})
console.log('global end');