JavaScript异步编程

450 阅读12分钟

一、 概述

采用单线程工作模式,js执行环境中负责执行代码的线程只有一个
好处:安全、简单
缺点:遇到特别耗时的任务,后面的任务需要等待上面任务完毕才能执行,会产生假死现象。

两种执行模式:同步模式、异步模式

二、 同步模式与异步模式

运行环境提供的API的工作方式是同步还是异步模式!!

1.同步模式

代码中的任务依次执行,后一个任务必须等待前一个任务代码执行结束才能开始执行。程序执行顺序和代码编写顺序一致。(不是同时执行,而是排队执行)

console.log('global begin') //(1)
function bar() {
    console.log('bar Task')
}
function foo() {
    console.log('foo Task')
    bar()
}
foo()(2console.log('global end') //(3)
//global begin
//foo Task
//bar Task
//global end

开始执行js引擎会在调用栈中压入一个匿名的调用(anonymous),可以理解为把所有的代码都放在一个匿名函数中去执行,然后开始逐行执行。步骤和调用栈里的内容
开始 (anonymous)
步骤(1) (anonymous) console.log('global begin')
步骤(1)执行完 (anonymous)
步骤(2-1) (anonymous) foo()
步骤(2-2) (anonymous) foo() console.log('foo Task') 步骤(2-3) (anonymous) foo() bar()
步骤(2-4) (anonymous) foo() bar() console.log('bar Task')
步骤(2-5) (anonymous) foo() bar()
步骤(2-6) (anonymous) foo()
步骤(2)执行完 (anonymous) console.log('global end')
步骤(3) (anonymous)
最后 清空
缺点:会等待前面任务结束才开始下一个任务

2. 异步模式

不会等待前面任务结束才开始下一个任务,开启过后就立即往后执行下一个任务,后续逻辑通过回调函数的方式去定义。
问题:代码执行顺序混乱,不会按照代码顺序执行。

console.log('global begin')//(1)
setTimeout(function timer1() { //(2)
    console.log('timer1 invoke')
}, 1800)
setTimeout(function timer2() { //(3)
    console.log('timer2 invoke')
    setTimeout(function inner() { 
        console.log('inner invoke')
    },1000)
},1000)
console.log('global end')//(4)

开始执行js引擎会在调用栈中压入一个匿名的调用(anonymous)

步骤<Call stack>内容
开始之前(anonymous)
步骤(1)(anonymous) console.log('global begin')
步骤(1)执行完(anonymous)
步骤(2)(anonymous) setTimeout(timer1) 【同时, 内部为timer1函数开启了一个倒计时器,然后单独放在一边(Web APIs),倒计时1.8s
步骤(2)执行完(anonymous)
步骤(3)(anonymous) setTimeout(timer2) 【同时, 内部为timer2函数开启了一个倒计时器,然后单独放在一边(Web APIs),倒计时1s
步骤(3)执行完(anonymous)
步骤(4)(anonymous) console.log('global end')
步骤(4)执行完(anonymous)
最后---(清空)

步骤执行时,Web APIs内容

步骤Web APIs
步骤(2)timer1()【倒计时1.8s】
步骤(3)timer1()【倒计时1.8s】 timer2()【倒计时1s】

当Web APIs中的timer2倒计时结束后会把他放入Queue消息队列中,然后timer1结束也是

时间点Queue
timer2()倒计时结束timer2()
timer1()倒计时结束timer2() timer1()

一旦消息队列(Queue)发生了变化我们的事件循坏就会监听到,就会把消息队列当中的第一个(timer2())压入调用栈(call stack),此时

时间点<Call stack>内容
timer2()倒计时结束timer2()
时间点Queue内容
timer2()倒计时结束timer()

此时就开启了新一轮的执行,又遇到了setTimeout(inner),重复上面执行情况,直到任务栈和消息队列中都没任务了,我们就认为任务执行完毕了。

如果我们把任务栈当作执行任务表,那么队列就是待办任务表。 js是先去处理任务栈中的任务,再通过事件循坏去队列中取一个任务去完成,在此期间我们随时可以给队列中加入新的任务,这些任务在消息队列中会排队等待完成。 js是单线程,浏览器不是!

三、异步编程的几种方式

1. 异步编程方式的根基 -- 回调函数

由调用者定义,交给执行者执行的函数,调用者告诉执行者异步任务结束后应该做什么

function foo(callback) { 
    setTimeout(function () { 
        callback()
    },300)
}
foo(function () { //`将函数作为参数`的方式
    console.log('我是一个回调函数')
})

2.Promise异步方案、宏任务/微任务队列

(1) 概述

一个对象用来表示,一个异步任务结束之后究竟是成功还是失败。

image.png

(2) 基本用法

const promise = new Promise(function (resolve, reject) {
  // resolve(100) //承诺达成//(1)
  reject(new Error('promise rejected'))//(2)
})
promise.then(function (value) {
  console.log('resolved', value) //resolved 100 //开启(1)
}, function (error) {
  console.log('rejected', error) //rejected Error: promise rejected //开启(2)
})
console.log('end') //end rejected Error: promise rejected //开启(2)

先输出"end"证明是异步执行!

(3) 使用案例

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('你的请求地址').then(function (res) {
      console.log(res) //{code: 200, buniss_code: 0,...}
  }, function (error) {
      console.log(error)
  })
  ***错误地址***
  ajax('你的错误地址').then(function (res) {
      console.log(res)
  }, function (error) {
      console.log(error) //Error: Not found
  })

promise本质上也是使用回调函数的方式,去定义异步任务结束后所需要执行的任务。只不过是通过then方法传递的,这个方法需要两个函数,一个成功回调一个失败回调

(4) 常见误区

ajax('你的请求地址').then(function (res) {
      console.log(res)
      ajax('你的请求地址').then(function (res) {
          console.log(res)
                 ...
          }, function (error) {
              console.log(error)
          })
  }, function (error) {
      console.log(error)
  })

如果有多个回调容易产生回调地狱,要尽量保证任务扁平化,所以要使用链式调用!

(5) 链式调用

ajax('xxx').then(function (res) {
      console.log(res) //{code: 200, buniss_code: 0,...}
  }, function (error) {
      console.log(error)
  }).then(function (val) {
      console.log(1) //1
      // return ajax('xxx')//相当于return上面的ajax('xxx'),返回的promise对象添加状态明确过后的回调
  }).then(function (val) {
      console.log(2)//2
      return 'abc'
  }).then(function (val) {
      console.log(3)//3
      console.log(val)//'abc
  })

then返回一个全新的promise对象,每一个then方法实际上都是在为上一个返回的promise对象添加状态明确过后的回调,这些回调会依次执行

  • promise 对象的then方法会返回一个全新的promise 对象
  • 后面的then方法就是在为上一个then返回的promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的是promise,那后面then方法的回调会等待它的结束

(6) 异常处理

onRejected回调在promise失败或者异常时它都会被执行。
onRejected回调的注册我们还有一个更常见的用法,使用promise实例的catch方法去注册

ajax('xxx').then(function (res) {
    console.log(res) //{code: 200, buniss_code: 0,...}
}).catch(function (error) {//catch 方法相当于then的第一个参数传递了undefined
    console.log(error)
})

then第二个参数捕获异常catch捕获异常的区别:

***then第二个参数捕获异常***
ajax('xxx').then(function (res) {
      console.log(res) //{code: 200, buniss_code: 0,...}
      return ajax('error-url')
  }, function (error) {//无法捕获到异常
      console.log(error)
  })

  ***catch捕获异常***
  ajax('xxx').then(function (res) {
      console.log(res) //{code: 200, buniss_code: 0,...}
      return ajax('error-url')
  }).catch(function (error) {//可以捕获到异常
      console.log(error)
  })

then第二个参数捕获异常无法捕捉到正确回调函数里面的异常,catch可以,catch可以捕获整条promise注册链上的异常,推荐使用catch捕获异常!除此之外,我们还可以在全局对上注册一个unhandledrejection事件,去处理那些我们代码中没有去手动捕获的异常。

//浏览器中
window.addEventListener('unhandledrejection', event => { 
  const { reason, promise } = event
  console.log(reason, promise)
  //reason => Promise 失败原因,一般是一个错误对象
  //promise => 出现异常的Promise对象
  event.preventDefault()
}, false)
// node中
process.on('unhandledrejection', event => { 
  console.log(reason, promise)
  //reason => Promise 失败原因,一般是一个错误对象
  //promise => 出现异常的Promise对象
})

全局捕获异常不推荐,我们应该在代码中明确捕获每一个异常!

(7) 静态方法

Promise.resolve() //可以快速的把一个值转为promise对象

Promise.resolve('foo').then(function (value) {
    console.log(value) //foo
})
//相当于
new Promise(function (resolve, reject) {
    resolve('foo')
}).then(val => console.log(val))//foo
var a = new Promise(function (resolve, reject) {
      resolve('foo')
})
var b = Promise.resolve(a)
console.log(a === b)//true
Promise.resolve({
      then: function (onFullfilled, onRejected) {//then方法对象实现了一个thenable的接口,他是一个可以被then的对象
          onFullfilled('foo')
      }
}).then(function (value) {
      console.log(value)//foo 也能拿到对应传入的值
})

Promise.reject() //传入的参数都会作为promise失败的原因

Promise.reject('anything').catch(function (error) { 
  console.log(error)
})

(8) 并行执行--需要并行执行多个任务,多个任务同时执行

  • Promise.all([]) //返回一个全新的promise对象,所有任务都结束才返回
//ajax('xxx')//包含所有url地址的对象

  ajax('xxx').then(value => { 
      const urls = Object.values(value)//Object.values方法获取对象中所以属性值,即URL地址组成的地址
      const tasks = urls.map(url => ajax(url))//字符串数组转换成一个包含全部请求任务的promise对象数组
      return Promise.all(tasks)//promise对象数组组成一个新的promise 返回
  }).then(values => { 
      console.log(values)
  })
  
  • Promise.race()//只会等待第一个结束的任务,可以作为ajax超时请求控制的一种方式
  const request = ajax('xxx')
  const timeout = new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('timeout')), 500)
  })
  Promise.race([request, timeout]).then(val => console.log(val)).catch(err => console.log(err))

