收藏
评论

脚本优化技巧官方


你好,我是李艺。

上节课我们主要学习了如何通过padding样式,或伪元素样式扩大元素的可单击区域。这节课我们学习脚本优化技巧。

首先看一下问题,JS是一门支持异步编程的单线程语言,拥有垃圾内存回收机制,但如果对象被异步线程引用,或者是被全局对象引用,便可能造成对象一直无法被正常释放的局面。这种情况极有可能造成内存泄漏,在编程时候我们要特别注意。下面我们看项目实践。


首先看实践一。

定时器是异步线程里的东西,在离开页面的时候一定要记得销毁。传递给定时器的回调函数是主线程传递给异步线程的,如果定时器使用结束以后主线程不能将定时器及时停止和销毁,那么异步线程会一直持有对回调函数的一个引用。如果使用定时器的页面或者是组件是重复产生的,在运行时又会创建很多定时器,这种情况便极容易产生内存泄露现。

下面我们看一个实例,如我们屏幕上展示的,在stopwatch_nw组件里面,有一个stop方法,这个方法它负责停止和清扫定时器,当组件从页面节点树中移除的时候,或者是页面不显示的时候。例如在hide方法里边,这时候定时器已经没有执行的必要了,此时也需要调用stop方法将其停止和销毁。

下面进行实践一的代码演示。

首先我们看一下目前的一个组件的代码,stopwatch_nw这个组件,我们看它的js代码,在js代码里面,有一个stop方法,这个方法里面调用了clearInterval,进行了定时器的停止和清理,这是正确的一种做法。但是在这个地方我们还少了另外的一个操作,我们组件本身它是有生命周期的,生命周期一共是有两个,一个是pageLifetimes,在我们VSCode里面,它的提示性不是很好,可能不是很好,我们可以转到我们微信开发者工具里面,在这个里面一般情况下它会给我们正常的一个提示,我们写lift,当我们打lift以后会出现两个,一个是lifetimes,一个是pageLifetimes。其中这个地方,这个对象它是指我们组件的生命周期,另外一个是pageLifetimes,这个是我们组件它所在页面的生命周期,我们需要实现的效果是这样的,当我们组件在我们这个页面里边不再显示的时候,然后停止定时器,然后我们需要写在pageLifetimes,在这个里面,上面这个是组件本身的,所以我们把这个给它去掉,写在这个地方,这是我们要实现的一个代码,现在我们看一下最终代码的一个实现。找到我们的组件,然后调用,没有问题,这是组件所在页面的生命周期对象,跟我们之前这个代码是一样的,只是多了两行注释,这是stopwatch_nw组件。

另外我们还有两个类似的组件也使用了定时器,第一个是这个,就是我们后缀有wk的这样的一个组件,我们看一下它这个js代码,这个里面也有一个stop,在stop方法也有定时器清理的代码,但是在这个组件里面我们缺少相关的周期函数,所以我们可以将这一段代码可以拷贝过来,直接放在这个地方。另外还有stopwatch_go组件也有stop,但是它没有周期函数,所以我们把关于周期函数的一些代码放在这个地方,完成以后我们单击编译按钮进行测试。我们先看一下我们当前主页里边所用的秒表组件是哪一个,我们现在所用的是nw组件 ,我们现在单击让它开始计时,当我们尝试单击的时候,我们这个地方报了一个错误,报了一个接口错误,它说我们后端接口不存在,我们可以到VSCode里面看一下我们后端的代码,这个地方可能存在额外的一个问题需要我们处理。找到server controllers,然后是api home,这个是存在的,它本身是存在的,这个接口是存在的,我们可以在浏览器里面查看一下它的执行效果,不存在。

在刚才我们发现一个问题,我们的一个接口,我们发现无法去访问它,我们可以在浏览器里面再尝试看一下它的一个运行结果,运行结果,这个地方可能还没有启动,我们把它启动一下,然后在浏览器里边访问一下这个地址,因为这是一个GET接口,我们是可以直接在浏览器标签里面进行访问的,我们发现这个接口它是Not Found,然后不存在。但其实在我们的代码里边这个地方是有的,这种情况貌似之前也出现过一次,什么原因。而且我们启动脚本也在启动,原因在于我们目前启动的目录,跟我们的所用的项目目录不是一样的,因为我们每一讲都有一个示例源码,每一个示例源码里面都有前端后端的代码,所以这个server目录一定要搞对,搞不对的话,可能就会出现刚才那样的问题,我们现在启动目录是在这个目录下,而我们实际上在用的其实是这个,我们只需要在这个地方,打开集成终端窗口,然后再看一下目前,这是我们的实际在用的目录,然后再使用yarn dev启动我们的后端程序,启动以后再刷新一下,第一次它会编译,它会有问题,然后再刷新一下,这个地方接口少了一个api,现在可以了。接口正常以后,接下来我们回到小程序,微信开发者工具里面,然后再单击编译,重新测试,继续刚才我们的测试,启动以后,我们先单击组件让它运行,然后再单击用户主页,单击用户主页,然后再选择主页,这个时候我们会发现计时器它已经自动停止,为啥会自动停止。因为我们组件里面加了关于页面生命周期的代码 ,当这个页面隐藏以后,我们就调用定时器的清除代码,调用stop,然后停止定时器的一个执行,这是我们实现的效果,我们正要实现的一个效果,这个代码演示我们就说到这里。


