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

2019-10-13 04:23 来源:未知

回调地狱

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

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 调用。

Await

我们创建了 promise 但不能同步等待它执行完成。我们只能通过 then 传一个回调函数。不允许等待 promise 是为了鼓励开发非阻塞代码。否则,开发者们总会忍不住执行阻塞操作,因为那比使用 promise 和回调更简单。

然而,为了让 promise 能同步执行,我们需要让他们等待彼此完成。换句话说,如果一个操作是异步的(即封装在 promise 中),它应该能够等待另一个异步操作执行完。但是 JavaScript 解释器怎么能知道一个操作是否在 promise 中运行呢?

答案就在 async 这个关键词中。每个 async 方法都返回一个 promise 对象。因此,JavaScript 解释器就明白所有 async 方法中的操作都被封装在 promise 里异步执行。所以解释器能够允许它们等待其他 promise 执行完。

下面引入 await 关键词。它只能被用在 async 方法中,让我们能同步等待 promise 执行完。如果在 async 函数外使用 promise, 我们仍然需要用 then 回调函数:

async function f(){ // response 就是 promise 执行成功的值 const response = await rp(''); console.log(response); } // 不能在 async 方法外面用 await // 需要使用 then 回调函数…… f().then(() => console.log('Finished'));

1
2
3
4
5
6
7
8
9
async function f(){
    // response 就是 promise 执行成功的值
    const response = await rp('http://example.com/');
    console.log(response);
}
 
// 不能在 async 方法外面用 await
// 需要使用 then 回调函数……
f().then(() => console.log('Finished'));

现在我们来看如何解决上一节的问题:

// 将解决方法封装到 async 函数中 async function solution() { // 等待第一个 HTTP 请求并打印出结果 console.log(await rp('')); // 创建两个 HTTP 请求,不等它们执行完 —— 让他们同时执行 const call2Promise = rp(''); // Does not wait! const call3Promise = rp(''); // Does not wait! // 创建完以后 —— 等待它们都执行完 const response2 = await call2Promise; const response3 = await call3Promise; console.log(response2); console.log(response3); } // 调用这一 async 函数 solution().then(() => console.log('Finished'));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 将解决方法封装到 async 函数中
async function solution() {
  
    // 等待第一个 HTTP 请求并打印出结果
    console.log(await rp('http://example.com/'));
 
  
    // 创建两个 HTTP 请求,不等它们执行完 —— 让他们同时执行
    const call2Promise = rp('http://example.com/');  // Does not wait!
    const call3Promise = rp('http://example.com/');  // Does not wait!
 
    
    // 创建完以后 —— 等待它们都执行完
    const response2 = await call2Promise;
    const response3 = await call3Promise;
 
    console.log(response2);
    console.log(response3);
}
 
 
// 调用这一 async 函数
solution().then(() => console.log('Finished'));

上面这段代码中,我们把解决方法封装到 async 函数中。这让我们能直接对里面的 promise 使用 await 关键字,所以不再需要使用 then 回调函数。最后,调用这个 async 函数,它简单地创建了一个 promise 对象, 这个 promise 封装了调用其他 promise 的逻辑。

当然,在第一个例子(没有用 async / await)中,两个 promise会被同时触发。这段代码也一样(7-8 行)。注意,直到第 11-12 行我们才使用 await, 将程序一直阻塞到两个 promise 执行完成。然后我们就能断定上例中两个 promise 都成功执行了(和使用 Promise.all(…).then(…) 类似)。

这背后的计算过程跟上一节给出的基本相当。但是代码可读性更强、更易于理解。

实际上,async / await 在底层转换成了 promise 和 then 回调函数。也就是说,这是使用 promise 的语法糖。每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。

我们再看看下面的例子:

async function f() { console.log('Starting F'); const result = await rp(''); console.log(result); }

1
2
3
4
5
async function f() {
    console.log('Starting F');
    const result = await rp('http://example.com/');
    console.log(result);
}

下面给出了函数 f 底层运算过程。由于 f 是 async 的,所以它会跟它的调用方同时执行:

彩民之家高手论坛 1

Await 的计算过程。

函数 f 开始运行并创建了一个 promise 对象。就在那一刻,函数中剩下的部分被封装到一个回调函数中,并在 promise 结束后执行。

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

使用 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 彩民之家高手论坛 3

1 赞 3 收藏 评论

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

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);

