Promise这种模式增强了事件订阅机制,很好地解决了控制反转带来的信任问题、硬编码回调执行顺序造成的“回调金字塔”问题,无疑大大提高了前端开发体验。但有了Promise就能完美地解决异步问题了吗?并没有。
首先,Promise仍然需要通过then方法注册回调,虽然只有一层,但沿着Promise链一长串写下来,还是有些让人头晕。
更大的问题在于Promise的错误处理比较麻烦,因为Promise链中抛出的错误会一直传到链尾,但在链尾捕获的错误却不一定清楚来源。而且,链中抛出的错误会fail掉后面的整个Promise链,如果要在链中及时捕获并处理错误,就需要给每个Promise注册一个错误处理回调。噢,又是一堆回调!
那么最理想的异步写法是怎样的呢?像同步语句那样直观地按顺序执行,却又不会阻塞主线程,最好还能用try-catch直接捕捉抛出的错误。也就是说,“化异步为同步”!
痴心妄想?
异步和同步之间的鸿沟在于:同步语句的执行时机是“现在”,而异步语句的执行时机在“未来”。为了填平鸿沟,如果一个异步操作要写成同步的形式,那么同步代码就必须有“等待”的能力,等到“未来”变成“现在”的那一刻,再继续执行后面的语句。
在不阻塞主线程的前提下,这可能吗?
听起来不太可能。幸好,Generator(生成器)为JS带来了这种超能力!
ES6引入的新特性中,Generator可能是其中最强大也最难理解的之一,即使看了阮一峰老师列举的大量示例代码,知道了它的全部API,也仍是不得要领,这是因为Generator的行为方式突破了我们所熟知的JS运行规则。可一旦掌握了它,它就能赋予我们巨大的能量,极大地提升代码质量、开发效率,以及FEer的幸福指数。
我们先来简单回顾一下,ES6之前的JS运行规则是怎样的呢?
1. JS是单线程执行,只有一个主线程
2. 宿主环境提供了一个事件队列,随着事件被触发,相应的回调函数被放入队列,排队等待执行
3. 函数内的代码从上到下顺序执行;如果遇到函数调用,就先进入被调用的函数执行,待其返回后,用返回值替代函数调用语句,然后继续顺序执行
对于一个FEer来说,日常开发中理解到这个程度已经够用了,直到他尝试使用Generator……
let it = gen(); // 获得迭代器 function request() { ajax({ url: 'www.someurl.com', onSuccess(res){ it.next(res); // 恢复Generator运行,同时向其中塞入异步返回的结果 } }); } function* gen() { let response = yield request(); console.log(response.text); } it.next(); // 启动Generator
注意let respon\= yield request()这行代码,是不是很有同步的感觉?就是这个Feel!
我们来仔细分析下这段代码是如何运行的。首先,最后一行it.next()使得Generator内部的代码从头开始执行,执行到yield语句时,暂停,此时可以把yield想象成return,Generator的栈帧需要被弹出,会先计算yield右边的表达式,即执行request函数调用,以获得用于返回给上一级栈帧的值。当然request函数没有返回值,但它发送了一个异步ajax请求,并注册了一个onSuccess回调,表示在请求返回结果时,恢复Generator的栈帧并继续运行代码,并把结果作为参数塞给Generator,准确地说是塞到yield所在的地方,这样response变量就获得了ajax的返回值。
可以看出,这里yield的功能设计得非常巧妙,好像它可以“赋值”给response。
更妙的是,迭代器不但可以.next,还可以.throw,即把错误也抛入Generator,让后者来处理。也就是说,在Generator里使用try-catch语句捕获异步错误,不再是梦!
先别急着激动,上面的代码还是too young too simple,要真正发挥Generator处理异步的威力,还得结合他的好兄弟——Promise一起上阵。代码如下:
function request() { // 此处的request返回的是一个Promise return new Promise((resolve, reject) => { ajax({ url: 'www.someurl.com', onSuccess(res) { resolve(res); }, onFail(err) { reject(err); } }); }); } let it = gen(); let p = it.next().value; // p是yield返回的Promise p.then(res => it.next(res), err => it.throw(err) // 发生错误时,将错误抛入生成器 ); function* gen() { try { let response = yield request(); console.log(response.text); } catch (error) { console.log('Ooops, ', error.message); // 可以捕获Promise抛进来的错误! } }
这种写法完美结合了Promise和Generator的优点,可以说是FEer们梦寐以求的超级武器。
但聪明的你一定看得出来,这种写法套路非常固定,当Promise对象一多时,就需要写许多类似于p.then(res => ...., err => ...)这样的重复语句,所以人们为了偷懒,就把这种套路给提炼成了一个更加精简的语法,那就是传说中的async/await:
async funtion fetch() { try { let response = await request(); // request定义同上一端段示例代码 console.log(response.text); } catch (error) { console.log('Ooops, ', error.message); } } fetch();
这这这。。。就靠拢同步风格的程度而言,我觉得async/await已经到了登峰造极的地步~
顺便说一句,著名Node.js框架Koa2正是要求中间件使用这种写法,足见其强大和可爱。
前端们,擦亮手中的新锐武器,准备迎接来自异步的高难度挑战吧!