异步promise、Async/await介绍

1,753 阅读6分钟

异步promise、Async/await介绍

promise基本介绍

  • Promise 是异步编程的一种解决方案,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,有以下特点:
  1. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
  3. 最要用于解决异步的层层嵌套,防止嵌套黑洞,是代码结构更简洁、提高可读性

promise 应用

  1. 最基本的用法
const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
  1. promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject,它们是两个函数。
  2. resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”,
  3. reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected)
  4. 实例生成后可以这样使用
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

2、延迟操作

function delay(time) {
    return new Promise(function(resolve, reject){
        setTimeout(resolve, time);
    });
}

delay(1000)
.then(function(){
    console.log("after 1000ms");
    return delay(2000);
})
.then(function(){
    console.log("after another 2000ms");
})
.then(function(){
    console.log("step 4 (next Job)");
    return delay(5000);
})
// ...
  1. 图片加载
function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}
  1. 简易版封装数据获取
const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出错了', error);
});
  • 示例5:每10秒检测一次用户是否登录
function onUserLoggedIn(id) {
  
  return ajax(`user/${id}`).then(user => {
    
    if (user.state === 'logged_in') {
      return;
    }
    
    return new Promise(resolve => {
      return setTimeout(resolve, 10 * 1000));
    }).then(() => {
      return onUserLoggedIn(id);
    })
  });
}

  • 示例5改进:业务代码与promise层的代码混合在一起,可读性差,继续改进
function delay(time) {
  return new Promise(resolve => {
    return setTimeout(resolve, time));
  });
}

function until(conditionFn, delayTime = 1000) {
  return Promise.resolve().then(() => {
    return conditionFn();
  }).then(result => {
    if (!result) {
      return delay(delayTime).then(() => {
        return until(conditionFn, delayTime);
      });
    }
  });
}
  • 示例5箭头函数改进
let delay = time =>
    new Promise(resolve =>
        setTimeout(resolve, time)
    );
let until = (cond, time) =>
    cond().then(result =>
        result || delay(time).then(() =>
            until(cond, time)
        )
    );
  • 使用示例5
// 写法一
function onUserLoggedIn(id) {
  return until(() => {
    return ajax(`user/${id}`).then(user => {
      return user.state === 'logged_in';
    });
  }, 10 * 1000);
}

// 写法二
async function onUserLoggedIn(id) {
  return await until(async () => {
    let user = await ajax(`user/${id}`);
    return user.state === 'logged_in';
  }, 10 * 1000);
}
  1. callback回调异步转换
function callbackToPromise(method, ...args) {
    return new Promise(function(resolve, reject) {
        return method(...args, function(err, result) {
            return err ? reject(err) : resolve(result);
        });
    });
}

async function getFirstUser() {
    let users = await callbackToPromise(getUsers);
    return users[0].name;
}
  • 异常处理
var p = Promise.resolve(374);

p.then(function fulfilled(msg){
    // numbers don't have string functions,
    // so will throw an error
    console.log(msg.toLowerCase());
})
.done(null, function() {
    // If an exception is caused here, it will be thrown globally 
});

Async/await

Async/await 基本介绍

  • async函数是对Generator 函数的改进

基本用法

  • async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
  • async函数内部return语句返回的值,会成为then方法回调函数的参数。
async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});
  • async 函数有多种使用形式。
// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};
  • await 命令
  • await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。
async function f() {
  return await 123;
}

f().then(v => console.log(v))
  • 只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行
async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

使用注意点

  • 陷阱:在使用异步方式时,忘记使用await
async function getFirstUser() {
    try {
        let users = await getUsers();
        return users[0].name;
    } catch (err) {
        return {
            name: 'default user'
        };
    }
}

// 注意添加 await
let user = await getFirstUser();
  • 陷阱:不存在继发关系同时使用多个await
let foo = await getFoo();
let bar = await getBar();

// 应改为如下所示的方法,并行运行等待结果,
//性能上有优势
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 第二种方式,不推荐这种方法,可读性差
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
  • 陷阱:await命令只能用在async函数之中,如果用在普通函数,就会报错
async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 报错
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

// 改进这是继发执行
async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

// 并发执行可作如此修改,写法1
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}
// 写法2
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}
  • 处理异常