简介

JavaScript ES7 中的 async / await 让多个异步 promise 协同工作起来更容易。如果要按一定顺序从多个数据库或者 API 异步获取数据,你可能会以一堆乱七八糟的 promise 和回调函数而告终。而 async / await 结构让我们能用可读性强、易维护的代码更加简洁地实现这些逻辑。

本教程用图表和简单示例讲解了 JavaScript 中 async / await 的语法和语义。

在深入之前,我们先简单回顾一下 promise. 如果你已经对 JS 的 promise 有所了解,可放心大胆地跳过这一部分。

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()

同步循环中的异步等待

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

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);

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

讨论

Async / await 是让 promise 更完美的语言结构。它让我们能用更少的代码使用 promise. 然而,async / await 并没有取代普通 promise. 例如,如果在普通函数中或者全局范围内调用 async 函数,我们就没办法使用 await 而要依赖于普通 promise:

async function fAsync() { // actual return value is Promise.resolve(5) return 5; } // can't call "await fAsync()". Need to use then/catch fAsync().then(r => console.log(`result is ${r}`));

1
2
3
4
5
6
7
async function fAsync() {
    // actual return value is Promise.resolve(5)
    return 5;
}
 
// can't call "await fAsync()". Need to use then/catch
fAsync().then(r => console.log(`result is ${r}`));

我通常会将大部分异步逻辑封装到一个或者几个 async 函数中,然后在非异步代码中调用。这让我尽可能少地写 try / catch 回调。

Async / await 结构是让使用 promise 更简练的语法糖。每一个 async / await 结构都可以写成普通 promise. 归根结底,这是一个编码风格和简洁的问题。

关于说明并发并行有区别的资料,可以查看 Rob Pike 关于这个问题的讨论,或者我这篇文章。并发是指将独立进程(通常意义上的进程)组合在一起工作,而并行是指真正同时处理多个任务。并发关乎应用设计和架构,而并行关乎实实在在的执行。

我们拿一个多线程应用来举例。应用程序分离成线程明确了它的并发模型。这些线程在可用内核上的映射定义了其级别或并行性。并发系统可以在单个处理器上高效运行,在这种情况下,它并不是并行的。

彩民之家高手论坛 4

并发VS并行

从这个意义上说,promise 让我们能够将程序分解成并发模块,这些模块可能会也可能不会并行执行。Javascript 实际否并行执行取决于具体实现方法。例如,Node JS 是单线程的,如果 promise 是计算密集型(CPU bound)那就不会有并行处理。但是,如果你用 Nashorn 之类的东西把代码编译成 java 字节码,理论上可能能够将计算密集型的 promise 映射到不同 CPU 核上,从而达到并行效果。所以我认为,promise(不管是普通的还是用了 async / await 的)组成了 JavaScript 应用的并发模块。

打赏支持我翻译更多好文章,谢谢!

打赏译者

番外 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
})

现代 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);

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

问题来了——组合 promise

只用一个 promise 很容易搞定。但是,当需要针对复杂异步逻辑编程时,我们很可能最后要同时用好几个 promise 对象。写一堆 then 语句和匿名回调很容易搞得难以控制。

例如,假设我们需要编程解决如下需求:

  1. 创建 HTTP 请求,等待请求结束并打印出结果;
  2. 再创建两个并行 HTTP 请求;
  3. 等这两个请求结束后,打印出它们的结果。

下面这段代码示范了如何解决此问题:

// 第一次调用 const call1Promise = rp(''); call1Promise.then(result1 => { // 第一个请求完成后会执行 console.log(result1); const call2Promise = rp(''); const call3Promise = rp(''); return Promise.all([call2Promise, call3Promise]); }).then(arr => { // 两个 promise 都结束后会执行 console.log(arr[0]); console.log(arr[1]); })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 第一次调用
const call1Promise = rp('http://example.com/');
 
call1Promise.then(result1 => {
    // 第一个请求完成后会执行
    console.log(result1);
    const call2Promise = rp('http://example.com/');
    const call3Promise = rp('http://example.com/');
 
    return Promise.all([call2Promise, call3Promise]);
}).then(arr => {
    // 两个 promise 都结束后会执行
    console.log(arr[0]);
    console.log(arr[1]);
})