(9) 执行顺序(宏任务和微任务

  console.log('global start')
  setTimeout(() => {
      console.log('setTimout')
  }, 0)
  Promise.resolve().then(() => console.log(1)).then(() => console.log(2)).then(() => console.log(3))
  console.log('global end')
  //global start
  //global end
  //1
  //2
  //3
  //setTimout

回调队列中的任务叫宏任务,宏任务执行过程过程中可以临时加上一些额外需求,这些临时加上一些额外需求可以选择作为一个新的宏任务进入队列中排队(如:setTimeout),也可以作为当前任务的微任务,直接在当前这个任务结束之后立即执行(如:promise)

微任务:提高整体响应能力

目前绝大多数异步调用都是作为宏任务执行,而Promise、MutationObserver、process.nextTick(node中的)会作为微任务,直接在本轮调用的末尾就执行了

3.Generator异步方案、Async/Await语法糖

传统异步代码写法

try {
    const val1 = ajax('xx1')
    console.log(val1)
    const val2 = ajax('xx2')
    console.log(val2)
    const val3 = ajax('xx3')
    console.log(val3)
    const val1 = ajax('xx4')
    console.log(val4)
} catch (e) { 
    console.log(e)
}

两种更优的异步编程写法 Generator、Async函数

(1) Generator

<1>异步方案-上
 function * foo() {
     console.log('start')
     yield 'foo' //我们可以在函数体内部随时使用 yield 返回一个值,遇到yield 时,foo暂停,yield后面的值就会作为next()的返回对象里面的value去返回
     //yield不像return一样立即结束这个执行,他只是暂停我们生成器的执行,直到我们外界下一次去调用我们生成器对象的next方法时,他就会继续从 yield 向下执行
     const r = yield 'foo'
     console.log(r)//bar //yield可以接收传入的值,bar作为这个语句的返回值,foo函数继续执行会执行到下一个yield的位置
     try { //用try catch捕获异常
         console.log(123)
     } catch(e){ 
         console.log(e)//Error: generator error
     }
 }
 const generator = foo()//调用一个生成器函数,并不会立即执行这个函数,而是得到一个生成器对象
 const res = generator.next()//直到我们手动调用这个函数的函数体,这个函数才会开始执行
 console.log(res)//{value:'foo',done:false}//在next方法返回对象中拿到这个返回值 done表示这个生成器是否已经全部执行完了

 generator.next('bar')//另外我们调用我们生成器对象方法时传入了一个参数的话,那所传入的这个参数会作为yield这个语句的返回值,也就是说我们在yield的左边实际上时可以接收到这个值的

 generator.throw(new Error('generator error'))//我们在生成器外部手动调用的是throw方法,也会让生成器继续执行,这个方法可以对生成器函数内部去抛出异常,内部在执行过程中就可以得到这个异常,用try catch捕获
<2>异步方案-中
 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()
     })
 }
 function* main() {
     const res1 = yield ajax('xxx')
     console.log(res1) //{code: 200, buniss_code: 0, message: "SUCCESS", data: {…}} 正常打印
     const res2 = yield ajax('xxx')
     console.log(res2) //{code: 200, buniss_code: 0, message: "SUCCESS", data: {…}} 正常打印
 }
 const g = main() //main {<suspended>}
 const r = g.next() //{value: Promise, done: false},r.value:yield返回的Promise对象
 r.value.then(data => { //通过then指定yield返回的Promise对象的回调,在Promise对象的回调对象当中拿到执行结果,通过再调用一次next,把结果data传递进去,main函数会继续执行,传递的data会作为当前的这个yield的返回值,这样就可以拿到这个res
     const a1 = g.next(data)//像这样在promise函数内部,我们就彻底消灭了Promise的回调,有一种近乎于同步代码的体验
     if (a1.done) return
     a1.value.then(data => { //以此类推,如果我们在main函数当中多次使用yield的方式去返回promise对象,而且每次返回的都是一个promise对象,那我们就可以不断的在结果对象then当中调用next,直到next返回对象的done为true,也是就main函数的对象执行完毕为止
         const a2 = g.next(data)
         console.log(a2)
         if (a2.done) return
         //...
     })
 })