myApp.registerEndpoint('GET', '/api/firstUser', async function(req, res) {
    try {
        let firstUser = await getFirstUser();
        res.json(firstUser)
    } catch (err) {
        console.error(err);
        res.status(500);
    }
});

promise与Asyns联合使用,解决继发、并发同存在的问题

  • 示例1
  • 我想做一个披萨
  • 独立制作面团
  • 独立制作酱料
  • 在我们决定什么样的奶酪放进披萨时,我们希望能够品尝酱汁,使披萨味道最美味,
// 示例1
async function makePizza(sauceType = 'red') {
  
  let dough  = await makeDough();
  let sauce  = await makeSauce(sauceType);
  let cheese = await grateCheese(sauce.determineCheese());
  
  dough.add(sauce);
  dough.add(cheese);
  
  return dough;
}
  • 示例1运行如下
|-------- dough --------> |-------- sauce --------> |-- cheese -->
制作面团--->制作酱料--->磨碎奶酪,这是最好的么?我们要等待面团完成后,才能尝酱的味道,有没有更快的方法呢?
  • 示例改进
async function makePizza(sauceType = 'red') {
  
  let [ dough, sauce ] =
    await Promise.all([ makeDough(), makeSauce(sauceType) ]);
  let cheese = await grateCheese(sauce.determineCheese());
  
  dough.add(sauce);
  dough.add(cheese);
  
  return dough;
}
  • 运行如下
|-------- dough -------->
|--- sauce --->           |-- cheese -->
- Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
- 虽然,douch与sauce并行,相较于1会更快一点磨碎奶酪,从Promise.all方法运行机制来看,但仍然要等待面、酱料制作好了,才能磨碎奶酪,那么有更好的办法么?
  • 示例3
function makePizza(sauceType = 'red') {
  
  let doughPromise  = makeDough();
  let saucePromise  = makeSauce(sauceType);
  let cheesePromise = saucePromise.then(sauce => {
    return grateCheese(sauce.determineCheese());
  });
  
  return Promise.all([ doughPromise, saucePromise, cheesePromise ])
    .then(([ dough, sauce, cheese ]) => {
      
      dough.add(sauce);
      dough.add(cheese);
      
      return dough;
    });
}

// or 用asyns/await改写
async function makePizza(sauceType = 'red') {
  
  let doughPromise = makeDough();
  let saucePromise = makeSauce(sauceType);
  
  let sauce  = await saucePromise;
  let cheese = await grateCheese(sauce.determineCheese());
  let dough  = await doughPromise;
  
  dough.add(sauce);
  dough.add(cheese);
  
  return dough;
}
  • 运行结果如下
|--------- dough --------->
|---- sauce ----> |-- cheese -->
从代码阅读来看,这不是最好的写法,继续改进
  • 示例1 终版跟记忆函数的结合
function memoize(method) {
    let cache = {};
    
    return async function() {
        let args = JSON.stringify(arguments);
        cache[args] = cache[args] || method.apply(this, arguments);
        return cache[args];
    };
}

async function makePizza(sauceType = 'red') {
  
  let prepareDough  = memoize(async () => makeDough());
  let prepareSauce  = memoize(async () => makeSauce(sauceType));
  let prepareCheese = memoize(async () => {
    return grateCheese((await prepareSauce()).determineCheese());
  });
  
  let [ dough, sauce, cheese ] = 
    await Promise.all([
      prepareDough(), prepareSauce(), prepareCheese()
    ]);
    
  dough.add(sauce);
  dough.add(cheese);
  
  return dough;
}
  • 示例二: 制作汤
async function getSoupRecipe(<soupType>)
async function hireSoupChef(<soupRecipe:requiredSkills>)
async function buySoupPan()
async function makeSoup(<soupChef>, <soupRecipe>, <soupPan>)
- 提供一些代码制作我最喜欢的汤
- 制作汤需要配方、厨师、锅
- 上面的代码对于一些方法来说是一个松散的界面,它可以让你得到你需要的东西来制作汤
  • 基本方法
async function getSoupRecipe(soupType) {
    return await http.get(`/api/soup/${soupType}`);
}
 
async function buySoupPan() {
    return await http.get(`/api/soupPan`);
}
 
