1. 回调的缺陷

想完全的理解Promise,需要从异步的发展开始深究,理解Promise的出现是为了解决什么问题。那么我们从回调开始。
回调的缺点是

  1. 代码中表达异步的方式不利于开发人员的理解
  2. 回调最大的问题就是控制反转,如果将回调传入一个第三方的api,那么无法信任这个api的安全性
  3. 可以发明一些特定逻辑来解决这些信任问题,但是会产生更加笨重、更难维护的代码。
  4. 回调没有为我们提供一种机制来核实返回类型检查
    其中第二点是关于回调编码的信任问题。把一个回调传入api可能出现以下问题:
  • 调用回调过早
  • 调用回调过晚
  • 调用回调次数过多或过少
  • 未能传递所需的环境和参数
  • 吞掉可能出现的错误和异常
    为了更加优雅的处理错误,有些api提供了分离回调:api(…args, success, failure);。以及node中的error-first风格:回调的第一个参数保留用作错误对象,然而使用这种方法也没有解决多次调用的问题,反而还需要我们在error和sucess两种情况都进行判断处理。
    为了解决由同步异步行为引起的不确定性,提出了永远异步调用回调,这样所有回调都是可预测的异步回调了。

2. Promise针对回调缺陷的改进

回调需要被传入api中,由别的api进行控制调用,而更好的方法则是api返回一个类似监听器的对象,由程序控制监听api的执行完成情况和执行结果即成功或失败。对控制反转的恢复实现了更好的关注点分离。其中类似监听器的对象就是Promise的一个模拟。
Promise的一个好处是将控制返还给调用代码。
Promise通过以下几种方式奠定了自己可信任的基础:

  • 只提供异步调用
  • 如果回调出错,Promise永远不会决议,如何捕获错误呢?Promise中有一种称为竞争的高级抽象机制。
  • Promise只会接受一次决议,不会因为多次调用决议而出现问题
  • 通过使用catch或是then的第二个onRejected回调来放置异常被吞掉
  • 通过Promise.resolve返回一个可信任的Promise
    因此Promise通过把控制返还给调用代码,并且将控制权放在一个可信任的系统中,使异步编码更清晰。

3. promise的原理

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
let defer = () => {
let pending = [],value;
return {
resolve(_value){
if(pending){
value = _value
for(let i = 0;i < pending.length; i++){
pending[i](value)
}
pending = undefined;
}else{
throw new Error("A promise can only be resolved once.")
}
},
promise: {
then (callback) {
if (pending) {
pending.push(callback);
} else {
callback(value);
}
}
}
}
}

4. Promise的局限性

当Promise的错误处理回调函数报错时无法处理。
Promise只有一个完成值或拒绝理由。如果完成值比较复杂,那么需要在每一步进行封装和解封。

5. Generator和自执行器co的原理

假设有下面一个读取文件的生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};

var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

可以看到readFile返回一个Promise对象。下面让我们手动来实现一个大概的co执行器:

1
2
3
4
5
6
7
8
9
10
11
12
function run(gen){
var g = gen(); //生成一个迭代器
function step(data) {
var result = g.next(data);
if(result.done) return result.value;
result.value.then(function(data){
step(data);
});
}
step();
}
run(gen);

可以看到这个执行器中,首先生成一个迭代器,然后使用迭代器的next方法开始迭代,先判断当前是否迭代完。如果没有迭代完,通过result.value拿到异步函数的Promise对象,然后在.then中调用step,并将这次Promise的返回值传入step,继续执行异步操作之后的内容。直到迭代完返回最后一次迭代的value。

6. async原理

其实一句话async就是Generator的语法糖。上面Generator的例子如果写成async如下:

1
2
3
4
5
6
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
async 函数对 Generator 函数的改进,体现在以下三点。

  1. 内置执行器
    Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
  2. 更好的语义
    async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
  3. 更广的适用性
    co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
    async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
1
2
3
4
5
6
7
8
9
10
11
async function fn(args){
// ...
}

// 等同于

function fn(args){
return run(function*() {
// ...
});
}

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。

如何捕获异步错误

  1. 定义一个函数将try…catch封装起来,然后在向异步函数中传入被封装过的回调。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function tryCatchWrap(fn){
    return function(){
    try{
    fn();
    }catch(err){
    console.log(err);
    }
    }
    }
    function test() {
    console.log('before');
    throw Error('error');
    console.log('after');
    }
    setTimeout(tryCatchWrapper(test) ,1000);
  2. 使用window.onerror 监听到了之后统一进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
window.onerror = function(err) {
console.log('window error', err);
};
</script>
<script>
setTimeout(() => {
throw Error('async error');
}, 3000);
throw Error('sync error');
</script>
</body>
</html>

参考:
https://juejin.im/entry/599968f6518825244630f809