Promise、Generator和async的原理
1. 回调的缺陷
想完全的理解Promise,需要从异步的发展开始深究,理解Promise的出现是为了解决什么问题。那么我们从回调开始。
回调的缺点是
- 代码中表达异步的方式不利于开发人员的理解
- 回调最大的问题就是控制反转,如果将回调传入一个第三方的api,那么无法信任这个api的安全性
- 可以发明一些特定逻辑来解决这些信任问题,但是会产生更加笨重、更难维护的代码。
- 回调没有为我们提供一种机制来核实返回类型检查
其中第二点是关于回调编码的信任问题。把一个回调传入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 | let defer = () => { |
4. Promise的局限性
当Promise的错误处理回调函数报错时无法处理。
Promise只有一个完成值或拒绝理由。如果完成值比较复杂,那么需要在每一步进行封装和解封。
5. Generator和自执行器co的原理
假设有下面一个读取文件的生成器:
1 | var fs = require('fs'); |
可以看到readFile返回一个Promise对象。下面让我们手动来实现一个大概的co执行器:
1 | function run(gen){ |
可以看到这个执行器中,首先生成一个迭代器,然后使用迭代器的next方法开始迭代,先判断当前是否迭代完。如果没有迭代完,通过result.value拿到异步函数的Promise对象,然后在.then中调用step,并将这次Promise的返回值传入step,继续执行异步操作之后的内容。直到迭代完返回最后一次迭代的value。
6. async原理
其实一句话async就是Generator的语法糖。上面Generator的例子如果写成async如下:
1 | var asyncReadFile = async function (){ |
一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
async 函数对 Generator 函数的改进,体现在以下三点。
- 内置执行器
Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。 - 更好的语义
async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。 - 更广的适用性
co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
1 | async function fn(args){ |
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。
如何捕获异步错误
定义一个函数将try…catch封装起来,然后在向异步函数中传入被封装过的回调。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function 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);使用window.onerror 监听到了之后统一进行处理
1 |
|