收藏
评论

使用异步转同步的编程范式官方


你好,我是李艺。

上节课我们主要学习了如何在小程序里面使用WebAssembly技术,这节课学习异步转同步的编程范式。

首先我们看一个问题,JS它作为一门应用最广泛的前端编程语言,主要有两大特点,第一个特点是单线程,第二个特点是异步编程。对于第一点单线程很好理解,不是JS不能支持多线程,而是JS在诞生的时候它本身就是作为HTML页面的交互语言而存在的,人类的屏幕交互本身就是一个单线程的,这要求JS也必须是单线程的,如果是在一个页面之上同时有多个交互控件需要人类去处理,那么这个用户他能处理过来吗,显然这可能是为章鱼而设计的对吧,不是为人类而设计的。

至于第二点异步编程,这是JS语言区别于Go语言、C、C++、Java等后端编程语言最主要的一个特征,在后端编程语言里边如果我们要读取文件或者是要发起一个网络请求,必须要等待这个结果返回,返回以后才可以进一步执行下面的一个代码,在编程里面这叫做同步开发 JS不一样,除了主线程以外还有一个或者多个异步线程,异步线程处理worker timer定时器、网络请求、用户输入监听、事件派发等等这些任务,当异步线程有回调函数代码需要执行的时候,异步代码它将这些代码推入到主线程的执行队列里面去,由主线程在不是很忙碌的时候尽快将这些代码进行执行,这种方式就叫做异步编程。

在异步编程里面如果有后续逻辑是依赖前面这个代码请求成功的前提才可以执行的,这些代码将写在回调函数里面,如果后续又有异步线程需要执行,还要进一步的内嵌,这种方式就使得我们代码可读性非常差,当然有经验的程序员可能会讲,你可以另外再创建一个函数,然后在这里去调用它,没有必要一定要把这个代码写成俄罗斯套娃的这样一种形式,这样当然也可以。但是另外定义函数上下交织进行调用,这又会破坏我们整体上代码线性逻辑的一个连续性,可读性又不能得到根本性的一个保证,计算机CPU的工作模式是多核并发的,但本质上人类的思维却是线性的,就像我们阅读小说一样,我们没有办法同时阅读多页或者是同时阅读多本书,这样也是不可以的,上下连贯的连续的逻辑更容易被人所理解和接受,为了解决异步线程在多层异步代码嵌套情况下带来的代码可读性差这个问题,我们可以使用异步转同步编程技巧,它依赖于ES6的async/await一套语法而实现,它本身是异步的,但是却可以以同步的方式编写和呈现,它既有后端同步编程的表又兼具我们前端异步编程的里,是一个非常不错的通用的代码优化技巧之一,下面我们看项目实践。


首先看实践一,编写和实现异步转同步编程范式的这个工具函数。

首先我们需要创建一个promisify的JS文件,这个文件里面会导出一个promisify函数,这个函数它最大的一个作用就是将异步请求的wx Api转化为可以同步调用的方法,小程序里面大多数接口都已经支持了Promise风格调用,凡是支持的在官方文档上都有标注,但是目前仍有少部分接口它是不支持的,不管是支持还是不支持,我们都可以用这个工具函数进行转化。在这个地方我们说一下,自定义的全局的require方法到底能不能使用,前面我们提到过这个问题,在使用require关键字引入模块的时候,有时候我们想使用绝对路径以保持在各个纵深层次文件里面引用同一个文件时的一个路径一致性,这时候我们可以在app.js文件里面,在这个App实例上定义一个替代的require方法,这样的一个require方法不仅可以用于分包异步化的一个加载,还可以用于普通的CommonJS模块加载,并且保持路径的写法一致性,好处实在是多多,但是有一点我们需要特别注意,替代的require方法使用了路径变量,这可能会给我们项目带来隐藏的麻烦,在微信小程序里面,使用require加载模块的时候,有时候我们可以使用变量来标识这个路径,有时候却不可以,为了保险起见,项目里面我们最好不要用这样的一个全局方法。

下面我们看实践一的代码演示。

我们这个函数要写在我们项目下面有一个特殊的目录叫做library,library目录本质上就是我们要放一些JS代码,并且是在所有分包里面基本上都要使用的一个JS代码,在下面有一个optimus这样的一个子目录,我们request替代的异步转同步的工具函数promisify就定义在这里,这个方法我们可以看到它其实已经存在了 对不对,就是这样的一个方法,本身导出,这是一个方法名字,然后它会传进来一个方法或者是函数,传进来以后,我们会导出,就返回这样的一个,本身它返回的也是一个函数,所以返回的结果我们首先要去调用,调用以后它会返回一个Promise的对象,Promise对象里边又有fn的一个调用,其中在这个地方我们重点是做了一个Object.assign将我们的后面的对象 自定义对象写到args对象里面去,这个地方我们重点是做了一个success向resolve以及fail向reject它本身的一个映射,从而实现我们这个方法调用的时候让它的调用结果变成一个Promise这样的结果,本身我们Promise对象是我们ES6,使用ES6这种语法以后,它本身就支持了,所以我们这个地方可以直接使用,这就是我们的promisify工具函数的一个实现了。