我们开头创建了第一个 HTTP 请求,并且加了个完成时候运行的回调(1-3行)。在这个回调函数里,我们为随后的 HTTP 请求创建了另外两个 promise(8-9行)。这两个 promise 同时执行,我们需要加一个能等它们都完成后才执行的回调函数。因此,我们需要用 Promise.all 将它们组合到同一个 promise 中(11 行),它们都结束后这个 promise 才算完成。这个回调返回的是 promise 对象,所以我们要再加一个 then 回调函数来打印结果(12-16行)。

下图描述了这一计算流程:

彩民之家高手论坛 5

Promise 组合的计算过程。我们用  Promise.all 将两个并行的 promise 组合到一个 promise 中。

对于这个简单的例子,我们最后用了两个 then 回调方法,并且不得不用 Promise.all 来让两个并行的 promise 同时执行。如果我们必须执行更多异步操作,或者加上错误处理会怎么样呢?这种方法最后很容易产生一堆乱七八糟的 then, Promise.all 和回调函数。

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

异步链式调用

任何返回 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(() => {
    // 清理操作放这儿!
  });
}

Async 方法

Async 是定义返回 promise 对象函数的快捷方法。

例如,下面这两种定义是等价的:

function f() { return Promise.resolve('TEST'); } // asyncF 和 f 是等价的 async function asyncF() { return 'TEST'; }

1
2
3
4
5
6
7
8
function f() {
    return Promise.resolve('TEST');
}
 
// asyncF 和 f 是等价的
async function asyncF() {
    return 'TEST';
}

类似地,抛出异常的 async 方法等价于返回拒绝 promise 的方法:

function f() { return Promise.reject('Error'); } // asyncF 和 f 是等价的 async function asyncF() { throw 'Error'; }

1
2
3
4
5
6
7
8
function f() {
    return Promise.reject('Error');
}
 
// asyncF 和 f 是等价的
async function asyncF() {
    throw 'Error';
}

番外 Decorator

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

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

Promises, Promises

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

关于作者:王浩

彩民之家高手论坛 6

phper @深圳 个人主页 · 我的文章 · 13 ·  

彩民之家高手论坛 7

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

通过回调实现异步

单线程产生了一个问题。当 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

Promises

在 JavaScript 中,promise 代表非阻塞异步执行的抽象概念。如果你熟悉Java 的 Future、C# 的 Task.aspx), 你会发现 promise 跟它们很像。

Promise 一般用于网络和 I/O 操作,比如读取文件,或者创建 HTTP 请求。我们可以创建异步 promise,然后用 then 添加一个回调函数,当 promise 结束后会触发这个回调函数,而非阻塞住当前“线程”。回调函数本身也可以返回一个 promise 对象,所以我们能够链式调用 promise。

为了简单起见,我们假设后面所有示例都已经像这样安装并加载了 request-promise 类库:

var rp = require('request-promise');

1
var rp = require('request-promise');

现在我们就可以像这样创建一个返回 promise 对象的简易 HTTP GET 请求:

const promise = rp('')

1
const promise = rp('http://example.com/')

我们现在来看个例子:

console.log('Starting Execution'); const promise = rp(''); promise.then(result => console.log(result)); console.log("Can't know if promise has finished yet...");

1
2
3
4
5
6
console.log('Starting Execution');
 
const promise = rp('http://example.com/');
promise.then(result => console.log(result));
 
console.log("Can't know if promise has finished yet...");

我们在第3行创建了一个 promise 对象,在第4行给它加了个回调函数。Promise 是异步的,所以当执行到第6行时,我们并不知道 promise 是否已完成。如果把段这代码多执行几次,可能每次都得到不同的结果。一般地说,就是 promise 创建后的代码和 promise 是同时运行的。

直到 promise 执行完,才有办法阻塞当前操作序列。这不同于 Java 的 Future.get, 它让我们能够在 Future 结束之前就阻塞当前线程。对于 JavaScript,我们没法等待 promise 执行完。在 promise 后面编排代码的唯一方法是用 then 给它添加回调函数。

下图描述了本例的计算过程:

彩民之家高手论坛 8

Promise 的计算过程。正在执行的“线程”无法等待 promise 执行完成。在 promise 后面编排代码的唯一方法是用 then 给它添加回调函数。

通过 then 添加的回调函数只有当 promise 成功时才会执行。如果它失败了(比如由于网络错误),回调函数不会执行。你可以用 catch 再附加一个回调函数来处理失败的 promise:

