![微信小游戏开发:后端篇](https://wfqqreader-1252317822.image.myqcloud.com/cover/710/47133710/b_47133710.jpg)
面向Promise编程:异步转同步
在JS开发中,异步转同步是一个很常用也很实用的编程技巧,它可以帮助我们写出优雅、简洁的代码。
启用异步转同步,要用到ES6的async/await语法。以前,在旧的微信开发者工具(版本低于v1.02.1904282)中,如果要使用async/await语法,需要在本地项目配置详情中先勾选“使用npm模块”选项,然后使用指令npm install regenerator安装模块。现在变得简单了,将微信开发者工具更新到v1.02.1904282以上,并在“本地设置”面板中勾选“将JS编译成ES5”选项(在有些旧软件版本中叫“增强编译”)就可以了,如图1-3所示。
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/19_01.jpg?sign=1738856768-G7IJz8pnGCbexfHpDUl6dR8Qp0ucwrsB-0-7d5a98cce6b4121b561b623d5c8b7837)
图1-3 项目的“本地设置”面板
为了方便说明“异步转同步”机制,先看一下异步代码是如何实现的,以及这样实现有什么问题。目前在data_service.js文件中,DataService是通过同步接口实现的,接下来我们改用异步接口实现同样的功能。改写后的代码如代码清单1-3所示。
代码清单1-3 异步接口实现DataService
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/19_02.jpg?sign=1738856768-adqSuSssNIEmnlByy1QRnMEsPd6HOAaD-0-9b2801b08b060cbc9d71715db773109c)
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/20_01.jpg?sign=1738856768-cJ4BlHfhrIf7hIAYYLMlELZXHHS4VSk8-0-2acc2009998b98c54191f74e29bc31b3)
看一下这个文件做了什么。
❑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 测试异步接口实现的数据服务模块
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/21_01.jpg?sign=1738856768-WfjIfa2kWpRxLuUZ7j05M5RzsmWZyEvb-0-a4dfefb5495d5d4c2eb12a57c371c631)
将第13~16行的旧测试代码注释掉,第19~25行是新的测试代码。注意,由于AsyncDataService是异步实现的,连消费代码都变得复杂了:第19行有一个回调函数,第22行又嵌套了一个回调函数。
重新编译测试,项目运行的输出效果与之前是一样的。
好了,现在项目配置已经完成了,我们对JS中的回调恶梦也有了初步认识。接下来看一看如何实现异步转同步,将原来复杂的、嵌套的代码变得简单又清晰。
首先在utils.js文件中实现一个工具方法:
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/21_02.jpg?sign=1738856768-R9AfCmSFAGOAc4T5wzQPPK153FPWEMPN-0-5bf4478251f82891a9bd40e0281dcdcd)
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/22_01.jpg?sign=1738856768-QLhCa6jKKwj0z2JE2fnfg6r6ftkRoXik-0-099c6b64904c8a2e512fc74754c2c478)
这个工具方法要做什么事情呢?
它强制对一个异步接口(asyncApi)的调用返回一个Promise对象。Promise在ES6中是内置类型,不需要任何引入声明就可以直接使用。resolve是代码正常时的回调函数,reject是代码异常时的回调函数,这两个回调函数都是在调用时才被指定的。面向Promise编程是JS编程世界非常了不起的一项发明,它基于一个十分简洁明了的机制,一劳永逸地解决了回调恶梦问题。Promise对象是与ES6的await/async关键字结合起来使用的,稍后我们会看到如何使用它们。
接着在data_service.js文件的新类AsyncToSyncDataService中,基于新创建的工具方法promisify以异步转同步的方式实现与DataService同样的功能,如代码清单1-5所示。
代码清单1-5 应用异步转同步技巧
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/22_02.jpg?sign=1738856768-diDD4TiEmmQRlWyHHpoq5AJk6S1MBo1g-0-86fcfec2033921b465554ba5f7692278)
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/23_01.jpg?sign=1738856768-8gu2TXtPtvSWqVav3p7vaC4Zmh7rzNfh-0-4490e432e983135afaacb8c5ee8bf93c)
这个文件发生了什么变化?
❑第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 消费异步转同步方法实现的数据服务模块
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/23_02.jpg?sign=1738856768-uXN7GoYx29WDQtw744XVZeGlylzLSp7s-0-2a40ad3504278cb4a42083bcb1f859f4)
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/24_01.jpg?sign=1738856768-dpf8KIX9D6syeyy3rgWhjQipAECoFGZO-0-d63af720febfa69cf3a6fdf4f0a9b203)
这个文件有什么变化?
❑第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所示。
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/24_02.jpg?sign=1738856768-22PJVpfnJ4PySWOkJaiOEQ16hXGC8x0G-0-e39d8e67f6d97f5b78b73b6be1e0d6c4)
图1-4 支持Promise风格调用的接口
凡支持Promise风格调用的接口,在接口下方都已注明。wx.setStorage和wx.getStorage都支持Promise风格调用,不必再用promisify方法包装。基于这个发现,我们再改造一下本地数据服务类,如代码清单1-7所示。
代码清单1-7 基于Promise风格改写数据服务模块
![](https://epubservercos.yuewen.com/BDE6C9/26480063801201206/epubprivate/OEBPS/Images/25_01.jpg?sign=1738856768-FACb5gcwnU3Kw6FJXnK7aPGkRE71oODt-0-99c4c82a1e47d80866bd508a3f0139c5)
这个文件有什么变化呢?
相比AsyncToSyncDataService类,只是将第14行、第20行对promisify工具方法的使用删除了,其他代码没有变化。
重新编译测试,项目运行的输出效果与之前是一样的,说明代码没有问题。
最后总结一下在微信小游戏开发中如何实现“异步转同步”。
先看接口,如果接口本身就支持Promise风格调用,直接使用await/async关键字就可以了;如果接口不支持,再使用工具方法promisify。
在微信小程序/小游戏接口尚未支持Promise风格调用的时候,有人写了promisifyAll方法,将wx对象下的所有接口都转换成同步接口,并挂在一个独立的wxp对象下,再将wxp对象挂在全局对象wx下,以方便全局调用。
现在不需要这样做了,篡改全局对象是一个非常不好的编程习惯。在复杂的前端项目中,很多时候我们不知道自己的全局修改对其他人有什么影响,也不清楚其他人的全局修改对我们有什么影响,那些被非法篡改的全局对象就像地雷一样,随时都可能造成意想不到的bug。