<3>异步方案-下

将上面的r.value.then改成递归形式

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()
     })
 }
function* main() {
     try {//try catch
         const res1 = yield ajax('xxx')
         console.log(res1) 
         const res2 = yield ajax('xxx')
         console.log(res2) 
     } catch (e) {
         console.log(e)
     }
 }
//把此处封装
const g = main() 
function handleResult(result) {
     if (result.done) return //生成器结束
     result.value.then(data => {
         handleResult(g.next(data))
     }, error => {
         g.throw(error)
     }))
}
handleResult(g.next())
function co(generator) {
  const g = generator() //main {<suspended>}
  function handleResult(result) {
      if (result.done) return //生成器结束
      result.value.then(data => {
          handleResult(g.next(data))
      }, error => {
          g.throw(error)
      })
  }
  handleResult(g.next())
}
co(main)

向上面CO这个生成器函数执行器,在社区有一个更完善的库,就叫CO,在async/await后不那么流行了

(2) Async函数-- Async/Await语法糖

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()
   })
}
async function main() {
   try {
       const res1 = await ajax('xxx')
       console.log(res1) //{code: 200,...} 正常打印
       const res2 = await ajax('xxx')
       console.log(res2) //{code: 200, ...} 正常打印
   } catch (e) {
       console.log(e)
   }
}
const promise = main()
promise.then(() => {
   console.log('completed')//completed
})
  • 语言层面标准异步语法,不需要构造CO这样的生成器
  • async返回一个promise对象,更加利于我们对整体代码的控制
  • await 关键词只能够出现在async函数内部,不能在外部(顶层作用域)使用

async是generator的语法糖,await是promise的语法糖