背景
最近在写微信小程序的loading组件时,发现弱网模式下,loading的状态出现的很慢。
我期望的效果如下:点击发起某个请求时,页面中立马出现一个loading的提示。
实现逻辑非常简单,点击按钮时通过setData让这个文字变成【loading】状态,并发起一个ajax请求去服务器请求数据。
在不同的网络状态下,这个效果有不同的表现:
- 去掉wx.request时,loading状态会切换得非常快(这个很好理解,都不需要网络了...)。
- 使用wx.request但是网络很好时,loading状态切换得也很快。
- 但是,在极端弱网模式下,等了几秒钟才出现这个loading,体验很差。
由此可以判断,从执行wx.request到真正的ajax请求发出前,中间还存在一个执行过程会阻塞页面渲染。
看到这里,你可能会有一些疑惑:
原因排查
首先在开发工具的【network】自定义一个1kb的弱网模式,loading状态渲染的效果会更明显。
在【performance】录制,找到影响耗时的那个long task
最终定位到uni.request(在微信小程序的环境下,也就是wx.request),由此可以确定,是wx.request阻塞了这次的渲染。
我用的框架是uni app,所以后面的代码中都是uni app和vue的代码。这个和我们今天要讨论的问题是没有关系的,你使用微信小程序原生代码也可以测试出来一样的结果。
与浏览器的请求对比
如果你在浏览器中发起一个ajax请求,打开【network】时会发现,这个ajax请求是会实时在network中显示的。即使你现在是弱网模式,顶多也就是在【pending】这个状态呆的久一些而已。
但是微信小程序中又是如何表现的呢?
细心的小伙伴可以发现,在我这张uni.request的代码图中,打了一句日志。
发起ajax请求的时候,在控制台可以看到日志已经正常打印出来了。
但是network中的请求等了几秒才开始出现,然后又pending了几秒,请求才返回结果。
这里可能又有小伙伴会有疑惑了:会不会只是微信开发工具中的【network】出现请求比较慢,实际上已经开始请求了呢?
小程序宿主环境
这里以微信小程序为例,那他的宿主环境就是微信。
小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型下图所示。
前面也提到小程序是通过setData
来更新页面的数据,而从【逻辑层】到【视图层】会存在一个数据通信的过程。
更多关于setData的注意事项可以看微信开发文档:developers.weixin.qq.com/miniprogram...
看到这里,相信大家已经已经有初步的头绪了,我们再来重新汇总一下线索:
1、发出请求之前,重置loading的状态,这个js代码在【逻辑层】(JsCore)
2、发起请求的代码(也就是wx.request),也在【逻辑层】(JsCore)
3、从JsCore到Https请求之间,隔了一个【Native】
4、在JsCore通过setData来改变view时,也需要经过【Native】才能完成渲染。
所以,在弱网的环境下,可以把阻塞渲染的范围缩小到【Native】这一层。
小程序【Native】
关于Native这一层的官方并没有太多介绍,通常可以认为他就是微信客户端,可以直接与设备的底层操作系统交互。
开发过混合应用或者微信H5页面的小伙伴对【JSBridge】这个东西应该都不陌生,【Native】中就集成了【JSBridge】和微信的能力,开发者可以通过wx.xxx
这种方式来调用微信的sdk。
所以当我使用wx.request时,会进到【Native】这一层,执行【JSBridge】中对应的【https request sdk】。虽然这个【https request sdk】对于调用者来说是黑盒的。但根据以上的测试,该sdk中必然存在一段同步逻辑,而且这段逻辑是依赖于网络的(有可能是检查网络之类的),所以才会阻塞了JsCore到视图层的通信。
如果有小伙伴了解更多关于【Native】的信息,欢迎一起交流~
如何解决?
这个解决其实很简单,只需要用一个异步任务把wx.request包起来即可。由于我用的是uni app,所以用的是nextTick,大家可以根据情况选择对应的微任务/宏任务。
javascript
nextTick(() => {
wx.request()
});
拓展:浏览器进程和线程
在浏览器的渲染进程中,GUI线程和JS引擎线程是2个不同的线程,而且彼此互斥,所以在JS引擎线程运行的时候,GUI线程是处于冻结状态的。
例子1:同步代码执行
javascript
// 页面
<template>
{{loading}}
</template>
// js代码
<script>
const loading = ref(false)
// 位置1
loading.value = true
// 执行了一段耗时3秒的逻辑
for(let i = 0; i < 非常大的数值; i++) {
}
// 位置2
loading.value = true
</script>
思考2个问题:
1、在【位置1】改变loading状态时,页面会立马渲染吗?
2、无论是【位置1】还是【位置2】,loading状态的变更都是3秒后吗?
问题1的答案是【不会】,问题2的答案是【是】。
这个其实很好理解,当我执行很耗时的一段js同步代码时,就会阻塞页面的渲染。
例子2:异步代码执行
还是上面的例子,但是耗时的逻辑改为在ajax异步请求的回调中执行。
javascript
// 页面
<template>
{{loading}}
</template>
// js代码
<script>
const loading = ref(false)
loading.value = true
// 执行了一个ajax回调
ajax().then(() => {
// 这是一段很耗时的逻辑
})
</script>
可以发现,页面的loading状态立马就改变了。
原因也很简单,发起ajax请求的时候,js引擎线程的任务已经执行完了,所以会直接走到GUI渲染线程的执行。而ajax异步请求属于【事件触发线程】,不会阻塞页面的渲染。
更多关于浏览器进程和线程的可以看:juejin.cn/post/699184...
很感谢大佬们能看到最后,标题的【面试官】是我用来测试一波是不是流量密码的,请无视他