金沙棋牌官方平台

当前位置:金沙棋牌 > 金沙棋牌官方平台 > 更好的异步解决方案,关于如何解决一个axios调用

更好的异步解决方案,关于如何解决一个axios调用

来源:http://www.logblo.com 作者:金沙棋牌 时间:2019-11-22 11:09

async / await:更好的异步解决方案

2017/07/27 · JavaScript · async, await

原文出处: 波同学   

在实际开发中总会遇到许多异步的问题,最常见的场景接口请求之后一定要等一段时间才能得到结果,如果遇到多个接口前后依赖,那么问题就变得复杂。大家都一直在尝试使用更好的方案来解决这些问题。最开始只能利用回调函数,后来开始有人使用Promise的思维来搞定。到ES6中开始支持原生的Promise,引入Generator函数。

直到ES7,有了async/await。

这是一个用同步的思维来解决异步问题的方案。

我想很多人可能还不太分得清同步与异步的区别。如果你已经彻底了解了事件循环,那么想必对异步的概念应该非常了解。当我们发出了请求,并不会等待响应结果,而是会继续执行后面的代码,响应结果的处理在之后的事件循环中解决。那么同步的意思,就是等结果出来之后,代码才会继续往下执行。

我们可以用一个两人问答的场景来比喻异步与同步。A向B问了一个问题之后,不等待B的回答,接着问下一个问题,这是异步。A向B问了一个问题之后,然后就笑呵呵的等着B回答,B回答了之后他才会接着问下一个问题。

那么我们先记住这个特点,async/await使用同步的思维,来解决异步的问题。在继续讲解它的语法与使用之前,我们先介绍一下如何在我们的开发环境中支持该语法。

如果你已经知道如何配置,可跳过

这里我们使用async/await

现代 JS 流程控制:从回调函数到 Promises 再到 Async/Await

2018/09/03 · JavaScript · Promises

原文出处: Craig Buckler   译文出处:OFED   

JavaScript 通常被认为是异步的。这意味着什么?对开发有什么影响呢?近年来,它又发生了怎样的变化?

看看以下代码:

result1 = doSomething1(); result2 = doSomething2(result1);

1
2
result1 = doSomething1();
result2 = doSomething2(result1);

大多数编程语言同步执行每行代码。第一行执行完毕返回一个结果。无论第一行代码执行多久,只有执行完成第二行代码才会执行。

本文作者: 伯乐在线 - ascoders 。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

一、如何在自己的开发环境中支持async/await语法

这里主要介绍两种方式。

await的含义为等待。意思就是代码需要等待await后面的函数运行完并且有了返回结果之后,才继续执行下面的代码。这正是同步的效果。

单线程处理程序

JavaScript 是单线程的。当浏览器选项卡执行脚本时,其他所有操作都会停止。这是必然的,因为对页面 DOM 的更改不能并发执行;一个线程
重定向 URL 的同时,另一个线程正要添加子节点,这么做是危险的。

用户不容易察觉,因为处理程序会以组块的形式快速执行。例如,JavaScript 检测到按钮点击,运行计算,并更新 DOM。一旦完成,浏览器就可以自由处理队列中的下一个项目。

(附注: 其它语言比如 PHP 也是单线程,但是通过多线程的服务器比如 Apache 管理。同一 PHP 页面同时发起的两个请求,可以启动两个线程运行,它们是彼此隔离的 PHP 实例。)

根据笔者的项目经验,本文讲解了从函数回调,到 es7 规范的异常处理方式。异常处理的优雅性随着规范的进步越来越高,不要害怕使用 try catch,不能回避异常处理。

1. webpack中支持该语法

首先在当前项目中使用npm下载babel-loader

> npm install babel-loader --save-dev

1
> npm install babel-loader --save-dev

然后在配置文件webpack.confing.dev.js中配置,在module.exports.module.rules中添加如下配置元素即可。

{ test: /.(js|jsx)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), options: { cacheDirectory: true, }, },

1
2
3
4
5
6
7
8
  {
    test: /.(js|jsx)$/,
    include: paths.appSrc,
    loader: require.resolve('babel-loader'),
    options: {
      cacheDirectory: true,
    },
  },

如果你使用最新版本的create-react-app或者vue-cli来构建你的代码,那么它们应该已经支持了该配置。

但是我们需要注意的是,await关键字只能在async函数中使用。并且await后面的函数运行后必须返回一个Promise对象才能实现同步的效果。

通过回调实现异步

单线程产生了一个问题。当 JavaScript 执行一个“缓慢”的处理程序,比如浏览器中的 Ajax 请求或者服务器上的数据库操作时,会发生什么?这些操作可能需要几秒钟 – 甚至几分钟。浏览器在等待响应时会被锁定。在服务器上,Node.js 应用将无法处理其它的用户请求。

解决方案是异步处理。当结果就绪时,一个进程被告知调用另一个函数,而不是等待完成。这称之为回调,它作为参数传递给任何异步函数。例如:

doSomethingAsync(callback1); console.log('finished'); // 当 doSomethingAsync 完成时调用 function callback1(error) { if (!error) console.log('doSomethingAsync complete'); }

1
2
3
4
5
6
7
doSomethingAsync(callback1);
console.log('finished');
 
// 当 doSomethingAsync 完成时调用
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}

doSomethingAsync() 接收回调函数作为参数(只传递该函数的引用,因此开销很小)。doSomethingAsync() 执行多长时间并不重要;我们所知道的是,callback1() 将在未来某个时刻执行。控制台将显示:

finished doSomethingAsync complete

1
2
finished
doSomethingAsync complete

我们需要一个健全的架构捕获所有同步、异步的异常。业务方不处理异常时,中断函数执行并启用默认处理,业务方也可以随时捕获异常自己处理。

2. gulp中支持该语法

首先安装gulp插件

> npm install gulp-babel --save-dev

1
> npm install gulp-babel --save-dev

然后编写任务

var gulp = require('gulp'); var babel = require('gulp-babel'); gulp.task('babel', function() { return gulp.src('src/app.js') .pipe(babel()) .pipe(gulp.dest('dist')); });

1
2
3
4
5
6
7
8
var gulp = require('gulp');
var babel = require('gulp-babel');
 
gulp.task('babel', function() {
  return gulp.src('src/app.js')
    .pipe(babel())
    .pipe(gulp.dest('dist'));
});

当我们使用一个变量去接收await的返回值时,该返回值为Promise中resolve出来的值(也就是PromiseValue)。

回调地狱

通常,回调只由一个异步函数调用。因此,可以使用简洁、匿名的内联函数:

doSomethingAsync(error => { if (!error) console.log('doSomethingAsync complete'); });

1
2
3
doSomethingAsync(error => {
  if (!error) console.log('doSomethingAsync complete');
});

一系列的两个或更多异步调用可以通过嵌套回调函数来连续完成。例如:

async1((err, res) => { if (!err) async2(res, (err, res) => { if (!err) async3(res, (err, res) => { console.log('async1, async2, async3 complete.'); }); }); });

1
2
3
4
5
6
7
async1((err, res) => {
  if (!err) async2(res, (err, res) => {
    if (!err) async3(res, (err, res) => {
      console.log('async1, async2, async3 complete.');
    });
  });
});