下面我们看实践二。

使用wx.onXxx全局绑定一定要小心,有一个监听必然要有一个反监听,在主页的JS文件里面,如我们屏幕上显示的,有对系统主题改变事件的监听代码,所有像wx.onXxx这样形式的监听都属于全局监听,一般我们在页面的onLoad周期函数里边添加这样的监听,同时在onUnload周期函数里面移除监听,以免造成无效页面被异步线程引用,而无法被GC及时回收的情况,主题切换事件既可以在手机上触发,也可以在模拟器中模拟。但是在模拟器中可以模拟的前提是app.json文件中必须有一个名称为darkmode的配置节点,如我们屏幕上看到的这样。

下面我们看代码演示。

首先我们看一下最终源码的实现,找到主页,主页的js文件,在这个文件里面我们有一个这个,themeChangeHandler这样的一个监听,这个代码它可能经过格式化,它其实是这样的,它应该在这里,这样这是它正常的一个表示,然后与它相关的是一个onLoad onUnload,在onLoad里边我们添加了一个wx.onThemeChange这样的一个监听,这是一个全局监听。同时我们在onUnload的周期函数里边我们加了一个wx.offThemeChange,这样的一个反监听,微信小程序的全局接口一般有这样的一种规则,就是有一个on接口,它必然会有一个off接口与之对应,一个是监听,一个是反监听。接下来我们首先将这个代码给它拷贝一下,到我们的目前的小程序项目里面来,这是我们代码已经有了,但是我们是不是有一个onUnload这个方法。看一下,这个地方也有了,但是onUnload没有,我们可以将onUnload加到这个地方来,这是有一个反监听,然后在onUnload里边我们添加了这个,添加了它,然后同时我们回调也有了,这是我们回调,当我们主题发生改变的时候,然后这个代码会执行,代码添加完了,然后现在我们单击编译进行测试一下,启动以后我们选择模拟器上方这有一个模拟操作,然后我们切换一下主题 单击深色,但是没有效果,再单击浅色,也没有效果,为啥没有效果。因为刚才我们提到了,我们如果想在模拟器上进行测试的话,我们需要在app.json配置文件里面,添加一个darkmode这样的一个配置节点,把它设置为true,然后再选择模拟操作。现在这个地方有一个打印对不对,当前主题是dark,然后我们再切换过来,选择浅色,当前主题是light 现在已经可以模拟这个测试了,代码演示就到这里。


下面我们看实践三。

使用全局对象要小心,所有在global上或者在app上定义的全局数据,或者在上面添加的事件监听,在使用的时候我们都要小心,在相关代码的使命完成以后,要记得及时做清理工作,自定义的类对象如果可以的话,我们也要实现一个dispose方法,在释放对象的时候先调用dispose方法,再将其从全局对象上删除,在我们的stopwatch_go组件里面加载与初始化,WebAssembly文件的时候也使用了全局对象,如我们屏幕上展示的。在组件的生命周期函数,detached内部也应该做相应的清理工作。

下面我们看代码演示。

