带你熟悉异步解决方案

1,020 阅读3分钟

什么是异步

简单来说就是遇到(网络请求,定时任务)不能卡住;

异步进化史

回调函数 —> Promise —> Generator —> async/await。

同步异步举例

// 异步 (callback 回调函数)
console.log(100)
setTimeout(function(){
  console.log(200)
},1000)
console.log(300)    
//100
//300
//200

// 同步
console.log(100)
alert(200)
console.log(300)
//100
//200
//300

异步应用场景

网络请求,如ajax图片加载

// ajax
console.log('start')
$.get('./data1/json',function(data1){
  console.log(data1)
})
console.log('end')

定时任务,如setTimeout

//图片加载
console.log('start')
let img = document.createElement('img')
img.onload = function(){
  console.log('loaded')
}
img.src = '/xxx.png'
console.log('end')

事件监听、绑定

// 事件监听
document.getElementById('#myDiv').addEventListener('click', function (e) {
  console.log('我被点击了')
}, false);

回调地狱

当回调只有一层的时候感觉没有什么问题,但是一但嵌套层级过多,代码可读性和可维护性就变差。

const https = require('https');


https.get('目标接口1', (res) => {
  console.log(data)
  https.get('目标接口2', (res) => {
    https.get('目标接口3'),  (res) => {
        console.log(data)
        https.get('目标接口4', (res) => {
          https.get('目标接口5', (res) => {
            console.log(data)
            .....
            // 无尽的回调
          }
        }
    }
  }
})

典型题

同步与异步的区别

  • 基于JS是单线程语言
  • 异步不会阻塞代码执行
  • 同步会阻塞代码执行

异步加载图片

function loadImg(src){
    return new promise(
        (resolve,reject)=>{
            const img = document.createElement('img')
            img.onload = () => {
                resolve(img)
            }
            img.onerror=() => {
                reject(new Error(`图片加载失败`))
            }
            img.src = src
        }
    )
}
const url1 = 'xxx'
const url2 = 'xxx'
loadImg(url).then(img=>{
    console.log(img1.width)
    return img1  //普通对象
}).then(img1=>{
    console.log(img.height)
  	return loadImg(url2)  //promise实例
}).then(img2=>{
   	console.log(img2.height)
}).catch(ex=>console.log(ex))

setTimeout场景题

// setTimeout笔试题
console.log(1)
setTimeout(function(){
  console.log(2)
},1000)
console.log(3)
setTimeout(funcion()=>{
  console.log(4)
},0)
console.log(5)

//1 3 5 4 2

回调函数有什么缺点?如何解决回调地狱问题?

回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})

回调地狱的根本问题就是:

  1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  2. 嵌套函数一多,就很难处理错误

当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return

Promise

Promise是什么?

先来看一段代码:

const https = require('https');

function httpPromise(url){
  return new Promise(function(resolve,reject){
    https.get(url, (res) => {
      resolve(data);
    }).on("error", (err) => {
      reject(error);
    });
  })
}

httpPromise().then( function(data){
  // todo 
}).catch(function(error){ //todo })


httpPromise(url1)
    .then(res => {
        console.log(res);
        return httpPromise(url2);
    })
    .then(res => {
        console.log(res);
        return httpPromise(url3);
    })
    .then(res => {
      console.log(res);
      return httpPromise(url4);
    })
    .then(res => console.log(res));。

从上面的例子可以看出,Promise 会接收一个执行器,在这个执行器里,我们需要把目标的异步任务给”填进去“。

在 Promise 实例创建后,执行器里的逻辑会立刻执行,在执行的过程中,根据异步返回的结果,决定如何使用 resolve 或 reject 来改变 Promise实例的状态。

Promise 实例有三种状态:

  • 等待中(pending):表示进行中。这是 Promise 实例创建后的一个初始态;
  • 完成了 (resolved):表示成功完成。这是我们在执行器中调用 resolve 后,达成的状态;
  • 拒绝了(rejected):表示操作失败、被拒绝。这是我们在执行器中调用 reject后,达成的状态;

一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变。

pending->resolved 或 pending->rejected变化不可逆。

// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {

})

// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve()
    })
})

// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject()
    })
})

// 直接返回一个 resolved 状态
Promise.resolve(100)
// 直接返回一个 rejected 状态
Promise.reject('some error')
new Promise((resolve, reject) => {
  resolve('success')
  // 无效
  reject('reject')
})

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的。

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh

状态和 then catch

状态变化会触发 then catch

  • pending 不会触发任何 then catch 回调
  • 状态变为 resolved 会触发后续的 then 回调
  • 状态变为 rejected 会触发后续的 catch 回调

then catch 会继续返回 Promise ,此时可能会发生状态变化!!!

// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
    return 100
})

// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
    throw new Error('err')
})

// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
})

// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
    throw new Error('err')
})

Promise常见题

Promise特点

Promise 的特点是什么,分别有什么优缺点?什么是 Promise 链?Promise 构造函数执行和 then 函数执行有什么区别?

特点:实现链式调用

优点:Promise 很好地解决了回调地狱的问题

缺点:比如无法取消 Promise,错误需要通过回调函数捕获。

Promise链:

  • 每次调用 then 之后返回的都是一个全新的Promise,因此又可以接着使用then方法,由此形成promise链
  • 在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装

Promise 构造函数执行和 then 函数执行有什么区别:

  • 构造 Promise 的时候,构造函数内部的代码是立即执行的
  • then函数在promise.resolve()执行后执行

代码题

// 第一题
Promise.resolve().then(() => { 
  //没报错返回resolved状态
    console.log(1)   // 1
}).catch(() => {
    console.log(2)   // 不会走
}).then(() => {
    console.log(3)  // 3
}) // resolved

//结果 1  3

// 第二题
Promise.resolve().then(() => { 
    console.log(1) 
  	// 返回 rejected 状态的 promise
    throw new Error('erro1')
}).catch(() => { 
  // 返回 resolved 状态的 promise
    console.log(2)
}).then(() => {
    console.log(3)
})

//结果 1 2 3

// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
    console.log(2)
}).catch(() => {
    console.log(3)
})

//结果 1 2

Generator

Generator是什么: ​

Generator 一个有利于异步的特性是,它可以在执行中被中断、然后等待一段时间再被我们唤醒。通过这个“中断后唤醒”的机制,我们可以把 Generator看作是异步任务的容器,利用 yield 关键字,实现对异步任务的等待。

function firstAjax() {
  ajax(url1, () => {
    // 调用secondAjax
    secondAjax()
  })
}

function secondAjax() {
  ajax(url2, () => {
    // 处理逻辑
  })
}

ajax(url, () => {
  // 调用firstAjax
  firstAjax()
})

我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:

function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

发现Generator确实有点麻烦,每次迭代都要.next()才能继续下一步的操作,直到done为true时停止。所以我们利用一个第三方库(co)直接执行:

co是什么:generator函数(生成器函数)的自动执行函数。

const co = require('co');
co(httpGenerator());

async/await

async/await产生背景

  • 异步回调callback
  • Promise then catch链式调用,但也是基于回调函数
  • async/await是同步语法,彻底消灭回调函数

使用

一个函数如果加上 async ,那么该函数就会返回一个 Promise

async function test() {
  return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}

async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值一样,并且 await 只能配套 async 使用。

用同步方法编写异步。

function loadImg(src) {
    const promise = new Promise((resolve, reject) => {
        const img = document.createElement('img')
        img.onload = () => {
            resolve(img)
        }
        img.onerror = () => {
            reject(new Error(`图片加载失败 ${src}`))
        }
        img.src = src
    })
    return promise
}

async function loadImg1() {
    const src1 = 'http:xxx.png'
    const img1 = await loadImg(src1)
    return img1
}