不幸的是,这引入了回调地狱 —— 一个臭名昭著的概念,甚至有专门的网页介绍!代码很难读,并且在添加错误处理逻辑时变得更糟。

回调地狱在客户端编码中相对少见。如果你调用 Ajax 请求、更新 DOM 并等待动画完成,可能需要嵌套两到三层,但是通常还算可管理。

操作系统或服务器进程的情况就不同了。一个 Node.js API 可以接收文件上传,更新多个数据库表,写入日志,并在发送响应之前进行下一步的 API 调用。

优雅的异常处理方式就像冒泡事件,任何元素可以自由拦截,也可以放任不管交给顶层处理。

二、如何使用

async函数是Generator的一个语法糖。如果你不知道Generator是什么函数也没有关系,我们只需要知道async函数实际上返回的是一个Promise对象即可。

async function fn() { return 30; } // 或者 const fn = async () => { return 30; }

1
2
3
4
5
6
7
8
async function fn() {
    return 30;
}
 
// 或者
const fn = async () => {
    return 30;
}

在声明函数时,前面加上关键字async,这就是async的用法。当我们用console.log打印出上面声明的函数fn,我们可以看到如下结果:

console.log(fn()); // result Promise = { __proto__: Promise, [[PromiseStatus]]: "resolved", [[PromiseValue]]: 30 }

1
2
3
4
5
6
7
8
console.log(fn());
 
// result
Promise = {
    __proto__: Promise,
    [[PromiseStatus]]: "resolved",
    [[PromiseValue]]: 30
}

很显然,fn的运行结果其实就是一个Promise对象。因此我们也可以使用then来处理后续逻辑。

fn().then(res => { console.log(res); // 30 })

1
2
3
fn().then(res => {
    console.log(res);  // 30
})

await的含义为等待。意思就是代码需要等待await后面的函数运行完并且有了返回结果之后,才继续执行下面的代码。这正是同步的效果。

但是我们需要注意的是,await关键字只能在async函数中使用。并且await后面的函数运行后必须返回一个Promise对象才能实现同步的效果。

当我们使用一个变量去接收await的返回值时,该返回值为Promise中resolve出来的值。

// 定义一个返回Promise对象的函数 function fn() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(30); }, 1000); }) } // 然后利用async/await来完成代码 const foo = async () => { const t = await fn(); console.log(t); console.log('next code'); } foo(); // result: // 30 // next code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义一个返回Promise对象的函数
function fn() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(30);
        }, 1000);
    })
}
 
// 然后利用async/await来完成代码
const foo = async () => {
    const t = await fn();
    console.log(t);
    console.log('next code');
}
 
foo();
 
// result:
// 30
// next code

运行这个例子我们可以看出,当在async函数中,运行遇到await时,就会等待await后面的函数运行完毕,而不会直接执行next code

如果我们直接使用then方法的话,想要达到同样的结果,就不得不把后续的逻辑写在then方法中。

const foo = () => { return fn().then(t => { console.log(t); console.log('next code'); }) } foo();

1
2
3
4
5
6
7
8
const foo = () => {
    return fn().then(t => {
        console.log(t);
        console.log('next code');    
    })
}
 
foo();

很显然如果使用async/await的话,代码结构会更加简洁,逻辑也更加清晰。

// 定义一个返回Promise对象的函数

Promises

ES2015(ES6) 引入了 Promises。回调函数依然有用,但是 Promises 提供了更清晰的链式异步命令语法,因此可以串联运行(下个章节会讲)。

打算基于 Promise 封装,异步回调函数必须返回一个 Promise 对象。Promise 对象会执行以下两个函数(作为参数传递的)其中之一:

  • resolve:执行成功回调
  • reject:执行失败回调

以下例子,database API 提供了一个 connect() 方法,接收一个回调函数。外部的 asyncDBconnect() 函数立即返回了一个新的 Promise,一旦连接创建成功或失败,resolve()reject() 便会执行:

const db = require('database'); // 连接数据库 function asyncDBconnect(param) { return new Promise((resolve, reject) => { db.connect(param, (err, connection) => { if (err) reject(err); else resolve(connection); }); }); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const db = require('database');
 
// 连接数据库
function asyncDBconnect(param) {
 
  return new Promise((resolve, reject) => {
 
    db.connect(param, (err, connection) => {
      if (err) reject(err);
      else resolve(connection);
    });
 
  });
 
}

Node.js 8.0 以上提供了 util.promisify() 功能,可以把基于回调的函数转换成基于 Promise 的。有两个使用条件:

  1. 传入一个唯一的异步函数
  2. 传入的函数希望是错误优先的(比如:(err, value) => …),error 参数在前,value 随后

举例:

// Node.js: 把 fs.readFile promise 化 const util = require('util'), fs = require('fs'), readFileAsync = util.promisify(fs.readFile); readFileAsync('file.txt');

1
2
3
4
5
6
7
// Node.js: 把 fs.readFile promise 化
const
  util = require('util'),
  fs = require('fs'),
  readFileAsync = util.promisify(fs.readFile);
 
readFileAsync('file.txt');

各种库都会提供自己的 promisify 方法,寥寥几行也可以自己撸一个:

// promisify 只接收一个函数参数 // 传入的函数接收 (err, data) 参数 function promisify(fn) { return function() { return new Promise( (resolve, reject) => fn( ...Array.from(arguments), (err, data) => err ? reject(err) : resolve(data) ) ); } } // 举例 function wait(time, callback) { setTimeout(() => { callback(null, 'done'); }, time); } const asyncWait = promisify(wait); ayscWait(1000);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// promisify 只接收一个函数参数
// 传入的函数接收 (err, data) 参数
function promisify(fn) {
  return function() {
      return new Promise(
        (resolve, reject) => fn(
          ...Array.from(arguments),
        (err, data) => err ? reject(err) : resolve(data)
      )
    );
  }
}
 
// 举例
function wait(time, callback) {
  setTimeout(() => { callback(null, 'done'); }, time);
}
 
const asyncWait = promisify(wait);
 
ayscWait(1000);

文字讲解仅是背景知识介绍,不包含对代码块的完整解读,不要忽略代码块的阅读。

异常处理

在Promise中,我们知道是通过catch的方式来捕获异常。而当我们使用async时,则通过try/catch来捕获异常。

function fn() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error.'); }, 1000); }) } const foo = async () => { try { await fn(); } catch (e) { console.log(e); // some error } } foo();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fn() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error.');
        }, 1000);
    })
}
 
const foo = async () => {
    try {
        await fn();
    } catch (e) {
        console.log(e);  // some error
    }
}
 
foo();

如果有多个await函数,那么只会返回第一个捕获到的异常。

function fn1() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error fn1.'); }, 1000); }) } function fn2() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error fn2.'); }, 1000); }) } const foo = async () => { try { await fn1(); await fn2(); } catch (e) { console.log(e); // some error fn1. } } foo();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function fn1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error fn1.');
        }, 1000);
    })
}
function fn2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error fn2.');
        }, 1000);
    })
}
 
const foo = async () => {
    try {
        await fn1();
        await fn2();
    } catch (e) {
        console.log(e);  // some error fn1.
    }
}
 
foo();

