- 小程序切换后台后,不再使用 setData
[视频] 你好,我是李艺。 上节课我们主要学习了如何使用串发复合命令延迟调用代码,这节课学习一个小技巧,就是在小程序切换至后台以后关闭对setData的一个调用。 首先我们看一个问题,前面我们在了解小程序的启动流程的时候已经知道了启动有两种一种是冷启动,一种是热启动,当小程序进入后台状态的时候,这个时候会有5秒的一个暂停状态,在这个状态里面小程序的代码仍然是可以运行的,如果5秒内小程序没有被唤起,这个时候它就会进入一种挂起状态,在挂起状态中小程序的JS代码不会运行,在后台状态里面小程序已经不可见,此时所有的与视图有关的代码,像setData的一个调用已经没有必要进行执行了,下面我们看项目实践。 首先看实践一,监听App进入后台的一个事件。 在这个小程序里面使用wx.onAppHide可以监听小程序进入后台的事件,或者是我们在App的周期函数里面,在它的onHide周期函数里面也可以捕捉小程序进入后台的一个时机,这个时候就可以停止页面里面对所有的setData的一个操作了。为了方便控制这个页面对象里面的setData方法,我们需要劫持Page对象,怎么样去劫持,首先需要在library目录下面创建一个page.js文件,在这个文件里面劫持onLoad和setData方法,因为setData方法只有在运行的时候才可以拿到引用,所以在这个地方先劫持onLoad的方法,然后在onLoad的方法里面再获得setData方法的一个引用,接着再往下需要在app.js文件里面调用并应用page.js的代码,使其劫持发生作用,最后在首页的onReady这个方法里面可以写一个定时器,不断地去调用setData去测试我们新编写的功能,在测试区的里面AppData面板里面,我们可以看到data数据属性它一直都在变化,在模拟器里面模拟单击Home键可以使小程序处于一种后台的状态,此时setData就不再更新了,AppData面板里面的数据也不再变化了。 接下来我们看实践一的代码演示。 要完成对setData方法的一个劫持,我们需要对我们当前的页面对象也是page进行一个改写,怎么样去改写,先看一下我们最终源码,在library下面有一个page.js文件,这是我们接下来要编写的一个文件,我们将这个文件先拷贝一下放到我们目前的项目下面,也放在同样的目录下 library下面,我们看一下这个代码主要是干了什么事情。 第一步首先是我们有一个常量的赋值,需要将我们原本的小程序本身,它原生的Page对象要做一个引用的一个保存,稍后因为这个地方我们可能还要用到,然后接下来我们会导出,有一个方法的导出,在这个里边,首先要从我们的options,options其实是我们这个页面里面我们自己设置的一些信息,包括data对象还有这些方法都在这个对象里面,然后我们要取到onLoad这个方法以后,接下来我们就开始重写这个方法了。 重写方法以后我们可以看到在这个地方,我们拿到了这个setData的一个引用,它是从this对象里面进行一个解构赋值,然后拿到这样一个引用的,在这个地方this其实就是我们当前的页面对象的一个实例,因为在这个地方它这个代码已经运行了,指的就是它的一个实例,再往下我们重新定义了setData的方法了,然后value是它的一个值,在这个地方我们看一下,我们用到了一个全局的状态就是global.state,现在是我们自定义的,现在它还没有值或者说它默认的它是undefined,在这个地方我们要检查,如果是这个状态等于hide,也就是它切到后台状态,非后台状态 这个地方有一个非的判断对吧,我们就调用setData方法对不对,如果是切到这个后台状态,那就是把我们这个调用给它忽略掉,这就是它的一个方法,这个地方有对于我们原来的方法就是一个再调用,我们把现在的方法放在前面执行,再执行具体的页面里边的自定义的那些代码逻辑,这就是我们整个的文件它的主要的一个代码,大概是这样的一个实现。 接下来我们看怎么样去应用这样的一个文件,首先看我们app.js里面,在这个地方有一个引入,我们将这个代码拷贝一下放在我们的app.js文件里面,放在这个页面的顶部 放在这个位置,这个地方我们看一下,当前其实它相当于是对我们当前的上下文执行环境里面的Page类型 类对象进行了一个重写,这个写法跟我们原来的普遍的一个写法有点不太一样,加载完成要取到default,然后将导出来的新的类对象然后赋值给它,这就完成了一个劫持,这个劫持完成以后为了测试效果,我们还需要写一个测试代码。 看一下我们这个里边是不是有一个叫做定时器,在这个地方有一个定时器,这个代码我们写到了onReady周期函数里面,找到首页,再找到它的onReady周期函数然后切到这个地方,这个代码稍后我们会删除,所以暂时就放在这个地方,然后这个里面我们做了一个什么事情,每隔500毫秒去设置当前这个页面上的一个名称,为xxx的这样的一个数据属性,当然这个数据属性在我们目前这个页面上它不存在也没有关系,不存在的话我们设置之后它会自动地去新增的,这地方设置这个方法调用就是setData(AppData),因为我们现在对setData有劫持,稍后我们再切换到后台的时候可以看到它会发挥作用,这个代码搞完了。 接下来我们开始在微信开发者工具里面进行测试,单击编译看它的一个表现,已经启动了,调试区没有错误,现在我们看一下AppData面板。看这个 这有一个三个x,有个数据属性,然后它后面这个值一直都在变化,它为啥会变化,因为我们里面有一个定时器500毫秒一个定时器,然后不断对它进行修改,所以它这个地方一直会变。现在我们在模拟器里面,我们选的这个地方有一个菜单,然后选择模拟操作里面有一个我们选择Home,当我们按下Home的时候,其实相当于在手机上按下Home键,这个时候小程序它会切到后台的状态,我们选这个不太明显对吧,重启了一下,我们刚才点了重启的按钮现在已经重新启动了,那么单击完以后这个不要去选了,因为它选了以后它又回到了那个状态。 项目在重启以后,我们看到我们小程序在切到后台状态以后,从我们AppData面板里面我们一直都可以看到三个x的数据属性一直都在变化,可以现在再打开模拟操作 选择Home,当这个面板浮现的时候其实已经代表无论我们接下来选哪一个,其实已经代表我们小程序已经进入一个后台状态了,但是此时我们看到AppData面板里面它这个数据属性仍然在发生改变,为什么仍然在发生改变,为啥发生改变,因为我们先前页面劫持里边用到了一个全局的数据属global.state对吧,现在global.state我们还没有设置,肯定它们不能发挥作用,所以接下来我们要在我们的AppData里面要添加程序的周期函数,然后去设置全局属性。 看一下我们最终的源码,找到app.js,在这个里边有两个方法在这里,一个是onShow,另外一个是onHide,下面这个是onUnhandledRejection,这个是什么意思呢,我们在用了Promise编程方式以后,它有一些没有捕捉到的一些reject的异常,我们可以捕捉以后在这个地方进行打印,这是它的一个作用,所以这三个方法我们都可以给它拷贝过来放在我们目前的项目里面,找到app.js放在下面 最下面就可以了,所有的周期函数我们都往下放 这样就可以了,这个代码修改完了 我们再次刷新项目看一下它的运行效果,现在我们看到这个数据属性一直在发生变化,这个时候我们单击模拟操作,然后选择Home,我们看到这个数据属性现在不在变化了,同时在这个地方,我们可以看到这个地方 setData没有作用,这个打印是在哪里打印的,就在我们劫持代码里面对不对,当我们选择一个入口方式以后,我们这个代码又开始恢复调用了,这个时候我们可以看到它定时器又起作用了,这样我们就实现了,对我们的所有页面里面的一个setData方法的自动的一个劫持,只要我们这个程序进入后台状态,无论是哪个页面里边对setData的一个调用都已经受到了我们的管制了,最后我们将我们的测试代码给它注掉。因为本身我们这个程序不需要这个测试代码,这个代码演示就到这里。 最后我们总结一下,我们想实现对setData方法的一个劫持,除了劫持原生的Page类对象以外,我们还可以定义一个工具函数 例如叫mySetData,在原来所有调用setData的地方改为调用mySetData,但是这种方式它比较繁杂,一般我们不这样采用对于原生的类对象的劫持,我们在使用的时候一定要克制,如果必须要发生劫持的话一定要把劫持代码统一放在程序的入口处,这样方便我们项目的其他维护者看到,如果是默默劫持了原生的类对象 而不加说明,这可能会给其他人 这个代码带来一些问题,导致他们的代码出错,这不是一个优雅的程序员所为。这节课我们就讲到这里,上面我们现在看到的这些网址是我们本节课涉及到的一些文档地址。 点击查看开放文档: 小程序运行机制框架接口 /小程序 App /App 这节课我们主要学习了如何劫持Page类对象以及如何在小程序进入后台状态的时候,不再真正的调用setData方法,下节课学习如何实现数据预拉取与周期性更新。 最后我们说一下思考题。这里有一个问题请你思考一下,在第一课我们讲启动流程的时候我们曾经介绍过微信特意为开发者提供了数据预拉取和周期性数据拉取的这样一种优化机制,对于一些大块量的这些数据,我们不需要开发者自己拉取了,微信可以代为拉取,拉取以后再将得到的数据再提供小程序直接使用,这种贴心的机制你知道它怎么使用吗?这个问题先留给你思考一下,下节课我们一起深入探讨一下这个问题。
2022-07-14 - 使用异步转同步的编程范式
[视频] 你好,我是李艺。 上节课我们主要学习了如何在小程序里面使用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开发里面也是它最重要的一个优化技巧之一,这节课我们就讲到这里。上面的网址是本课涉及的文档地址。 点击查看开放文档: 网络 /发起请求 /wx.request 这节课主要学习了异步转同步的一个编程范式,那下节课我们学习并使用并发复合命令模式对齐代码的执行点。 最后我们说一下思考题。这里有个问题请你思考一下,在这个项目开发里面我们经常会遇到这样的一个问题,某一处代码C的执行需要同时满足条件A和条件B,假设获得条件A需要三秒,获得条件B需要两秒,那么最快我们可以多久可以执行这个代码C,在数学上这个答案很明显 是三秒对不对,但是开发里面,因为获得条件A和获得条件B的代码可能不在同一个文件里面,并且有时候它们也不一定可以同时开始,谁先开始 谁后开始不一定,那么在这种情况下,我们应该如何从整体上设计代码才能让最后这个代码C尽快得到执行呢?这个问题先留给你思考一下,下节课我们一起来深入探讨一下这个问题。
2022-07-14 - 使用WXWebAssembly优化运算性能
[视频] 你好,我是李艺。 上节课我们主要学习了代码的分布异步化以及插件代码的分布异步化。这节课我们学习如何使用WebAssembly技巧以及如何优化代码的执行性能。 首先我们看一个问题,WebAssembly号称是Web的汇编,它允许开发者使用Go C C++等后端强类型语言编写代码,然后将其编译为一种类似于汇编代码的二进制中间代码,使用这种代码可以在Web页面里边直接运行,汇编语言的性能是毋庸置疑的,这项技术从根本上一举弥补了解析型语言JS在执行性能上的一个不足,小程序已经添加了WXWebAssembly功能,实现了对WebAssembly技术的一个全面基础的支持,接下来我们看项目实践。 首先看实践一,使用Go语言编译WebAssembly的文件。 首先第一步我们需要编写Go语言的逻辑代码,在我们源码里面有一个叫做main.go这样的一个文件,这个文件它的一个主要作用是代替我们原来的stopwatch组件里面内嵌函数,叫做convertTimeStampToString这个函数的功能,Go语言在这个时间操作上,它有标准库方法支持,比较完善,已经不需要像JS代码那样进行时间字符串的一个拼接了,第二步编译和压缩wasm文件,编译以及压缩文件的脚本在我们最终源码里面已经写好了,这个编译脚本是build.sh,压缩脚本是compress.sh 直接取用就可以了,第三步我们要从Go语言这个源码里面拷贝并且修改一个wasm_exec.js这样的一个文件,这是让这个wasm文件与这个JS协同工作的一个桥梁,它是一个必不可少的一个文件。 源码里面有一个叫做copyjs.sh这样的一个脚本,我们使用这个脚本就可以从Go语言源码包里面拷贝出wasm_exec.js这个文件了,这里有一个问题需要注意一下,在这个小程序里面使用的时候,wasm_exec.js这个文件的源码里面有些地方必须要修改,但是这些你已经不需要做了,因为修改之后的文件我也已经放在源码里面了,还有就是同时也需要一个text_encoder.js文件,它是这个wasm_exec.js文件执行的时候需要的一个模块,在使用的时候直接拷贝到我们小程序项目里面就可以了,这里有一个非常重要的点需要注意,如果你安装的Go语言版本不是go1.17.8这个版本的话,那么就很有可能不能用我修改过的JS文件,因为这个文件它与版本是高度绑定的,换一次版本你就要修改一次。在源码里面还有一个叫做index.html这个文件,这是一个测试文件,我们运行这个文件以后可以验证编译出来的WebAssembly的文件结果是否正常。 下面我们看实践一的代码演示。 首先我们需要在我们这个项目的根目录下创建一个新的目录,用于放我们Go语言代码,我们这个目录可以起名叫做go_stopwatch,下面的事情我们要看一下我们已经编译好的最终代码了,第一个我们要拿来取用的一定是我们的Go语言的代码,main.js 这是我们第一个要启用的代码,这个文件我们可以拷贝一下放在我们的目录下面,这个文件我们可以简单看一下 它主要的一个代码,首先这个地方是一个package的一个声明,它声明我们这个包的一个名字,这个是不可或缺的一个,再往下是import 这个是模块的一个引入,我们现在使用的都是标准库的一个模块 没有第三方的,这个地方是一个zero 一个临时对象,下面我们要使用它,因为它没有必要重复创建,所以把它作为一个模块变量放在这个地方。 这个地方有一个getFormatedMiniSeconds,这个方法是我们将一个一个ts 一个时间 一个值,一个ms 一个时间值传进来以后,用Go语言进行一个格式化,格式化成这样的一种格式,在Go语言里面我们注意一下它的格式化字符串很特别,它是拿指定的时间的字符串 然后那种格式、去作为一个格式化字符串的,就是这个地方,比如说04:05这个数字我们不可以改,它就是默认作为一个特殊的字符串进行存在的,调用方式跟我们一般的Go语言的调用方式有点不一样,因为我们这个地方,本身我们参数的接收是从JS语言里面接收的,然后它返回也是向JS语言进行返回的,我们看到这个地方接收,首先this对象,第一个this对象,它的值是js.Value,而js.Value类型其实是在syscall/js标准库里面,它是这样引用出来的。 本身这个功能就是Go语言支持的这种WebAssembly文件编译的这种功能,也是官方的标准库支持的,并没有第三方的类库的参与,再往下面是args这是我们从JS传递过来的一个参数,它是一个数组,我们在这个地方取到了它的参数,第一个是ms,也就是一个毫秒的字符串,第二个是我们的一个回调函数,然后在这个地方我们是先计算,计算完成以后把计算出来这个结果传给callback,也就是JS里面的一个回调函数,传给它。 这个地方我们还用了协程,这个是Go语言里面协程的一个写法就是go func,然后后面跟一个函数然后就可以执行了,它就新开一个这样的一个协程并执行,这是Go语言里面的一个主函数 就是main,在这个里面我们首先是创建了一个channel 一个通道,这是一个需要从JS里面去获取它的console的这样的一个接口,因为我们要用它里面console.log这样一个方法,第二步我们获取了 不是获取 这是设置,我们这是Set,Get是获取,Set了一个方法,就是getFormatedMiniSeconds,这个方法稍后我们在JS里面会调用。 最后一步这个地方我们有一个阻塞这个程序退出的通道,就是有这样的一个通道读写,尝试从里面去读,但其实它里面没有东西 是读不出来的,所以它这个程序会一直停在这个地方,这是防止我们程序退出,如果你退出的话,在JS里面你就调不到这个里面设置的方法了,这就是我们Go语言的一个代码,它作为一个示例十分简单,但是它已经演示了我们如何去使用Go语言去编写一个WebAssembly这样的一个文件。 接下来我们还需要做一些工作,首先是我们要将这几个脚本文件给它拿过来,build.sh这些脚本文件一共是三个,把它给拷贝一下然后放过来,为了节省时间 所以我们脚本其实不用去手敲代码了,当然你们自己如果想练习的话也可以手敲代码。首先是build 这个代码是这样的,你在执行的时候第一步一定要先安装Go语言的语言包,并且这个版本要和我保持一致,不保持一致的后果前面我们也提到了对吧,它可能会遇到一些未知的问题,你可能有一些额外的工作需要做,这个是比较麻烦的,因为我之前已经做过了,这个脚本出来以后,接下来我们要做一个事情就是调用了,调用之前首先我们要把环境变量给它关一下,这个是它的模块的一个开关,先给它关上,关上以后我们就可以调用我们build.sh了。 脚本大概的意思它会编译我们main.go这个文件,然后编译成一个stopwatch.wasm这样的一个文件,然后执行,执行成功了,这是我们编译出来的一个文件,编译出来以后还没完,为啥还没完,因为在这个小程序里面提供了对br压缩文件这种功能的一个支持,因为这个文件它编译以后文件大小比较大,直接放到我们小程序代码包里面直接打包的话,其实会占很大的一个代码包额度,所以这个地方我们最好要进行一次压缩,压缩的话其实也很简单,我们需要使用一个叫做brotli的这样的一个模块,用这个模块对它进行压缩,这个模块如果你本地还没有的话,需要先用这个brew然后进行安装,当然如果包管理工具你没有的话,可以用上面这个指令进行先安装一下,安装以后,再安装下面的模块,安装以后然后就可以去压缩这个文件了。压缩的时候是这样,我们先把目标的位置,这是我们要放的位置,如果有的话先给它删掉,删掉以后,接下来就调它的brotli这个模块 ,然后去以我们当前的文件为基础进行压缩,废话不多说,我们就运行一下我们这个代码,已经执行完了。 我们看一下我们这个目标目录下面这个地方,在.go这个就是我们的目标文件,但是因为我们原来的它这个地方也有一个文件,所以我们可以先将它删掉,删掉以后然后再看,已经有了对吧,这是压缩 压缩以后,另外还有一个文件需要处理就是copyjs.sh,这个脚本它本质是从我们当前系统里面安装源,就是Go语言的源码里面去它的指定位置去拷贝这样的一个文件,然后拷贝到当前的目录下,当然这个脚本我现在不要去执行它,因为我本地的文件我已经做过修改了,拷贝以后如果你自己要做修改的话,我告诉你有哪些需要修改,可以拿这个IsWechat去搜索一下当前这个文件里面,你看有很多地方吗,这些地方都是要修改的,并且如果你的Go语言版本跟我这个不太一样的话,它导出来的文件也不一样,所以这就是我们前面说的,你最好与我的版本保持一致,如果是一致的话 这个文件你没必要修改了,直接拿我这个文件用就可以了。 这个地方我们还有一个web脚本,这个脚本是它原生的一个脚本,用于这个地方,我们把这个IsWechat改成false了,这个文件我们可以用在我们的另外一个测试文件里面,就是index.html,加载JS脚本,然后在下面我们这个地方有一个JS代码去加载我们这个文件对吧,编译好的这个wasm这个文件加载以后,又在这个地方去调用了这个里面的方法getFormatedMiniSeconds,这个方法就是刚才我们在Go语言里边去输出的方法、导出的方法,所有的这些文件现在这个 还有这个JS 这个 这个,这几个文件我们一起来拷贝一下,放在我们的这个地方,放进来了,然后在我们目录下面为了测试我们工程链的一个可行性,稍微可以对我们这个代码做一点点的一个修改,做一点点的修改,怎么修改,比方说我们这个地方有一个res,res是个变量,假如我们把它改一下的话,res等于,算了我们不改这个了,如果我们要用什么的话,可能还需要去添加新的模块,所以我们就拼接就可以了 我们随便给它加个后缀 加个st就好了,或者加个ms。 加完以后,第一步我们要先编译我们这个代码,build先编译,编译以后然后进行一个压缩,压缩完成以后,其实这个地方是可以通过看它的时间来判断它是不是我们修改过后的一个文件,比方说我们在这个目录,这是我们的目标目录,在资源管理器里面打开,然后你看一下它这个时间7:52对不对,这就是我们刚才发生的时间,它已经被修改了,接下来我们测试 怎么测试,首先我们要看一下这个文件正不正常,这个地方有一个专门用于测试的文件,这个文件我们可以拿一个插件Live Server,Live Server是我安装的,你可以在VSCode里面进行安装,我们在浏览器里面打开这个文件,然后打开调试区看一下这个地方 看到没有,get res 然后后面有一个时间字符串,并且后面多了一个ms,因为ms是我们刚才新加的对吧,调用结果说明我们这个测试的代码是没有问题的。 接下来我们开始搞 再往下检查一下我们这些文件,在这个目录下br文件已经有了对吧,text_encoder文件也有了,然后wasm_exec.js这个文件有了,并且这个里面我们可以看到它前面IsWechat等于true,正是我们要在小程序里面使用的这个版本,代码演示我们就说到这里。 接下来我们看实践二,在小程序里面创建和使用stopwatch_go组件。 第一步首先我们需要将相关的这个JS文件,wasm文件等这些文件全部拷贝到我们目录下面,与我们组件放在一起,并且在这个目录下面要依照原来的stopwatch组件的代码创建新的组件,stopwatch_go组件的JS代码就是在我们原来组件的基础之上然后修改过来的,在这个组件里面有一个特殊的flag变量,这个变量的作用是节制我们对WebAssembly文件的调用,为什么要节制呢,因为跨程序文件之间的调用,它与这个设备性能有很大的关系,我们并不能保证调用之后多久可以拿到返回的结果,所以这个地方我们一定要有节制,stopwatch_go组件的wxss样式代码还有它的wxml这个标签代码与我们原来组件是一样的 没有变化,直接拷贝过来就可以使用了,第二步就是我们要修改主页配置里面对组件的一个使用,开始使用新组件然后进行测试,从最终的一个测试结果来看效果还是不错的,下面我们开始代码演示。 下面我们开始实践二的代码演示。 目前在我们这个项目里面的wasm.br组件已经准备好了 我们看一下,在这个目录下 这个文件就是我们新编译的一个文件 对吧,这个组件代码已经有了,当它是从我们最终源码里面拷贝过来就可以了,简单看一下wxml标签代码跟之前是一样的,样式代码也没有变化,然后在JS文件里面 这个文件里面我们要看一下,重点是在这里面,这个地方我们引入了一个wasm_exec.js这个文件,这个文件就是先前我们拷贝过来的对吧,放在这个组件目录下的,然后第二个是我们做了一个路径,就是这个文件的一个路径,wasm.br这个文件的一个路径,这个路径不能使用相对地址,必须使用绝对地址,再往下,上面这些都是一样的,然后在我们的methods里面有一个intGo初始化,因为在使用之前我们要初始化,初始化我们在哪里调用 在lifetimes是我们组件的生命周期,它的这样一个函数组,其中里面有个ready是周期函数,在这个ready里面是组件准备好以后我们要执行代码,这个地方我们看global.console等于console,这个里面是去命名我们Go语言里面设的打印的代码接口,设置接口以便Go语言可以调用。 这个地方我们用了await,然后去等待我们的初始化代码的一个完成,因为它如果初始化不完成的话,你调用它是没有作用的,必须等它初始完成,而且这个初始化它不是很快就可以完成,这个地方是访问从global.Go上面,这个是全局的对象,从上面然后去访问它再调用WXWebAssembly,instantiate这个是小程序它本身提供的接口方法去实例化这个文件,实例化以后,其实这个代码它执行到这个位置,可以认为它第一次执行已经结束了,然后其实它不会有一个结果,它如果要有结果的话就是我们Go语言main那个方法它退出了,如果它退出的话接下来你去调用JS 调用Go里面那个方法就调用不到了,所以这个地方初始化其实它只是执行了一次,执行了第一次那个方法,后面那个是走不到的。 再往下就是start stop还有switch,stop和switch没有什么变化 和原来是一样的,重点是start里面 在这个里面我们有变化,这个地方,这个名字还是原来的名字,这个地方我们去直接在global上面调了getFormatedMiniSeconds,这个方法其实是Go语言通过WebAssembly,通过那个文件然后写到全局对象上的,所以我们在这个地方可以调用,把ts传给它,然后拿到这个res 就是我们那个结果,再通过setData然后进行视图的一个渲染。 这个地方注意有一个flag,我们刚才提到了我们要加一个flag,为什么加一个flag 就是为了标识一下每次调用成功以后,我们才可以进行第二次的一个调用,如果是上一次调用没有回来 你这次不要去急着去调用,如果你调用多的话,CPU 内存可能就吃不消了,程序可能就卡死了,所以这个地方我们看到这有一个flag,它每次有一个循环就是一个定时器,然后去调用,这样的一个结果,这就是这个代码。 接下来我们开始修改我们调用代码,开始去使用我们写的组件,在我们index文件里面看一下这个地方把这个组件的地址改成_go 对不对,只要改这一个地方就可以了,其他地方其实不需要修改,然后编译,有的时候组件它是不显示的,但是我们需要有一点点的耐心,可以清一下编译缓存然后重新再点一下编译按钮,是这样的,刚才我把index_addons分包给它删掉了,因为之前测试它老是加载不到这个组件,所以我们把分包给它去掉,分包去掉以后组件,我们index_addons这个目录其实现在已经属于主包了,所以我们现在把它去掉以后,它其实可以更优先被加载,优先被注入和使用。 现在这个文件,这个组件已经显示了,然后我们尝试去单击这个组件,看到没有,有变化,后面这有一个ms,原本那个文件里面是没有了ms是我们刚才新加的,而且看起来好像很流畅,具体流不流畅,其实无法靠我们的视觉进行评判,因为视觉会产生误差,特别是同样的一个代码,如果你在你自己的机器上运行和你同事的机器上运行,可能它这个结果,产生性能的表现它也是不一样的,比较公平的一个方式就是我们查看Performance面板,让它启动,然后打开Performance面板的一个录制按钮,等到执行几次以后单击停止,这一段已经停止了所以它后面没有,然后这一段是执行时间,我们可以看到这个性能是相当的喜人,总执行时间只有7ms 7毫秒,看见没有,厉害,Go语言果然很强大,这个地方18毫秒,所以它这么短的时间根本就没有红三角,这个时间其实是反应是很快的,从直观上来看,单击以后你看这个很流畅,然后再单击停止也很流畅,所以这个效果还是很不错的。 当然这个跟我们做了一些优化也有关系,因为我们前面提到了,这个文件里面我们加了一个flag,如果你没有这个flag的话,你认着这个定时器,然后一直往Go语言的那个wasm文件里面去发请求的话,它如果处理不过来,你积压这个请求越来越多,最后你这个内存它会一直飙升,然后拖到最后可能就把你这个程序给拖垮,所以在合适的节奏里面去调用这个代码是相当重要的。我们代码演示就说到这里。 最后我们来总结一下,Go语言是强类型编译语言,它被称作是互联网时代的C语言,根据2022年3月份腾讯发布的研发报告,Go语言在腾讯内部项目里面的使用已经超过了C++,成为腾讯后台编程语言里面的第一选择。我们在Go语言里边还使用了协程,还有就是我们这个代码实例其实逻辑也十分简单,所以我们一点也不怀疑我们Go语言代码的执行效率,那么在这种情况下,为什么还要在这个小程序里面,在调用这个wasm这个文件的时候还要加上这个调用限制,Go语言它执行效率虽然很高,但是JS和Go语言之间相互调用中间还有一个wasm_exec.js这个文件,这个文件它是无法跨过的一个瓶颈,所以WebAssembly这种技术适合做大块数据的一个运算,运行一次相比JS执行可以节省很多时间,但其实它并不适合高频运算这样的一个需求场景。这节课我们就讲到这里,现在屏幕上显示的网址是我们本节课所涉及的文档地址。 点击查看开放文档: 性能与体验 /WXWebAssembly 这节课我们主要学习到如何使用Go语言编写代码、如何编译为这个wasm文件以及如何在这个小程序里面使用WebAssembly的技术。下节课我们学习异步转同步的编程范式。 最后我们看一下思考题,这里有一个问题请你思考一下,就是异步编程它是JS的一大特点,但是太多的回调函数一层嵌套一层又使得这个代码看起来逻辑非常的不清晰,非常不利于维护,那么有没有一种办法既可以享有异步编程的无阻塞优点,又能保持我们这个代码的清晰连贯性,这个问题先留给你思考一下。下节课我们一起来深入探讨一下这个问题。
2022-07-14