async function loadImg2() {
    const src2 = 'https://xxx.png'
    const img2 = await loadImg(src2)
    return img2
}

(async function () {
    // 注意:await 必须放在 async 函数中,否则会报错
    try {
        // 加载第一张图片
        const img1 = await loadImg1()
        console.log(img1)
        // 加载第二张图片
        const img2 = await loadImg2()
        console.log(img2)
    } catch (ex) {
        console.error(ex)
    }
})()

典型场景

并发

下面代码模拟了三个请求接口,也就是三个请求没有任何依赖关系,却要等到上一个执行完才执行下一个,带来时间上的浪费。

(async () => {
  const getList1 = await getList1();
  const getList2 = await getList1();
  const getList3 = await getList2();
})();

解决方案:

(async () => {
  const listPromise = getList();
  const anotherListPromise = getAnotherList();
  await listPromise;
  await anotherListPromise;
})();

// 也可以使用
(async () => {
  Promise.all([getList(), getAnotherList()]).then(...);
})();

捕获错误

使用 try catch 捕获错误,当我们需要捕获多个错误并做不同的处理时,try catch 会导致代码杂乱:

async function asyncTask(cb) {
    try {
       const res1 = await request1(resByRequest1);  //resByRequest1返回值为promise
       if(!res1) return cb('error1');
    } catch(e) {
        return cb('error2');
    }

    try {
       const res2 = await request2(resByRequest2); //resByRequest2返回值为promise
    } catch(e) {
        return cb('error3');
    }
}

简化错误捕获:添加一个中间函数:

export default function to(promise) {
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]);
}

错误捕获的代码:

async function asyncTask() {
     let err, data
     [err, data1] = await to(resByRequest1);
     if(!data1) throw new Error('xxxx');
  
     [err, data2] = await to(resByRequest2);
     if(!data2) throw new Error('xxxx');
}

async/await和Promise的关系

  • async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
  • await相当于Promise的then
  • try...catch可捕获异常,代替了Promise的catch
async function fn2() {
    return new Promise(() => {})
}
console.log( fn2() )

async function fn1() { // 执行async函数,返回的是一个Promise对象
    return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
  • await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
  • await 后续跟非 Promise 对象:会直接返回
(async function () {
    const p1 = new Promise(() => {})
    await p1
    console.log('p1') // 不会执行
})()

(async function () {
    const p2 = Promise.resolve(100)
    const res = await p2
    console.log(res) // 100
})()

(async function () {
    const res = await 100
    console.log(res) // 100
})()

(async function () {
    const p3 = Promise.reject('some err')
    const res = await p3
    console.log(res) // 不会执行
})()

try...catch 捕获 rejected 状态

(async function () {
    const p4 = Promise.reject('some err')
    try {
        const res = await p4
        console.log(res)
    } catch (ex) {
        console.error(ex)
    }
})()

总体来看:

  • async 封装 Promise
  • await 处理 Promise 成功
  • try...catch 处理 Promise 失败

异步本质

await 是同步写法,但本质还是异步调用。

async function async1 () {
  console.log('async1 start')   // 2
  await async2() 
  console.log('async1 end') // 5   
}

async function async2 () {
  console.log('async2') // 3
}

console.log('script start')  // 1
async1()
console.log('script end') //4

async和await常见题

async 及 await 的特点,它们的优点和缺点分别是什么?await 原理是什么?

特点:

  • 一个函数如果加上async 那么其返回值是Promise,async 就是将函数返回值使用 Promise.resolve() 进行包裹,和then处理返回值一样
  • await只能配合async使用 不能单独使用

优点:

  • 相比于Promise来说优势在于能够写出更加清晰的调用链,并且也能优雅的解决回调地狱的问题

缺点:

  • 因为await将异步代码变成了同步代码,如果多个异步之间没有关系,会导致性能降低

原理:

  • await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator

参考链接

后续