rp(''). then(() => console.log('Success')). catch(e => console.log(`Failed: ${e}`))

1
2
3
rp('http://example.com/').
    then(() => console.log('Success')).
    catch(e => console.log(`Failed: ${e}`))

最后,为了测试,我们可以用 Promise.resolve 和 Promise.reject 很容易地创建执行成功或失败的“傻瓜” promise:

const success = Promise.resolve('Resolved'); // 打印 "Successful result: Resolved" success. then(result => console.log(`Successful result: ${result}`)). catch(e => console.log(`Failed with: ${e}`)) const fail = Promise.reject('Err'); // 打印 "Failed with: Err" fail. then(result => console.log(`Successful result: ${result}`)). catch(e => console.log(`Failed with: ${e}`))

1
2
3
4
5
6
7
8
9
10
11
12
const success = Promise.resolve('Resolved');
// 打印 "Successful result: Resolved"
success.
    then(result => console.log(`Successful result: ${result}`)).
    catch(e => console.log(`Failed with: ${e}`))
 
 
const fail = Promise.reject('Err');
// 打印 "Failed with: Err"
fail.
    then(result => console.log(`Successful result: ${result}`)).
    catch(e => console.log(`Failed with: ${e}`))

想要更详细的 promise 教程,可以参考这篇文章。

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)
})

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

丑陋的 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 异步函数规范
  • 用异步函数简化异步编码

错误处理

前面大部分例子中,我们都假设 promise 执行成功。因此在 promise 上使用 await 会返回值。如果我们进行 await 的 promise 失败了,async 函数就会发生异常。我们可以用标准的 try / catch 来处理这种情况:

async function f() { try { const promiseResult = await Promise.reject('Error'); } catch (e){ console.log(e); } }

1
2
3
4
5
6
7
async function f() {
    try {
        const promiseResult = await Promise.reject('Error');
    } catch (e){
        console.log(e);
    }
}

Async 函数不会处理异常,不管异常是由拒绝的 promise 还是其他 bug 引起的,它都会返回一个拒绝 promise:

async function f() { // Throws an exception const promiseResult = await Promise.reject('Error'); } // Will print "Error" f(). then(() => console.log('Success')). catch(err => console.log(err)) async function g() { throw "Error"; } // Will print "Error" g(). then(() => console.log('Success')). catch(err => console.log(err))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function f() {
    // Throws an exception
    const promiseResult = await Promise.reject('Error');
}
 
// Will print "Error"
f().
    then(() => console.log('Success')).
    catch(err => console.log(err))
 
async function g() {
    throw "Error";
}
 
// Will print "Error"
g().
    then(() => console.log('Success')).
    catch(err => console.log(err))

这让我们能得心应手地通过熟悉的异常处理机制来处理拒绝的 promise.

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

任选一种支付方式

彩民之家高手论坛 9 彩民之家高手论坛 10

2 赞 1 收藏 3 评论

JavaScript 之旅

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

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

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

1 赞 收藏 评论

彩民之家高手论坛 11

用图表和实例解释 Await 和 Async

2018/08/13 · JavaScript · async, await, Promise, 异步

本文由 伯乐在线 - 王浩 翻译,艾凌风 校稿。未经许可,禁止转载!
英文出处:Nikolay Grozev。欢迎加入翻译组。

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

前途光明吗?

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

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

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

延伸阅读:

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

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

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 均已实现。

如是说,没有完美的…

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 最顶层的错误进行统一处理。

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

使用 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);
  });

番外 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 模拟,需要根据条件判断是否已经将函数执行完毕。

单线程处理程序

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

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

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

补充

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 主页: 希望结交有识之士!

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

打赏作者

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: 请求失败

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('请求处理') // 永远不会执行
})

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('请求处理') // 永远不会执行
})

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()

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()

关于作者:ascoders

彩民之家高手论坛 12

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

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

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 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 的特性,我们发现它也可以达到这种效果。

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) // 其他错误不想管,把球继续踢走
        }
    }
}

番外 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 有着莫大的关联,桥梁就是 生成器,我们稍后介绍 生成器

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

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) // 请求处理异常 用户不存在
})

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()
TAG标签: JavaScript ES6
版权声明:本文由彩民之家高手论坛发布于前端知识,转载请注明出处:现代 JS 流程控制:从回调函数到 Promises 再到 A