首先我们在VSCode里面,找到我们当前的项目,程序项目,然后看一下在library optimus里面有一个command这样的目录,在这个文件里面主要是关于命令类对象,Command是它的一个基类,在Command里面,其实我们实现了一个dispose,这个方法本身它的使命就是什么,释放,移除事件监听,做清理工作的,就是所有的清理工作我们都要在这里面来做,在这个地方。因为我们当前这个类,它继承于EventDispatcher,所以第一步我们要调用,父类的一个off方法,将所有的事件然后移除,同时将data,我们这里面可能会有一个临时数据仓对象data将它置为null,这是一个。另外重写类,重写对象它的一个子类,我们可以看一个 比如parallel_command,它里面也有一个dispose,本身它要先调用父类,然后再进行它自身的一个清扫工作,还有另外的一个是serial_command,这个里面同样也是,但是它不需要额外的清扫,所以这个地方它没有,这是我们关于dispose的方法。接下来我们要做一个事情,我们要检查一下,在当前我们这个项目里边,当前我们小程序项目里边,我们全局查找一下global全局对象,global是小程序的全局对象。在这个里面首先在这个app.json里边,我们知道这个地方有一个什么,它有一个asyncRetrieveSystemInfo,这样的一个 在全局对象上,然后注册的这样的对象,然后去执行,当它执行完以后,我们需要做一个什么操作,需要做及时的清理,对不对,它在什么时候完成的。我们看一下它的监听是在这个地方,这是它的一个监听onComplete对不对,然后在它完成以后,我们要注意一下,这个地方有一个global.retrieveHomeData.dispose,这有一个释放,然后同时还有一个删除,就是将用delete关键字,将这个对象从我们全局对象然后进行删除,这是一个清理工作,除了这样一个全局对象,另外我们这个里边,还有另外一个。来看一看有一个,再看我们另外的一个地方,retrieveHomeData这个地方它也是一个全局对象,在它的onComplete这个监听,这个回调里边,我们可以看到它也有对于dispose的一个调用,后面也是一个使用delete关键字,进行了一个全局对象引用的删除,这个地方也是有的,这是两个全局对象的一个清理。

另外我们再看我们的组件,我们有一个组件,有一个使用了WebAssembly的一个组件,这个组件位于我们的这个目录下,然后我们看一下它的JS代码,在这个里边我们也使用了global全局对象,然后在上面我们访问了它 对不对,在组件上负责清理的是谁负责清理的是我们stop,就停止的时候我们要对它进行一个清扫,对不对。首先我们看一下我们最终源码里面都有哪些清扫的动作,找一下我们最终的源码,这个地方还没有,我们可以往后找一找,奇怪,这个地方也没有,不管它了 我们自己写一下,找到我们组件,定位到它的JS源码,在这个地方我们可以看一下,这有一个detached,detached它在我们生命周期里边,它是一个反挂载,就是它从页面里面移除的时候,它所需要进行的一个动作,然后它与我们前面的ready它是相反的,我们在挂载的时候,组件准备好的时候,我们要进行初始化,然后组件从页面里边从视图,从DOM树里面进行移除的时候,我们要进行另外一个操作就是把console文档,然后删除,再删除这个全局对象,然后再删除这样的一个全局对象,这些全局对象其实是我们的,在我们源码里面有用到的,并且是由WXWebAssembly,由它帮助我们在全局对象上注册的一些子对象,所以我们在这个地方进行清除,这是关于它的一个清除方式。这个代码其实不太适合放在我们stop里边,stop是在我们 这个页面它隐藏的时候,我们需要停止计时器,停止它执行的时候,我们调stop,但是当这个页面它在显示的时候,我们还需要继续,然后有可能会继续执行这个定时器,所以这个时候我们去清扫不太合适,而只有当我们组件从DOM树里面移除,我们不再需要的时候,然后这个时候我们可以进行清扫了,这个时候是没有问题的,所以我们把清扫代码放在detached放在这样的一个生命周期函数里面去使用,代码演示我们就说到这里。


下面看实践四。

使用this对象要谨慎,this对象在JS中它是一个动态对象,对它的飘忽不定,很多程序员都深恶痛绝,在周期性发生的异步回调函数里面,使用this对象需要特别谨慎,在商品详情页面中对于animation2方法中的动态属性清理工作,如我们屏幕上展示的便使用了this关键字,这个地方的代码是可以优化的,因为绑定的this对象是当前页面,并不是一个简单的对象,优化以后会比优化前在执行时间上大概会少3ms的一个时间。

下面进行实践四的代码演示。

首先我们看一下目前的商品详情页的代码,在里面我们需要找到一个叫做animation2这样的一个方法,这是我们原来设置动画的代码,但是目前这个里面没有,为啥没有。因为可能是我们前面在这个示例演化的过程中给它去掉了,但是没关系,我们可以回到我们的,打开我们的2.4,我们动画是在2.4里面添加的,打开这个目录,这个目录下面我们可以看到这商品详情页,这个里边,它是有animation1,animation2这个代码都是存在的,我们可以将我们的目前的detail,把这个目录给它拷贝一下,然后到我们的目前目录下 到这个下面,然后粘过来,把这个给它删掉,将我们后拷贝的目录的名字给它改一改,改成detail,这就可以了。首先我们要找到animation,animation2这样的一个方法,在这个里边我们往下看,这个地方这是我们传给我们的目前的animate,它的一个参数,最后的一个参数是一个回调函数,然后在这个里面我们是用了一个什么,用了一个this对不对,this因为箭头函数它没有this,所以this它会向上找,找我们当前的页面对象,所以本质上我们就传递过去,传给异步线程的,然后异步线程又执行的这个函数,它其实是一个闭包,它里边裹挟了我们当前这个页面里面的this对象,接下来我们要做一个改进,什么改进,我们传递给我们这个方法,它的最后的参数,这个的参数,这都是它的参数,然后这个参数我们做一个改造,我们在这个上面加一个小括号,然后同时在这个地方,这个关系可能不太对,需要再重新理一下,在这个地方加一个,然后是这个地方加一个,这样就对了。同时在这个对象上面我们加一个bind调用,将this给它传进去,传完以后其实是在我们最后的一个参数上面,我们绑定了this对象给它,绑定this对象给它以后,然后它里面的这个函数它在执行的时候,它就会使用我们绑定的this对象,当然这个地方我们还需要将它改一下,不使用箭头函数,而改用这种普通的function的匿名函数的一个写法,然后传进来,这样的话this它其实取的是我们绑进去的对象了,不是原来我们刚才说的那种闭包的裹挟的那种方式,代码已经改完了,下面我们进行一个测试。

首先我们单击编译按钮,然后我们打开调试区的Performance面板,单击商品详情页,当然我们在这个地方也可以直接选择商品详情页作为启动页,这个地方它目前可能没有办法直接启动,我们可以换成普通编译,现在到了这样的一个页面,然后动画它现在已经执行了,我们现在在Performance面板里面打开录制,我们要看一下它的动画的一个执行表现,大概有一次执行就可以了,然后我们让它停止,我们目前是使用了bind方法对不对,使用了bind方法 所以我们看一下,它里面目前的,它的这个执行 动画的一个执行,大概是用了多长时间,这个地方会有一个Timer Fired,会有一个Timer Fired,然后一次触发,这个时间我们可以大致的看一下,它这个地方也有,这两个是类似的,然后这个时间大概是1.48ms,1.48的ms,现在我们再将我们这个代码给它做一下恢复,找到我们的商品详情页,找到这个位置,最后这一个,我们可以将,这是有两个,将这个往下放一点,然后这个地方我们可以把它拷贝一下,把这个代码给它注掉,然后下面这个代码,把我们刚才添加的再给它注掉,这个地方也拿掉,function也去掉,也变成原来的箭头函数这样的一种写法 ,代码已经恢复了,然后我们再单击编译再测试一下,把原来的结果给它清除一下,然后单击商品页面,动画开始执行了,我们现在单击录制,然后停止,最后我们这有一个触发,我们看一下最后的一次的Timer的一个触发,这个时间也非常短,前面这个时间稍微有点长,大概是2.04,再往前是2.88,整体上比我们平均来讲比我们刚才的时间,稍微有1到1.5的ms这样的一个差别,所以我们这个地方其实未来优化的话这个代码我们是可以将它删掉,还是使用我们这种绑定的方式改回来,然后使用这样的方式,然后再测试一下。


下面看一下小结,在JS编程里面一般很难产生内存泄漏问题,如果产生了多半是因为对引用类型的对象使用不当造成 的,JS中有引用类型也有值类型,对于值类型基本不用担心,我们需要担心的是引用类型,Object是最基本的引用类型,所有的组件,页面对象的实例,原生组件的引用都是引用类型,包括数组也是引用类型,这些类型在使用的时候我们都需要小心,在Memory面板里边的对比视图中,我们查看delta数据的时候也应该多关注引用类型的一个数据增长情况,要确保引用类型的变量,尤其是不断增长的临时变量,不被全局对象,异步线程对象,也就是定时器对象引用或间接引用,在使用完成以后要记得及时进行清理,这节课我们就讲到这里。

点击查看开放文档:


上面的网址是本课涉及的文档地址,这节课我们主要学习了脚本优化技巧,下节课学习setData方法在使用时注意的技巧

最后看一下思考题,这里有个问题请你思考一下,对于列表数据的更新,setData方法的调用也有技巧,如果我们想更新列表中的某一条数据,其实不需要更新整个列表的数据,只需要结合JS对象的计算属性,按照索引更新方法,指定更新列表里边某一条数据就可以了,这种更新传递的数据大小会小很多,你知道这种索引更新的方式如何使用吗?下节课我们就一起来探讨一下这个问题。

最后一次编辑于  2022-07-15
赞 3
收藏

1 个评论

  • 清蒸鱼
    清蒸鱼
    2022-12-20

    ·bind(this)不也是当前页面对象吗?如果不是的话是什么

    2022-12-20
    赞同
    回复
登录 后发表内容

小程序性能优化实践

课程标签