async function hireSoupChef(requiredSkills) {
    return await http.post(`/api/soupChef/hire`, {
        requiredSkills: requiredSkills
    });
}
 
async function makeSoup(soupChef, soupRecipe, soupPan) {
    return await http.post(`api/makeSoup`, {
        soupRecipe, soupPan, soupChef
    });
}
  • 示例2:暴力写法
async function makeSoupFromType(soupType) {

    let soupRecipe = await getSoupRecipe(soupType);
    let soupPan    = await buySoupPan();
    let soupChef   = await hireSoupChef(soupRecipe.requiredSkills);
    
    return await makeSoup(soupChef, soupRecipe, soupPan);
}
  • 代码是可以工作的,但是是继发执行的,买锅和聘请厨师并没有前后关系,这就会导致效率的低下,煮汤的时间延长,会饿肚子的。如何改进成将有继发关系的让他一步一步执行,没有继发关系的,并发执行,以提升效率。

  • 示例2改进型

async function makeSoupFromType(soupType) {
    
    let [soupRecipe, soupPan] = await* [
      getSoupRecipe(soupType),
      buySoupPan()
    ];
    let soupChef = await hireSoupChef(soupRecipe.requiredSkills);
    
    return await makeSoup(soupChef, soupRecipe, soupPan);
}
  • await* 只是一种表示方法,在真实的代码中应该用Promise.all()
  • 这时代码发生了耦合,明确指出'这两种方法必须并行运行'和'此后必须运行,维护性较差
  • 聘请厨师还是需要有锅,不合逻辑。
  • 继续改进示例2
async function makeSoupFromType(soupType) {
    
    let soupRecipePromise = getSoupRecipe(soupType);
    
    async function hireSoupChefWithSoupRecipe(_soupRecipePromise) {
        let soupRecipe = await _soupRecipePromise;
        return await hireSoupChef(soupRecipe.requiredSkills);
    }
    
    let [ soupRecipe, soupPan, soupChef ] = await* [
        
        soupRecipePromise,
        buySoupPan(),
        hireSoupChefWithSoupRecipe(soupRecipePromise)
    ]);
    
    return await makeSoup(soupChef, soupRecipe, soupPan);
}
  • 汤料要获取两次,并提前缓存汤料

  • 不方便阅读理解,在以后的维护、改进过程中,如果在 getSoupRecipe(soupType)加上await又会变慢

  • 示例2:继续改进,看看缓存函数和asyns能发生什么化学反应

  • 考虑我们在上述方法中遇到的问题。我们需要得到一份汤配方,但我们不想两次拿取,所以我们必须自己手动缓存。

function memoize(method) {
    let cache = {};
    
    return async function() {
        let args = JSON.stringify(arguments);
        cache[args] = cache[args] || method.apply(this, arguments);
        return cache[args];
    };
}

let getSoupRecipe = memoize(async function(soupType) {
    return await http.get(`/api/soup/${soupType}`);
});
 
let buySoupPan = memoize(async function() {
    return await http.get(`/api/soupPan`);
});
 
let hireSoupChef = memoize(async function(soupType) {
    let soupRecipe = await getSoupRecipe(soupType)
    
    return await http.post(`/api/soupChef/hire`, {
        requiredSkills: soupRecipe.requiredSkills
    });
});
 
let makeSoup = memoize(async function(soupType) {
    
    let [ soupRecipe, soupPan, soupChef ] = await* [     
      getSoupRecipe(soupType), buySoupPan(), hireSoupChef(soupType)
    ];
    
    return await http.post(`api/makeSoup`, {
      soupRecipe, soupPan, soupChef
    });
});
  • memoize()把任何异步函数变成一个memoized函数 - 也就是说,如果它被调用了两次相同的参数,它将在第二次返回相同的缓存值。
  • memoize函数中不使用await - 我们实际缓存的内容是方法返回的诺言,而不是最终值。这意味着我们甚至不需要等待异步方法返回任何内容,然后再缓存它的未来值。

思考点

  • Async/await可以解决回调黑洞的问题,给开发、和层序的阅读性带来便捷。但是暴力、滥用Async/await同样也会带来性能问题,并发关系的被写成继发关系,就会大大影响性能。
  • 要做好异常的处理工作。

参考链接