另外我们还需要再看一下我们前面提到的,在这个app.js里边,我们提到的会有require这个函数 还有requireAsync,本质上它们两个原理是一样的,这种方式最好不要去用,当然你如果大胆一点你也可以用,但是如果是你项目里面出现了不可理解的一个异常的时候,你如果要排查错误,可以先把它给它注掉,注掉后看看它的一个表现,默认情况下我们就不使用这样的一个方法了,至于路径写法稍微麻烦我们就麻烦一点,其实麻烦一点,本质上也只需要改一次,我们路径拷贝以后只需要修改一次就可以了,它也不需要重复进行变动,代码演示我们就说到这里。


接下来我们看实践二,改写wx.request的请求。

接下来开始改写,使用wx.request编写的网络请求代码,这是一个标准的wx API,根据官方文档显示,它本身是不支持Promise风格调用的,它也有返回值,但是这些都不影响我们使用统一的编程范式去改写它,使用统一的编程风格可以有效减少我们脑细胞的一些浪费,以最经济的一种方式实现,具有良好的可阅读性,以及可维护性的一些代码,具体实践的时候可以在这个项目里面搜索,然后wx.request,看看有哪些代码用到这个接口了,再以promisify函数修改为伪同步调用的方式。

在我们这个项目里面大概会有三处以上的代码涉及到修改,在改写的时候我们主要有三点需要注意,第一点就是引入这个工具函数promisify的时候,因为我们的工具函数本身是用ES Module这种规范编写的,所以在引入的时候如果是用CommonJS模块规范进行动态引入的话,要从default上面做解构赋值,第二点就是给父方法添加async标记,为什么要加这个标记,因为在这个方法内部要使用await关键字,第三点就是在promisify这个工具函数改造wx.request接口并且调用的时候,主体代码的逻辑基本是保持不变的,其实只需要将上下这个代码位置稍微变一下就可以了。

下面我们看实践二的代码演示。

这个代码演示在微信开发者工具里面不是特别的好改写,不过我们同时也有VSCode,我们用VSCode来改写,首先找到我们的项目目录,找到小程序的根目录,然后在上面用这个文件夹里边搜索这个功能,让我们搜索wx.request,这个地方注意 可以看一下这个地方有1个 2个 3个 4个,一共涉及到3个文件里面4处代码的一个修改。

我们先看第一个,这个地方首先这是我们的商品详情页,这个地方首先我们要把我们这个工具函数进行引入,引入这个地方要用一个析构赋值 default等于promisify等于request,这个地方就要写相对路径了,好在VSCode有着很不错的一个代码提示功能,当我们敲这个路径的时候它可以自动帮我们完成,这样就可以了,引入了对吧,引入了以后接下来改写,怎么改写 首先第一步前面这个地方要加async,然后这个地方我们将下面先关掉,在前面加一个 res等于await然后等于它,后面我们要用promisify工具函数把它转一下,把它当成一个参数传进去,传进去以后,后面这个参数其实不变,在这个地方正常的结果我们要放在它的下面,改变一下它的位置,然后这个代码现在要给它去掉 就不需要了,success也不需要了可以放在,直接这么写是不可以的,要加一个catch,这样就可以了。后面这个是对前面返回结果的一个调用,这样就可以了,这是第一个修改。

接下来我们再看第二个修改,这个地方第一步仍然是引入default:promisify等于require然后路径library optimus Promisify.js已经引入了,找到下面这个的位置,在这个位置首先一定别忘了里面前面加async关键字,然后在这个地方const res等于await,然后promisify把它转化一下,这个代码是最终正确逻辑的一个处理代码,然后拷贝到下面,这个缩进稍微处理一下,这个给它去掉,然后这个地方加上catch,这个就改写完了。

下面还有一个,前面那个工具方法已经引入过了,它是同一个文件,所以我们直接使用就可以了,同样的方式,也是将它转化,转化以后调用,然后正确处理的代码拷贝一下 放在下面,这个地方删掉加一个默认的catch,是不是很简单。

还有最后一个文件,最后一个文件改造方法是类似的,等于require,找到合适的路径,再选择我们这个方法,这个地方加上res等于await,然后转化,这个地方不要忘了加async关键字对吧,我们要加在这个地方,因为它本身这个是一个函数常量,正确的处理结果放在下面对吧,方式是一样的,改写方式是相同的,然后最后是catch,这样就结束了,我们的改写已经完成了。

回到我们小程序里面单击编译,重新测试一下效果,首先看看有没有编译错误,目前来看没有什么编译错误,然后看一下表现,我们发现这些加载的数据也如期加载了没有问题,然后在商品详情页里边好像也没有什么问题,我们可以点一点看看有没有问题,也没有问题。这就是使用promisify对我们wx.request的接口的一个改写,这个里边我们有一点可以多说一点,就是在我们目录下面还有另外的一个文件同时还有一个promisify_on,这是做什么用的,它这个其实是改写我们小程序里面有一些以ws开头的onXxx这样的一类的接口,将这类接口然后改写为这个,就是异步转同步这样同样的一个编程方式这样的一种写法,当然我们不常用,所以在我们项目里面其实也没有用到,我们最常用的其实就是这样一个方法,这个代码演示就说到这里。