function fn() {

异步链式调用

任何返回 Promise 的函数都可以通过 .then() 链式调用。前一个 resolve 的结果会传递给后一个:

asyncDBconnect('') .then(asyncGetSession) // 传递 asyncDBconnect 的结果 .then(asyncGetUser) // 传递 asyncGetSession 的结果 .then(asyncLogAccess) // 传递 asyncGetUser 的结果 .then(result => { // 同步函数 console.log('complete'); // (传递 asyncLogAccess 的结果) return result; // (结果传给下一个 .then()) }) .catch(err => { // 任何一个 reject 触发 console.log('error', err); });

1
2
3
4
5
6
7
8
9
10
11
asyncDBconnect('http://localhost:1234')
  .then(asyncGetSession)      // 传递 asyncDBconnect 的结果
  .then(asyncGetUser)         // 传递 asyncGetSession 的结果
  .then(asyncLogAccess)       // 传递 asyncGetUser 的结果
  .then(result => {           // 同步函数
    console.log('complete');  //   (传递 asyncLogAccess 的结果)
    return result;            //   (结果传给下一个 .then())
  })
  .catch(err => {             // 任何一个 reject 触发
    console.log('error', err);
  });

同步函数也可以执行 .then(),返回的值传递给下一个 .then()(如果有)。

当任何一个前面的 reject 触发时,.catch() 函数会被调用。触发 reject 的函数后面的 .then() 也不再执行。贯穿整个链条可以存在多个 .catch() 方法,从而捕获不同的错误。

ES2018 引入了 .finally() 方法,它不管返回结果如何,都会执行最终逻辑 – 例如,清理操作,关闭数据库连接等等。当前仅有 Chrome 和 Firefox 支持,但是 TC39 技术委员会已经发布了 .finally() 补丁。

function doSomething() { doSomething1() .then(doSomething2) .then(doSomething3) .catch(err => { console.log(err); }) .finally(() => { // 清理操作放这儿! }); }

1
2
3
4
5
6
7
8
9
10
11
function doSomething() {
  doSomething1()
  .then(doSomething2)
  .then(doSomething3)
  .catch(err => {
    console.log(err);
  })
  .finally(() => {
    // 清理操作放这儿!
  });
}

1. 回调

如果在回调函数中直接处理了异常,是最不明智的选择,因为业务方完全失去了对异常的控制能力。

下方的函数 请求处理 不但永远不会执行,还无法在异常时做额外的处理,也无法阻止异常产生时笨拙的 console.log('请求失败') 行为。

function fetch(callback) { setTimeout(() = > { console.log('请求失败') }) } fetch(() = > { console.log('请求处理') // 永远不会执行 })

1
2
3
4
5
6
7
8
9
function fetch(callback) {
    setTimeout(() = > {
        console.log('请求失败')
    })
}
fetch(() = > {
    console.log('请求处理') // 永远不会执行
})
实践

在实践中我们遇到异步场景最多的就是接口请求,那么这里就以jquery中的$.get为例简单展示一下如何配合async/await来解决这个场景。

// 先定义接口请求的方法,由于jquery封装的几个请求方法都是返回Promise实例,因此可以直接使用await函数实现同步 const getUserInfo = () => $.get('xxxx/api/xx'); const clickHandler = async () => { try { const resp = await getUserInfo(); // resp为接口返回内容,接下来利用它来处理对应的逻辑 console.log(resp); // do something } catch (e) { // 处理错误逻辑 } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 先定义接口请求的方法,由于jquery封装的几个请求方法都是返回Promise实例,因此可以直接使用await函数实现同步
const getUserInfo = () => $.get('xxxx/api/xx');
 
const clickHandler = async () => {
    try {
        const resp = await getUserInfo();
        // resp为接口返回内容,接下来利用它来处理对应的逻辑
        console.log(resp);
 
        // do something
    } catch (e) {
        // 处理错误逻辑
    }
}

为了保证逻辑的完整性,在实践中try/catch必不可少。总之,不处理错误逻辑的程序员不是好程序员。

与Promise相比,个人认为async/await有一定的简洁性,但也并非就比Promise有绝对的优势,因此只能算是提供了另外一种稍好的方式,至于大家学习之后选择哪种方式来解决自己的问题,这仅仅只是你的个人喜好问题。

1 赞 3 收藏 评论

图片 1

    return new Promise((resolve, reject) => {

使用 Promise.all() 处理多个异步操作

Promise .then() 方法用于相继执行的异步函数。如果不关心顺序 – 比如,初始化不相关的组件 – 所有异步函数同时启动,直到最慢的函数执行 resolve,整个流程结束。

Promise.all() 适用于这种场景,它接收一个函数数组并且返回另一个 Promise。举例:

Promise.all([ async1, async2, async3 ]) .then(values => { // 返回值的数组 console.log(values); // (与函数数组顺序一致) return values; }) .catch(err => { // 任一 reject 被触发 console.log('error', err); });

1
2
3
4
5
6
7
8
Promise.all([ async1, async2, async3 ])
  .then(values => {           // 返回值的数组
    console.log(values);      // (与函数数组顺序一致)
    return values;
  })
  .catch(err => {             // 任一 reject 被触发
    console.log('error', err);
  });

任意一个异步函数 rejectPromise.all() 会立即结束。

2. 回调,无法捕获的异常

回调函数有同步和异步之分,区别在于对方执行回调函数的时机,异常一般出现在请求、数据库连接等操作中,这些操作大多是异步的。

异步回调中,回调函数的执行栈与原函数分离开,导致外部无法抓住异常。

从下文开始,我们约定用 setTimeout 模拟异步操作

function fetch(callback) { setTimeout(() = > { throw Error('请求失败') }) } try { fetch(() = > { console.log('请求处理') // 永远不会执行 }) } catch (error) { console.log('触发异常', error) // 永远不会执行 } // 程序崩溃 // Uncaught Error: 请求失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fetch(callback) {
    setTimeout(() = > {
        throw Error('请求失败')
    })
}
try {
    fetch(() = > {
        console.log('请求处理') // 永远不会执行
    })
} catch (error) {
    console.log('触发异常', error) // 永远不会执行
}
// 程序崩溃
// Uncaught Error: 请求失败

        setTimeout(() => {

使用 Promise.race() 处理多个异步操作

Promise.race()Promise.all() 极其相似,不同之处在于,当首个 Promise resolve 或者 reject 时,它将会 resolve 或者 reject。仅有最快的异步函数会被执行:

Promise.race([ async1, async2, async3 ]) .then(value => { // 单一值 console.log(value); return value; }) .catch(err => { // 任一 reject 被触发 console.log('error', err); });

1
2
3
4
5
6
7
8
Promise.race([ async1, async2, async3 ])
  .then(value => {            // 单一值
    console.log(value);
    return value;
  })
  .catch(err => {             // 任一 reject 被触发
    console.log('error', err);
  });

3. 回调,不可控的异常

我们变得谨慎,不敢再随意抛出异常,这已经违背了异常处理的基本原则。

虽然使用了 error-first 约定,使异常看起来变得可处理,但业务方依然没有对异常的控制权,是否调用错误处理取决于回调函数是否执行,我们无法知道调用的函数是否可靠。

更糟糕的问题是,业务方必须处理异常,否则程序挂掉就会什么都不做,这对大部分不用特殊处理异常的场景造成了很大的精神负担。

function fetch(handleError, callback) { setTimeout(() = > { handleError('请求失败') }) } fetch(() = > { console.log('失败处理') // 失败处理 }, error = > { console.log('请求处理') // 永远不会执行 })

1
2
3
4
5
6
7
8
9
10
11
function fetch(handleError, callback) {
    setTimeout(() = > {
        handleError('请求失败')
    })
}
fetch(() = > {
    console.log('失败处理') // 失败处理
}, error = > {
    console.log('请求处理') // 永远不会执行
})

            resolve(30);

前途光明吗?

Promise 减少了回调地狱,但是引入了其他的问题。

教程常常不提,整个 Promise 链条是异步的,一系列的 Promise 函数都得返回自己的 Promise 或者在最终的 .then().catch() 或者 .finally() 方法里面执行回调。

我也承认:Promise 困扰了我很久。语法看起来比回调要复杂,好多地方会出错,调试也成问题。可是,学习基础还是很重要滴。

延伸阅读:

  • MDN Promise documentation
  • JavaScript Promises: an Introduction
  • JavaScript Promises … In Wicked Detail
  • Promises for asynchronous programming

番外 Promise 基础

Promise 是一个承诺,只可能是成功、失败、无响应三种情况之一,一旦决策,无法修改结果。

Promise 不属于流程控制,但流程控制可以用多个 Promise 组合实现,因此它的职责很单一,就是对一个决议的承诺。

resolve 表明通过的决议,reject 表明拒绝的决议,如果决议通过,then 函数的第一个回调会立即插入 microtask 队列,异步立即执行

简单补充下事件循环的知识,js 事件循环分为 macrotask 和 microtask。 microtask 会被插入到每一个 macrotask 的尾部,所以 microtask 总会优先执行,哪怕 macrotask 因为 js 进程繁忙被 hung 住。 比如 setTimeout setInterval 会插入到 macrotask 中。

const promiseA = new Promise((resolve, reject) = > { resolve('ok') }) promiseA.then(result = > { console.log(result) // ok })

1
2
3
4
5
6
const promiseA = new Promise((resolve, reject) = > {
    resolve('ok')
})
promiseA.then(result = > {
    console.log(result) // ok
})

如果决议结果是决绝,那么 then 函数的第二个回调会立即插入 microtask 队列。

const promiseB = new Promise((resolve, reject) = > { reject('no') }) promiseB.then(result = > { console.log(result) // 永远不会执行 }, error = > { console.log(error) // no })

1
2
3
4
5
6
7
8
const promiseB = new Promise((resolve, reject) = > {
    reject('no')
})
promiseB.then(result = > {
    console.log(result) // 永远不会执行
}, error = > {
    console.log(error) // no
})

如果一直不决议,此 promise 将处于 pending 状态。

const promiseC = new Promise((resolve, reject) = > { // nothing }) promiseC.then(result = > { console.log(result) // 永远不会执行 }, error = > { console.log(error) // 永远不会执行 })

1
2
3
4
5
6
7
8
const promiseC = new Promise((resolve, reject) = > {
    // nothing
})
promiseC.then(result = > {
    console.log(result) // 永远不会执行
}, error = > {
    console.log(error) // 永远不会执行
})

未捕获的 reject 会传到末尾,通过 catch 接住

const promiseD = new Promise((resolve, reject) = > { reject('no') }) promiseD.then(result = > { console.log(result) // 永远不会执行 }). catch (error = > { console.log(error) // no })

1
2
3
4
5
6
7
8
9
const promiseD = new Promise((resolve, reject) = > {
    reject('no')
})
promiseD.then(result = > {
    console.log(result) // 永远不会执行
}).
catch (error = > {
    console.log(error) // no
})

resolve 决议会被自动展开(reject 不会)

const promiseE = new Promise((resolve, reject) = > { return new Promise((resolve, reject) = > { resolve('ok') }) }) promiseE.then(result = > { console.log(result) // ok })

1
2
3
4
5
6
7
8
const promiseE = new Promise((resolve, reject) = > {
    return new Promise((resolve, reject) = > {
        resolve('ok')
    })
})
promiseE.then(result = > {
    console.log(result) // ok
})

链式流,then 会返回一个新的 Promise,其状态取决于 then 的返回值。

const promiseF = new Promise((resolve, reject) = > { resolve('ok') }) promiseF.then(result = > { return Promise.reject('error1') }).then(result = > { console.log(result) // 永远不会执行 return Promise.resolve('ok1') // 永远不会执行 }).then(result = > { console.log(result) // 永远不会执行 }). catch (error = > { console.log(error) // error1 })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promiseF = new Promise((resolve, reject) = > {
    resolve('ok')
})
promiseF.then(result = > {
    return Promise.reject('error1')
}).then(result = > {
    console.log(result) // 永远不会执行
    return Promise.resolve('ok1') // 永远不会执行
}).then(result = > {
    console.log(result) // 永远不会执行
}).
catch (error = > {
    console.log(error) // error1
})

        }, 1000);

Async/Await

Promise 看起来有点复杂,所以 ES2017 引进了 asyncawait。虽然只是语法糖,却使 Promise 更加方便,并且可以避免 .then() 链式调用的问题。看下面使用 Promise 的例子:

function connect() { return new Promise((resolve, reject) => { asyncDBconnect('') .then(asyncGetSession) .then(asyncGetUser) .then(asyncLogAccess) .then(result => resolve(result)) .catch(err => reject(err)) }); } // 运行 connect 方法 (自执行方法) (() => { connect(); .then(result => console.log(result)) .catch(err => console.log(err)) })();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function connect() {
 
  return new Promise((resolve, reject) => {
 
    asyncDBconnect('http://localhost:1234')
      .then(asyncGetSession)
      .then(asyncGetUser)
      .then(asyncLogAccess)
      .then(result => resolve(result))
      .catch(err => reject(err))
 
  });
}
 
// 运行 connect 方法 (自执行方法)
(() => {
  connect();
    .then(result => console.log(result))
    .catch(err => console.log(err))
})();

使用 async / await 重写上面的代码:

  1. 外部方法用 async 声明
  2. 基于 Promise 的异步方法用 await 声明,可以确保下一个命令执行前,它已执行完成

async function connect() { try { const connection = await asyncDBconnect(''), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return log; } catch (e) { console.log('error', err); return null; } } // 运行 connect 方法 (自执行异步函数) (async () => { await connect(); })();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function connect() {
 
  try {
    const
      connection = await asyncDBconnect('http://localhost:1234'),
      session = await asyncGetSession(connection),
      user = await asyncGetUser(session),
      log = await asyncLogAccess(user);
 
    return log;
  }
  catch (e) {
    console.log('error', err);
    return null;
  }
 
}
 
// 运行 connect 方法 (自执行异步函数)
(async () => { await connect(); })();

await 使每个异步调用看起来像是同步的,同时不耽误 JavaScript 的单线程处理。此外,async 函数总是返回一个 Promise 对象,因此它可以被其他 async 函数调用。

async / await 可能不会让代码变少,但是有很多优点:

  1. 语法更清晰。括号越来越少,出错的可能性也越来越小。
  2. 调试更容易。可以在任何 await 声明处设置断点。
  3. 错误处理尚佳。try / catch 可以与同步代码使用相同的处理方式。
  4. 支持良好。所有浏览器(除了 IE 和 Opera Mini )和 Node7.6+ 均已实现。

如是说,没有完美的…

4 Promise 异常处理

不仅是 reject,抛出的异常也会被作为拒绝状态被 Promise 捕获。

function fetch(callback) { return new Promise((resolve, reject) = > { throw Error('用户不存在') }) } fetch().then(result = > { console.log('请求处理', result) // 永远不会执行 }). catch (error = > { console.log('请求处理异常', error) // 请求处理异常 用户不存在 })

1
2
3
4
5
6
7
8
9
10
11
12
function fetch(callback) {
    return new Promise((resolve, reject) = > {
        throw Error('用户不存在')
    })
}
fetch().then(result = > {
    console.log('请求处理', result) // 永远不会执行
}).
catch (error = > {
    console.log('请求处理异常', error) // 请求处理异常 用户不存在
})

    })

Promises, Promises

async / await 仍然依赖 Promise 对象,最终依赖回调。你需要理解 Promise 的工作原理,它也并不等同于 Promise.all()Promise.race()。比较容易忽视的是 Promise.all(),这个命令比使用一系列无关的 await 命令更高效。

5 Promise 无法捕获的异常

但是,永远不要在 macrotask 队列中抛出异常,因为 macrotask 队列脱离了运行上下文环境,异常无法被当前作用域捕获。

function fetch(callback) { return new Promise((resolve, reject) = > { setTimeout(() = > { throw Error('用户不存在') }) }) } fetch().then(result = > { console.log('请求处理', result) // 永远不会执行 }). catch (error = > { console.log('请求处理异常', error) // 永远不会执行 }) // 程序崩溃 // Uncaught Error: 用户不存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fetch(callback) {
    return new Promise((resolve, reject) = > {
        setTimeout(() = > {
            throw Error('用户不存在')
        })
    })
}
fetch().then(result = > {
    console.log('请求处理', result) // 永远不会执行
}).
catch (error = > {
    console.log('请求处理异常', error) // 永远不会执行
})
// 程序崩溃
// Uncaught Error: 用户不存在

不过 microtask 中抛出的异常可以被捕获,说明 microtask 队列并没有离开当前作用域,我们通过以下例子来证明:

Promise.resolve(true).then((resolve, reject) = > { throw Error('microtask 中的异常') }). catch (error = > { console.log('捕获异常', error) // 捕获异常 Error: microtask 中的异常 })

1
2
3
4
5
6
Promise.resolve(true).then((resolve, reject) = > {
    throw Error('microtask 中的异常')
}).
catch (error = > {
    console.log('捕获异常', error) // 捕获异常 Error: microtask 中的异常
})

至此,Promise 的异常处理有了比较清晰的答案,只要注意在 macrotask 级别回调中使用 reject,就没有抓不住的异常。

}

同步循环中的异步等待

某些情况下,你想要在同步循环中调用异步函数。例如:

async function process(array) { for (let i of array) { await doSomething(i); } }

1
2
3
4
5
async function process(array) {
  for (let i of array) {
    await doSomething(i);
  }
}

不起作用,下面的代码也一样:

async function process(array) { array.forEach(async i => { await doSomething(i); }); }

1
2
3
4
5
async function process(array) {
  array.forEach(async i => {
    await doSomething(i);
  });
}

循环本身保持同步,并且总是在内部异步操作之前完成。

ES2018 引入异步迭代器,除了 next() 方法返回一个 Promise 对象之外,与常规迭代器类似。因此,await 关键字可以与 for ... of 循环一起使用,以串行方式运行异步操作。例如:

async function process(array) { for await (let i of array) { doSomething(i); } }

1
2
3
4
5
async function process(array) {
  for await (let i of array) {
    doSomething(i);
  }
}

然而,在异步迭代器实现之前,最好的方案是将数组每项 mapasync 函数,并用 Promise.all() 执行它们。例如:

const todo = ['a', 'b', 'c'], alltodo = todo.map(async (v, i) => { console.log('iteration', i); await processSomething(v); }); await Promise.all(alltodo);

1
2
3
4
5
6
7
8
const
  todo = ['a', 'b', 'c'],
  alltodo = todo.map(async (v, i) => {
    console.log('iteration', i);
    await processSomething(v);
});
 
await Promise.all(alltodo);

这样有利于执行并行任务,但是无法将一次迭代结果传递给另一次迭代,并且映射大数组可能会消耗计算性能。

6 Promise 异常追问

如果第三方函数在 macrotask 回调中以 throw Error 的方式抛出异常怎么办?

function thirdFunction() { setTimeout(() = > { throw Error('就是任性') }) } Promise.resolve(true).then((resolve, reject) = > { thirdFunction() }). catch (error = > { console.log('捕获异常', error) }) // 程序崩溃 // Uncaught Error: 就是任性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function thirdFunction() {
    setTimeout(() = > {
        throw Error('就是任性')
    })
}
Promise.resolve(true).then((resolve, reject) = > {
    thirdFunction()
}).
catch (error = > {
    console.log('捕获异常', error)
})
// 程序崩溃
// Uncaught Error: 就是任性

值得欣慰的是,由于不在同一个调用栈,虽然这个异常无法被捕获,但也不会影响当前调用栈的执行。

我们必须正视这个问题,唯一的解决办法,是第三方函数不要做这种傻事,一定要在 macrotask 抛出异常的话,请改为 reject 的方式。

function thirdFunction() { return new Promise((resolve, reject) = > { setTimeout(() = > { reject('收敛一些') }) }) } Promise.resolve(true).then((resolve, reject) = > { return thirdFunction() }). catch (error = > { console.log('捕获异常', error) // 捕获异常 收敛一些 })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function thirdFunction() {
    return new Promise((resolve, reject) = > {
        setTimeout(() = > {
            reject('收敛一些')
        })
    })
}
Promise.resolve(true).then((resolve, reject) = > {
    return thirdFunction()
}).
catch (error = > {
    console.log('捕获异常', error) // 捕获异常 收敛一些
})

请注意,如果 return thirdFunction() 这行缺少了 return 的话,依然无法抓住这个错误,这是因为没有将对方返回的 Promise 传递下去,错误也不会继续传递。

我们发现,这样还不是完美的办法,不但容易忘记 return,而且当同时含有多个第三方函数时,处理方式不太优雅:

function thirdFunction() { return new Promise((resolve, reject) = > { setTimeout(() = > { reject('收敛一些') }) }) } Promise.resolve(true).then((resolve, reject) = > { return thirdFunction().then(() = > { return thirdFunction() }).then(() = > { return thirdFunction() }).then(() = > {}) }). catch (error = > { console.log('捕获异常', error) })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function thirdFunction() {
    return new Promise((resolve, reject) = > {
        setTimeout(() = > {
            reject('收敛一些')
        })
    })
}
Promise.resolve(true).then((resolve, reject) = > {
    return thirdFunction().then(() = > {
        return thirdFunction()
    }).then(() = > {
        return thirdFunction()
    }).then(() = > {})
}).
catch (error = > {
    console.log('捕获异常', error)
})

是的,我们还有更好的处理方式。

// 然后利用async/await来完成代码

丑陋的 try/catch

如果执行失败的 await 没有包裹 try / catchasync 函数将静默退出。如果有一长串异步 await 命令,需要多个 try / catch 包裹。

替代方案是使用高阶函数来捕捉错误,不再需要 try / catch 了(感谢@wesbos的建议):

async function connect() { const connection = await asyncDBconnect(''), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return true; } // 使用高阶函数捕获错误 function catchErrors(fn) { return function (...args) { return fn(...args).catch(err => { console.log('ERROR', err); }); } } (async () => { await catchErrors(connect)(); })();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function connect() {
 
  const
    connection = await asyncDBconnect('http://localhost:1234'),
    session = await asyncGetSession(connection),
    user = await asyncGetUser(session),
    log = await asyncLogAccess(user);
 
  return true;
}
 
// 使用高阶函数捕获错误
function catchErrors(fn) {
  return function (...args) {
    return fn(...args).catch(err => {
      console.log('ERROR', err);
    });
  }
}
 
(async () => {
  await catchErrors(connect)();
})();

当应用必须返回区别于其它的错误时,这种作法就不太实用了。

尽管有一些缺陷,async/await 还是 JavaScript 非常有用的补充。更多资源:

  • MDN async 和 await
  • 异步函数 – 提高 Promise 的易用性
  • TC39 异步函数规范
  • 用异步函数简化异步编码

番外 Generator 基础

generator 是更为优雅的流程控制方式,可以让函数可中断执行:

function* generatorA() { console.log('a') yield console.log('b') } const genA = generatorA() genA.next() // a genA.next() // b

1
2
3
4
5
6
7
8
function* generatorA() {
console.log('a')
yield
console.log('b')
}
const genA = generatorA()
genA.next() // a
genA.next() // b

yield 关键字后面可以包含表达式,表达式会传给 next().value

next() 可以传递参数,参数作为 yield 的返回值。

这些特性足以孕育出伟大的生成器,我们稍后介绍。下面是这个特性的例子:

function * generatorB(count) { console.log(count) const result = yield 5 console.log(result * count) } const genB = generatorB(2) genB.next() // 2 const genBValue = genB.next(7).value // 14 // genBValue undefined

1
2
3
4
5
6
7
8
9
function * generatorB(count) {
    console.log(count)
    const result = yield 5
    console.log(result * count)
}
const genB = generatorB(2)
genB.next() // 2
const genBValue = genB.next(7).value // 14
// genBValue undefined

第一个 next 是没有参数的,因为在执行 generator 函数时,初始值已经传入,第一个 next 的参数没有任何意义,传入也会被丢弃。

const result = yield 5

1
const result = yield 5

这一句,返回值不是想当然的 5。其的作用是将 5 传递给 genB.next(),其值,由下一个 next genB.next(7) 传给了它,所以语句等于 const result = 7

最后一个 genBValue,是最后一个 next 的返回值,这个值,就是函数的 return,显然为 undefined

我们回到这个语句:

const result = yield 5

1
const result = yield 5

如果返回值是 5,是不是就清晰了许多?是的,这种语法就是 await。所以 Async Awaitgenerator 有着莫大的关联,桥梁就是 生成器,我们稍后介绍 生成器

const foo = async () => {

JavaScript 之旅

异步编程是 JavaScript 无法避免的挑战。回调在大多数应用中是必不可少的,但是容易陷入深度嵌套的函数中。

Promise 抽象了回调,但是有许多句法陷阱。转换已有函数可能是一件苦差事,·then() 链式调用看起来很凌乱。

很幸运,async/await 表达清晰。代码看起来是同步的,但是又不独占单个处理线程。它将改变你书写 JavaScript 的方式,甚至让你更赏识 Promise – 如果没接触过的话。

1 赞 收藏 评论

图片 2

番外 Async Await

如果认为 Generator 不太好理解,那 Async Await 绝对是救命稻草,我们看看它们的特征:

const timeOut = (time = 0) = > new Promise((resolve, reject) = > { setTimeout(() = > { resolve(time + 200) }, time) }) async function main() { const result1 = await timeOut(200) console.log(result1) // 400 const result2 = await timeOut(result1) console.log(result2) // 600 const result3 = await timeOut(result2) console.log(result3) // 800 } main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const timeOut = (time = 0) = > new Promise((resolve, reject) = > {
    setTimeout(() = > {
        resolve(time + 200)
    }, time)
})
async
function main() {
    const result1 = await timeOut(200)
    console.log(result1) // 400
    const result2 = await timeOut(result1)
    console.log(result2) // 600
    const result3 = await timeOut(result2)
    console.log(result3) // 800
}
main()

所见即所得,await 后面的表达式被执行,表达式的返回值被返回给了 await 执行处。

但是程序是怎么暂停的呢?只有 generator 可以暂停程序。那么等等,回顾一下 generator 的特性,我们发现它也可以达到这种效果。

    const t = await fn();

番外 async await 是 generator 的语法糖

终于可以介绍 生成器 了!它可以魔法般将下面的 generator 执行成为 await 的效果。

function * main() { const result1 = yield timeOut(200) console.log(result1) const result2 = yield timeOut(result1) console.log(result2) const result3 = yield timeOut(result2) console.log(result3) }

1
2
3
4
5
6
7
8
function * main() {
    const result1 = yield timeOut(200)
    console.log(result1)
    const result2 = yield timeOut(result1)
    console.log(result2)
    const result3 = yield timeOut(result2)
    console.log(result3)
}

下面的代码就是生成器了,生成器并不神秘,它只有一个目的,就是:

所见即所得,yield 后面的表达式被执行,表达式的返回值被返回给了 yield 执行处。

达到这个目标不难,达到了就完成了 await 的功能,就是这么神奇。

function step(generator) { const gen = generator() // 由于其传值,返回步骤交错的特性,记录上一次 yield 传过来的值,在下一个 next 返回过去 let lastValue // 包裹为 Promise,并执行表达式 return () = > Promise.resolve(gen.next(lastValue).value).then(value = > { lastValue = value return lastValue }) }

1
2
3
4
5
6
7
8
9
10
function step(generator) {
    const gen = generator()
    // 由于其传值,返回步骤交错的特性,记录上一次 yield 传过来的值,在下一个 next 返回过去
    let lastValue
    // 包裹为 Promise,并执行表达式
    return () = > Promise.resolve(gen.next(lastValue).value).then(value = > {
        lastValue = value
        return lastValue
    })
}

利用生成器,模拟出 await 的执行效果:

const run = step(main) function recursive(promise) { promise().then(result => { if (result) { recursive(promise) } }) } recursive(run) // 400 // 600 // 800

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const run = step(main)
 
function recursive(promise) {
    promise().then(result => {
        if (result) {
            recursive(promise)
        }
    })
}
 
recursive(run)
// 400
// 600
// 800

可以看出,await 的执行次数由程序自动控制,而回退到 generator 模拟,需要根据条件判断是否已经将函数执行完毕。

    console.log(t);

7 Async Await 异常

不论是同步、异步的异常,await 都不会自动捕获,但好处是可以自动中断函数,我们大可放心编写业务逻辑,而不用担心异步异常后会被执行引发雪崩:

function fetch(callback) { return new Promise((resolve, reject) => { setTimeout(() => { reject() }) }) } async function main() { const result = await fetch() console.log('请求处理', result) // 永远不会执行 } main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fetch(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject()
        })
    })
}
 
async function main() {
    const result = await fetch()
    console.log('请求处理', result) // 永远不会执行
}
 
main()

    console.log('next code');

8 Async Await 捕获异常

我们使用 try catch 捕获异常。

认真阅读 Generator 番外篇的话,就会理解为什么此时异步的异常可以通过 try catch 来捕获。

因为此时的异步其实在一个作用域中,通过 generator 控制执行顺序,所以可以将异步看做同步的代码去编写,包括使用 try catch 捕获异常。

function fetch(callback) { return new Promise((resolve, reject) => { setTimeout(() => { reject('no') }) }) } async function main() { try { const result = await fetch() console.log('请求处理', result) // 永远不会执行 } catch (error) { console.log('异常', error) // 异常 no } } main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function fetch(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('no')
        })
    })
}
 
async function main() {
    try {
        const result = await fetch()
        console.log('请求处理', result) // 永远不会执行
    } catch (error) {
        console.log('异常', error) // 异常 no
    }
}
 
main()

}

9 Async Await 无法捕获的异常

和第五章 Promise 无法捕获的异常 一样,这也是 await 的软肋,不过任然可以通过第六章的方案解决:

function thirdFunction() { return new Promise((resolve, reject) => { setTimeout(() => { reject('收敛一些') }) }) } async function main() { try { const result = await thirdFunction() console.log('请求处理', result) // 永远不会执行 } catch (error) { console.log('异常', error) // 异常 收敛一些 } } main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function thirdFunction() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('收敛一些')
        })
    })
}
 
async function main() {
    try {
        const result = await thirdFunction()
        console.log('请求处理', result) // 永远不会执行
    } catch (error) {
        console.log('异常', error) // 异常 收敛一些
    }
}
 
main()

现在解答第六章尾部的问题,为什么 await 是更加优雅的方案:

async function main() { try { const result1 = await secondFunction() // 如果不抛出异常,后续继续执行 const result2 = await thirdFunction() // 抛出异常 const result3 = await thirdFunction() // 永远不会执行 console.log('请求处理', result) // 永远不会执行 } catch (error) { console.log('异常', error) // 异常 收敛一些 } } main()

1
2
3
4
5
6
7
8
9
10
11
12
async function main() {
    try {
        const result1 = await secondFunction() // 如果不抛出异常,后续继续执行
        const result2 = await thirdFunction() // 抛出异常
        const result3 = await thirdFunction() // 永远不会执行
        console.log('请求处理', result) // 永远不会执行
    } catch (error) {
        console.log('异常', error) // 异常 收敛一些
    }
}
 
main()

foo();

10 业务场景

在如今 action 概念成为标配的时代,我们大可以将所有异常处理收敛到 action 中。

我们以如下业务代码为例,默认不捕获错误的话,错误会一直冒泡到顶层,最后抛出异常。

const successRequest = () => Promise.resolve('a') const failRequest = () => Promise.reject('b') class Action { async successReuqest() { const result = await successRequest() console.log('successReuqest', '处理返回值', result) // successReuqest 处理返回值 a } async failReuqest() { const result = await failRequest() console.log('failReuqest', '处理返回值', result) // 永远不会执行 } async allReuqest() { const result1 = await successRequest() console.log('allReuqest', '处理返回值 success', result1) // allReuqest 处理返回值 success a const result2 = await failRequest() console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行 } } const action = new Action() action.successReuqest() action.failReuqest() action.allReuqest() // 程序崩溃 // Uncaught (in promise) b // Uncaught (in promise) b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
 
class Action {
    async successReuqest() {
        const result = await successRequest()
        console.log('successReuqest', '处理返回值', result) // successReuqest 处理返回值 a
    }
 
    async failReuqest() {
        const result = await failRequest()
        console.log('failReuqest', '处理返回值', result) // 永远不会执行
    }
 
    async allReuqest() {
        const result1 = await successRequest()
        console.log('allReuqest', '处理返回值 success', result1) // allReuqest 处理返回值 success a
        const result2 = await failRequest()
        console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行
    }
}
 
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()
 
// 程序崩溃
// Uncaught (in promise) b
// Uncaught (in promise) b

为了防止程序崩溃,需要业务线在所有 async 函数中包裹 try catch

我们需要一种机制捕获 action 最顶层的错误进行统一处理。

为了补充前置知识,我们再次进入番外话题。

// result:

番外 Decorator

Decorator 中文名是装饰器,核心功能是可以通过外部包装的方式,直接修改类的内部属性。

装饰器按照装饰的位置,分为 class decorator method decorator 以及 property decorator(目前标准尚未支持,通过 get set 模拟实现)。

// 30

Class Decorator

类级别装饰器,修饰整个类,可以读取、修改类中任何属性和方法。

const classDecorator = (target: any) => { const keys = Object.getOwnPropertyNames(target.prototype) console.log('classA keys,', keys) // classA keys ["constructor", "sayName"] } @classDecorator class A { sayName() { console.log('classA ascoders') } } const a = new A() a.sayName() // classA ascoders

1
2
3
4
5
6
7
8
9
10
11
12
13
const classDecorator = (target: any) => {
    const keys = Object.getOwnPropertyNames(target.prototype)
    console.log('classA keys,', keys) // classA keys ["constructor", "sayName"]
}
 
@classDecorator
class A {
    sayName() {
        console.log('classA ascoders')
    }
}
const a = new A()
a.sayName() // classA ascoders

// next code

Method Decorator

方法级别装饰器,修饰某个方法,和类装饰器功能相同,但是能额外获取当前修饰的方法名。

为了发挥这一特点,我们篡改一下修饰的函数。

const methodDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { return { get() { return () => { console.log('classC method override') } } } } class C { @methodDecorator sayName() { console.log('classC ascoders') } } const c = new C() c.sayName() // classC method override

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const methodDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    return {
        get() {
            return () => {
                console.log('classC method override')
            }
        }
    }
}
 
class C {
    @methodDecorator
    sayName() {
        console.log('classC ascoders')
    }
}
const c = new C()
c.sayName() // classC method override

运行这个例子我们可以看出,当在async函数中,运行遇到await时,就会等待await后面的函数运行完毕,而不会直接执行next code。

Property Decorator

属性级别装饰器,修饰某个属性,和类装饰器功能相同,但是能额外获取当前修饰的属性名。

为了发挥这一特点,我们篡改一下修饰的属性值。

const propertyDecorator = (target: any, propertyKey: string | symbol) => { Object.defineProperty(target, propertyKey, { get() { return 'github' }, set(value: any) { return value } }) } class B { @propertyDecorator private name = 'ascoders' sayName() { console.log(`classB ${this.name}`) } } const b = new B() b.sayName() // classB github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const propertyDecorator = (target: any, propertyKey: string | symbol) => {
    Object.defineProperty(target, propertyKey, {
        get() {
            return 'github'
        },
        set(value: any) {
            return value
        }
    })
}
 
class B {
    @propertyDecorator
    private name = 'ascoders'
 
    sayName() {
        console.log(`classB ${this.name}`)
    }
}
const b = new B()
b.sayName() // classB github

11 业务场景 统一异常捕获

我们来编写类级别装饰器,专门捕获 async 函数抛出的异常:

const asyncClass = (errorHandler?: (error?: Error) => void) => (target: any) => { Object.getOwnPropertyNames(target.prototype).forEach(key => { const func = target.prototype[key] target.prototype[key] = async (...args: any[]) => { try { await func.apply(this, args) } catch (error) { errorHandler && errorHandler(error) } } }) return target }

1
2
3
4
5
6
7
8
9
10
11
12
13
const asyncClass = (errorHandler?: (error?: Error) => void) => (target: any) => {
    Object.getOwnPropertyNames(target.prototype).forEach(key => {
        const func = target.prototype[key]
        target.prototype[key] = async (...args: any[]) => {
            try {
                await func.apply(this, args)
            } catch (error) {
                errorHandler && errorHandler(error)
            }
        }
    })
    return target
}

将类所有方法都用 try catch 包裹住,将异常交给业务方统一的 errorHandler 处理:

const successRequest = () => Promise.resolve('a') const failRequest = () => Promise.reject('b') const iAsyncClass = asyncClass(error => { console.log('统一异常处理', error) // 统一异常处理 b }) @iAsyncClass class Action { async successReuqest() { const result = await successRequest() console.log('successReuqest', '处理返回值', result) } async failReuqest() { const result = await failRequest() console.log('failReuqest', '处理返回值', result) // 永远不会执行 } async allReuqest() { const result1 = await successRequest() console.log('allReuqest', '处理返回值 success', result1) const result2 = await failRequest() console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行 } } const action = new Action() action.successReuqest() action.failReuqest() action.allReuqest()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
 
const iAsyncClass = asyncClass(error => {
    console.log('统一异常处理', error) // 统一异常处理 b
})
 
@iAsyncClass
class Action {
    async successReuqest() {
        const result = await successRequest()
        console.log('successReuqest', '处理返回值', result)
    }
 
    async failReuqest() {
        const result = await failRequest()
        console.log('failReuqest', '处理返回值', result) // 永远不会执行
    }
 
    async allReuqest() {
        const result1 = await successRequest()
        console.log('allReuqest', '处理返回值 success', result1)
        const result2 = await failRequest()
        console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行
    }
}
 
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()

我们也可以编写方法级别的异常处理:

const asyncMethod = (errorHandler?: (error?: Error) => void) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { const func = descriptor.value return { get() { return (...args: any[]) => { return Promise.resolve(func.apply(this, args)).catch(error => { errorHandler && errorHandler(error) }) } }, set(newValue: any) { return newValue } } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const asyncMethod = (errorHandler?: (error?: Error) => void) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    const func = descriptor.value
    return {
        get() {
            return (...args: any[]) => {
                return Promise.resolve(func.apply(this, args)).catch(error => {
                    errorHandler && errorHandler(error)
                })
            }
        },
        set(newValue: any) {
            return newValue
        }
    }
}

业务方用法类似,只是装饰器需要放在函数上:

const successRequest = () => Promise.resolve('a') const failRequest = () => Promise.reject('b') const asyncAction = asyncMethod(error => { console.log('统一异常处理', error) // 统一异常处理 b }) class Action { @asyncAction async successReuqest() { const result = await successRequest() console.log('successReuqest', '处理返回值', result) } @asyncAction async failReuqest() { const result = await failRequest() console.log('failReuqest', '处理返回值', result) // 永远不会执行 } @asyncAction async allReuqest() { const result1 = await successRequest() console.log('allReuqest', '处理返回值 success', result1) const result2 = await failRequest() console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行 } } const action = new Action() action.successReuqest() action.failReuqest() action.allReuqest()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
 
const asyncAction = asyncMethod(error => {
    console.log('统一异常处理', error) // 统一异常处理 b
})
 
class Action {
    @asyncAction async successReuqest() {
        const result = await successRequest()
        console.log('successReuqest', '处理返回值', result)
    }
 
    @asyncAction async failReuqest() {
        const result = await failRequest()
        console.log('failReuqest', '处理返回值', result) // 永远不会执行
    }
 
    @asyncAction async allReuqest() {
        const result1 = await successRequest()
        console.log('allReuqest', '处理返回值 success', result1)
        const result2 = await failRequest()
        console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行
    }
}
 
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()

12 业务场景 没有后顾之忧的主动权

我想描述的意思是,在第 11 章这种场景下,业务方是不用担心异常导致的 crash,因为所有异常都会在顶层统一捕获,可能表现为弹出一个提示框,告诉用户请求发送失败。

业务方也不需要判断程序中是否存在异常,而战战兢兢的到处 try catch,因为程序中任何异常都会立刻终止函数的后续执行,不会再引发更恶劣的结果。

像 golang 中异常处理方式,就存在这个问题 通过 err, result := func() 的方式,虽然固定了第一个参数是错误信息,但下一行代码免不了要以 if error {...} 开头,整个程序的业务代码充斥着巨量的不必要错误处理,而大部分时候,我们还要为如何处理这些错误想的焦头烂额。

而 js 异常冒泡的方式,在前端可以用提示框兜底,nodejs端可以返回 500 错误兜底,并立刻中断后续请求代码,等于在所有危险代码身后加了一层隐藏的 return

同时业务方也握有绝对的主动权,比如登录失败后,如果账户不存在,那么直接跳转到注册页,而不是傻瓜的提示用户帐号不存在,可以这样做:

async login(nickname, password) { try { const user = await userService.login(nickname, password) // 跳转到首页,登录失败后不会执行到这,所以不用担心用户看到奇怪的跳转 } catch (error) { if (error.no === -1) { // 跳转到登录页 } else { throw Error(error) // 其他错误不想管,把球继续踢走 } } }

1
2
3
4
5
6
7
8
9
10
11
12
async login(nickname, password) {
    try {
        const user = await userService.login(nickname, password)
        // 跳转到首页,登录失败后不会执行到这,所以不用担心用户看到奇怪的跳转
    } catch (error) {
        if (error.no === -1) {
            // 跳转到登录页
        } else {
            throw Error(error) // 其他错误不想管,把球继续踢走
        }
    }
}

补充

nodejs 端,记得监听全局错误,兜住落网之鱼:

process.on('uncaughtException', (error: any) => { logger.error('uncaughtException', error) }) process.on('unhandledRejection', (error: any) => { logger.error('unhandledRejection', error) })

1
2
3
4
5
6
7
process.on('uncaughtException', (error: any) => {
    logger.error('uncaughtException', error)
})
 
process.on('unhandledRejection', (error: any) => {
    logger.error('unhandledRejection', error)
})

在浏览器端,记得监听 window 全局错误,兜住漏网之鱼:

window.addEventListener('unhandledrejection', (event: any) => { logger.error('unhandledrejection', event) }) window.addEventListener('onrejectionhandled', (event: any) => { logger.error('onrejectionhandled', event) })

1
2
3
4
5
6
window.addEventListener('unhandledrejection', (event: any) => {
    logger.error('unhandledrejection', event)
})
window.addEventListener('onrejectionhandled', (event: any) => {
    logger.error('onrejectionhandled', event)
})

如有错误,欢迎斧正,本人 github 主页: 希望结交有识之士!

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

图片 3 图片 4

2 赞 1 收藏 3 评论

关于作者:ascoders

图片 5

前端小魔法师 个人主页 · 我的文章 · 7

本文由金沙棋牌发布于金沙棋牌官方平台,转载请注明出处:更好的异步解决方案,关于如何解决一个axios调用

关键词: