面向Promise编程:异步转同步
在JS开发中,异步转同步是一个很常用也很实用的编程技巧,它可以帮助我们写出优雅、简洁的代码。
启用异步转同步,要用到ES6的async/await语法。以前,在旧的微信开发者工具(版本低于v1.02.1904282)中,如果要使用async/await语法,需要在本地项目配置详情中先勾选“使用npm模块”选项,然后使用指令npm install regenerator安装模块。现在变得简单了,将微信开发者工具更新到v1.02.1904282以上,并在“本地设置”面板中勾选“将JS编译成ES5”选项(在有些旧软件版本中叫“增强编译”)就可以了,如图1-3所示。
图1-3 项目的“本地设置”面板
为了方便说明“异步转同步”机制,先看一下异步代码是如何实现的,以及这样实现有什么问题。目前在data_service.js文件中,DataService是通过同步接口实现的,接下来我们改用异步接口实现同样的功能。改写后的代码如代码清单1-3所示。
代码清单1-3 异步接口实现DataService
看一下这个文件做了什么。
❑AsyncDataService是一个以异步接口实现的本地数据服务类,第14行与第25行调用的都是微信小游戏的异步接口(wx.setStorage和wx.getStorage)。
❑第7行、第24行中的writeLocalData和readLocalData这两个方法,与原来DataService类的同名方法实现的功能是一样的,但是因为我们是基于异步接口实现的,所以必须在参数中都加上一个callback函数,以便向消费代码传递异步回调结果。
❑由于必须先拉取到本地缓存对象(localScoreData),然后才能在这个对象上添加新的数据,所以第9~19行代码必须放在readLocalData方法的回调函数内。
❑第17行、第18行在回调函数中又有对回调函数的设置,幸好箭头函数可以让回调函数看起来简单一些,不然回调函数一层层嵌套下去,非常不利于代码的阅读与维护。
❑第34行,只需要将新创建的类的实例导出就可以了,对于旧的导出代码,注释掉即可。注意,新类AsyncDataService的实现方法与DataService是相同的,这样可以减少对消费代码的改动。
有人说:“回调函数是JS编程的恶梦。”确实,这句话在某些场景下没有错。只有经历过回调恶梦的人,才能深刻体会异步转同步的好处。
AsyncDataService类创建完了,它能不能正常工作呢?我们看一下测试代码,如代码清单1-4所示。
代码清单1-4 测试异步接口实现的数据服务模块
将第13~16行的旧测试代码注释掉,第19~25行是新的测试代码。注意,由于AsyncDataService是异步实现的,连消费代码都变得复杂了:第19行有一个回调函数,第22行又嵌套了一个回调函数。
重新编译测试,项目运行的输出效果与之前是一样的。
好了,现在项目配置已经完成了,我们对JS中的回调恶梦也有了初步认识。接下来看一看如何实现异步转同步,将原来复杂的、嵌套的代码变得简单又清晰。
首先在utils.js文件中实现一个工具方法:
这个工具方法要做什么事情呢?
它强制对一个异步接口(asyncApi)的调用返回一个Promise对象。Promise在ES6中是内置类型,不需要任何引入声明就可以直接使用。resolve是代码正常时的回调函数,reject是代码异常时的回调函数,这两个回调函数都是在调用时才被指定的。面向Promise编程是JS编程世界非常了不起的一项发明,它基于一个十分简洁明了的机制,一劳永逸地解决了回调恶梦问题。Promise对象是与ES6的await/async关键字结合起来使用的,稍后我们会看到如何使用它们。
接着在data_service.js文件的新类AsyncToSyncDataService中,基于新创建的工具方法promisify以异步转同步的方式实现与DataService同样的功能,如代码清单1-5所示。
代码清单1-5 应用异步转同步技巧
这个文件发生了什么变化?
❑第8行中的AsyncToSyncDataService是使用异步接口实现的本地数据服务类。在DataService类中,我们使用的wx.setStorageSync、wx.getStorageSync是同步接口;在新类(AsyncDataService)中,我们改用异步接口wx.setStorage、wx.getStorage。
❑第24行表示如果能够取到res,还要进一步取它的data,因为小程序/小游戏的异步接口在返回结果对象时又多包裹了一层。结果对象的数据格式为{data, errMsg},其中data才是真正的数据。
❑第17行和第23行都有一个await,这是ES6的关键字,代表等待一个Promise对象的resolve回调结果。await与async总是成对出现,如果方法内使用了await关键字,则必须在方法前面(第10行、第22行)加上async,代表这个方法内部使用了异步转同步代码。
❑第17行和第23行后面都有一个catch,即catch(console.log),这是为了接管Promise的reject回调,以防止程序出错。当有错误发生,即res为undefined时,可以继续向下执行,不会影响程序的正常运行。这是一种方便运用的容错机制。
示例讲解完了,现在总结一下什么叫“异步转同步”。
第17行、第23行就是异步转同步代码。由于使用了await关键字和Promise对象,代码不需要写then回调,也不需要使用回调函数(callback),这使得异步代码可以像同步代码一样,自上而下依次书写,没有恼人的层层嵌套。这就是“异步转同步”。
data_service.js文件的修改完成以后,消费代码也需要做一点点修改,如代码清单1-6所示。
代码清单1-6 消费异步转同步方法实现的数据服务模块
这个文件有什么变化?
❑第25行,在调用一个声明了async的方法时,如果不加await,取到的是一个Promise对象;加了await,取到的才是我们真正想要的数据结果。这一点很重要,一定要记好。
❑第23行,如果不加await,程序执行到这里时不会停留,会继续向下执行,这会导致在25行取到的结果不包括第23行新添加的数据。
❑第10行,在方法前要加上async关键字,因为函数内使用了await。
重新编译测试,项目运行的输出效果与之前是一样的。
除了使用自定义的工具方法promisify之外,其实还有一种更简单的改进本地数据服务类的方法。
早期微信小程序/小游戏接口都不支持Promise调用,但目前大部分接口已经实现了Promise化。以我们使用的wx.setStorage接口为例,接口文档地址为https://developers.weixin.qq.com/minigame/dev/api/storage/wx.setStorage.html。接口文档截图如图1-4所示。
图1-4 支持Promise风格调用的接口
凡支持Promise风格调用的接口,在接口下方都已注明。wx.setStorage和wx.getStorage都支持Promise风格调用,不必再用promisify方法包装。基于这个发现,我们再改造一下本地数据服务类,如代码清单1-7所示。
代码清单1-7 基于Promise风格改写数据服务模块
这个文件有什么变化呢?
相比AsyncToSyncDataService类,只是将第14行、第20行对promisify工具方法的使用删除了,其他代码没有变化。
重新编译测试,项目运行的输出效果与之前是一样的,说明代码没有问题。
最后总结一下在微信小游戏开发中如何实现“异步转同步”。
先看接口,如果接口本身就支持Promise风格调用,直接使用await/async关键字就可以了;如果接口不支持,再使用工具方法promisify。
在微信小程序/小游戏接口尚未支持Promise风格调用的时候,有人写了promisifyAll方法,将wx对象下的所有接口都转换成同步接口,并挂在一个独立的wxp对象下,再将wxp对象挂在全局对象wx下,以方便全局调用。
现在不需要这样做了,篡改全局对象是一个非常不好的编程习惯。在复杂的前端项目中,很多时候我们不知道自己的全局修改对其他人有什么影响,也不清楚其他人的全局修改对我们有什么影响,那些被非法篡改的全局对象就像地雷一样,随时都可能造成意想不到的bug。