最后我们总结一下,因为小程序里边require它不支持绝对路径,而使用这个相对路径又会给我们编程带来额外的一些麻烦,一个路径长不说,在拷贝的时候还不能直接复用原来的模块引入代码,为什么在app.js文件里面定义全局的require方法可以使用绝对的路径,主要有两点原因 一是全局的程序的实例app,它可以通过getApp进行取用,然后取到它以后就可以调用它上面的方法了;二是app.js这个文件里面本身这个文件它位于项目的根目录下,我们要利用它得天独厚的位置以便可以从这个项目的根目录进行路径的一个标记,工具函数promisify在使用的时候有两点我们需要特别注意,第一点,函数在调用的时候为了避免这个程序报错,我们一般在后面一定要加一个默认的catch设置,回调函数如果没有特别的需要,我们可以默认传递console.log这样的一个方法,如果程序对这个错误处理有特别的一个需求,在传递这个console.log这个方法的一个地方,我们还可以进一步的进行完善和优化,第二点就是使用promisify函数的父函数,由于我们在这个里边使用了这个await

所以在外面,一定要添加async关键字,在添加async关键字以后,它已经不是一个异步执行的这样一个函数了,我们一定要避免在主线程上以阻塞的方式,也就是我们前面提到的添加await这样的一种方式直接去调用这个函数,关于第二点它很重要,如果使用不好的话可能会有副作用,我们可以再进一步的来阐述一下这个问题,在主线程上面可以挂很多使用async标记的函数,但是这些函数它其实都应该像葡萄一样挂在主线程之上,它们执行不应该阻塞主线程的一个执行,async这个关键字 这个单词它本身是异步的意思,放在这个函数前面表示这个函数里面有await起始的异步代码,在运行的时候我们可以理解为它执行了两次,第一次是执行到await关键字的时候,这个代码停在这里,第二次它拿到异步的结果以后继续从await后面的代码处继续往前执行,async await这一对关键字它是ES6的语法,它编译为ES5代码以后和我们原来使用回调函数的写法是类似的,但是这种写法前面我们讲了,它有助于让我们这个代码更加清晰 易读,代码的执行效率很重要,代码的易读性对开发者来说也很重要,所以异步转同步的编程范式是JS开发里面也是它最重要的一个优化技巧之一,这节课我们就讲到这里。上面的网址是本课涉及的文档地址。

点击查看开放文档:


这节课主要学习了异步转同步的一个编程范式,那下节课我们学习并使用并发复合命令模式对齐代码的执行点。

最后我们说一下思考题。这里有个问题请你思考一下,在这个项目开发里面我们经常会遇到这样的一个问题,某一处代码C的执行需要同时满足条件A和条件B,假设获得条件A需要三秒,获得条件B需要两秒,那么最快我们可以多久可以执行这个代码C,在数学上这个答案很明显 是三秒对不对,但是开发里面,因为获得条件A和获得条件B的代码可能不在同一个文件里面,并且有时候它们也不一定可以同时开始,谁先开始 谁后开始不一定,那么在这种情况下,我们应该如何从整体上设计代码才能让最后这个代码C尽快得到执行呢?这个问题先留给你思考一下,下节课我们一起来深入探讨一下这个问题。

最后一次编辑于  2022-07-14
赞 2
收藏

3 个评论

  • 爱心雨
    爱心雨
    06-17

    我已经看迷糊了,感觉还是C的单线程方式好理解

    06-17
    赞同
    回复
  • 清蒸鱼
    清蒸鱼
    2022-11-01

    ·promisify这个方法愣是没看懂为什么可以这样写;

    2022-11-01
    赞同
    回复
  • 清蒸鱼
    清蒸鱼
    2022-11-01

    JS既然是单线程的设计,那为什么又能拥有多个异步线程;这不还是算多线程吗

    2022-11-01
    赞同
    回复 3
    • 等闲识得东风面
      等闲识得东风面
      2023-08-08
      虽然叫异步线程 但是处理的任务还是要等主线程处理完才去处理其余的任务,不能实现两个任务并发去执行
      2023-08-08
      回复
    • 清蒸鱼
      清蒸鱼
      2023-08-10回复等闲识得东风面
      那这个异步线程并不存在线程 只是虚拟的?单纯符合定义的实现
      2023-08-10
      回复
    • 等闲识得东风面
      等闲识得东风面
      2023-08-10回复清蒸鱼
      js代码在执行过程中会有很多任务,这些任务总的分为两类:同步任务和异步任务。异步任务又可以细分为宏任务与微任务。
      2023-08-10
      回复
登录 后发表内容

小程序性能优化实践

课程标签