JS 异步编程

606 阅读9分钟

同步模式与异步模式

JavaScript将任务执行的顺序分成了两种,同步模式和异步模式

同步模式

同步模式指代码中的任务依次执行,后一个任务等待前一个任务结束后才开始执行,程序的执行顺序与代码的编写顺序是完全相同的,在单线程的情况下,大多数任务都会以同步模式执行。

异步模式

异步模式不同于同步模式,异步模式的API不会等待这一个任务结束才开始下一个任务,对于耗时操作,他都是开启过后就立即执行下一个任务,耗时操作的后续逻辑一般通过回调函数的方式来定义,在内部耗时任务完成过后,就会自动传入我们的回调函数。 异步模式对JavaScript十分重要,如果没有异步模式,单线程的JavaScript就无法同时处理大量耗时任务。但异步模式代码执行顺序不会像同步那样通俗易懂,要充分分析理解。

js异步模式执行过程

看图分析一下啦,假如js线程某一个时刻发起了异步调用,它会紧接着执行其他的任务,此时异步线程会单独的去执行异步任务,在执行完这个任务后,会将这个任务的回调放到消息队列,js主线程在完成了所有的任务之后,会在依次执行我们消息队列中的任务。

我们需要特别注意,JavaScript确实是单线程的,而浏览器并非单线程的,更具体一点来说,我们通过JavaScript调用的某些内部的API并不是单线程的,例如常使用的倒计时器,它内部就会有一个线程去倒数,在时间到了之后就会将我们的回调放入消息队列,这样的事件有一个单独的线程去操作,而我们所说的单线程是执行我们代码的是一个线程,也就是说我们的API会有一个单独的线程去执行这些等待的操作

2023-01-09 (10).png

异步编程的方式

回调函数

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函数

  1. 如果ajax请求一个根本不存在的地址,会调用onRejected失败函数
  2. 在ajax函数里调用一个根本不存在的方法,最终也会调用onRejected失败函数
  3. 在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');