在微信小程序诞生之前,最流行的技术是Hybird混合开发技术。Hybird有两个优势 :跨平台和热更新。微信小程序相当于运行在微信这个特定环境的Hybird技术。接下来我们从4个方面讲一讲小程序的运行机制,看一看有哪些创新点。
此外还有,大家都知道wxs是微信自已打造的小程序编程语言。既然js是小程序的主要开发语言,用js已足以,为什么还要发明一个wxs呢?何况这个wxs的功能并不完善,在语法上十分类似于js语言。我们一起探索一下这个问题。
一、启动机制
小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。 假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。
那可能有人问了,小程序从本地读取缓存了,什么时候更新版本?
小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果我们需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。
三种启动方式
图:小程序启动机制图解
我们看一下这张图,小程序的启动情况有三种:
1,首次打开小程序,从微信云端下载小程序包并运行。
2,最近启动过,还在后台运行着,直接将小程序从后台状态切换到前台显示
3,长时间未运行,或被微信主动销毁后,再打开,就是冷启动,这时候直接从本地缓存读取程序包,同时从微信云端检查版本有没有更新。
小程序被主动销毁有两种情况:
1)当小程序进入后台,在后台维持运行,超过一定时间,目前为5分钟,会被微信主动销毁;
2)当短时间内,目前为5s,连续两次以上收到系统内存告警,微信会对小程序进行主动销毁。收到警示语为:
“运行内存不足,请重新打开该小程序”
确定后,小程序就直接退出了,这种体验对用户很不友好。在必要时可以使用 wx.onMemoryWarning 接口,监听内存告警事件,提前做处理。
两种状态
与小程序冷、热启动相关的,是小程序的两种状态:
小程序启动后,界面被展示给用户,此时小程序处于前台状态。
当用户点击右上角胶囊按钮关闭小程序,或者按了设备 Home 键离开微信时,小程序并没有完全终止运行,小程序还可以运行一小段时间,这是进入了后台状态。
当用户再次进入微信或再次打开小程序,小程序又会从后台进入前台状态。
小程序在一定时间内,可以直接从后台切换到前台,这是小程序的热启动机制。这种机制,相当于浏览器打开的标签,未被关闭,只是被切换了,等切换回来马上就能看到。热启动的机制,有助于提升小程序的类App用户体验。
二、双线程架构
为了安全和管控, 小程序使用双线程执行:视图线程和逻辑线程。
其中视图层,主要提供各类组件以渲染界面;逻辑层,主要提供各种API来处理业务逻辑。两者都是通过WeixinJSBridge和底层通信的。
图:小程序双线程示意图
我们看这张小程序双线程示意图,上半部分左边是视图线程,右边是逻辑线程。小程序的能力主要是两块,一个是组件,另一个是Api,分别对应的就是这左右两部分。
视图层和逻辑层,通过下面的WeixinJsBridage与微信native底层进行通信。在事件event与数据data的交互处理上,逻辑层把数据变化通知到视图层,触发视图层更新;视图层再把触发的事件,通知到逻辑层进行业务逻辑的处理。
问题:视图持续更新是怎么实现的?
简单说,是通过setData实现的。
webView.evaluateJavascript("javascript:方法名()",
new ValueCallback() {
@Override
public void onReceiveValue(String value) {
...
}
});
代码:Hybird执行js示例
我们先看一下,在Hybird中native是如何执行js代码的?如上所示,native通过evaluateJavascript这个底层接口,执行了js代码。
在小程序中,setData在底层也是通过evaluateJavascript实现的。在程序中,视图层和逻辑层的数据传输,实际上都是通过WeixinJsBridage,通过原生的 evaluateJavascript 方法实现的。
setData要求更新的数据,首先将其转换为数据字符串,接着将数据字符串内容拼接成 JS 脚本,最后通过evaluateJavascript这个原生方法执行 JS 脚本。
小程序在视图更新上做了一些虚拟DOM的优化,数据到达视图层,这个更新并不是实时的。
问题:使用setData可能遇到哪些问题?
从前面我们可以看出,由于视图线程和逻辑线程分属两个线程,两个线程之间通过前置方法setData驱动的数据交换,还要通过WeixinJsBridage进行中转,这个效率是极期低下的。
所以,有时候 Android 的小程序用户在进行界面滑动时,会感觉到卡顿。这是因为视图线程一直在努力执行渲染,逻辑层发来的更新请求被阻塞了,当这种阻塞达到200毫秒以上的,视图效率便是卡顿。机器性能越差越明显。
卡顿不仅跟更新的频率有关,跟更新的数据量也有关系。当使用setData更新大列表数据时,或更新Size比较大的图片时,也容易出现卡顿。
在 iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,一部分 WKWebView会回收掉,也就是说,曾经打开的小程序页面,将会退出历史记录,回不去了。
问题:微信为什么要造一个wxs?
WXS是WeiXin Script的缩写,是微信打造的一套小程序脚本语言。WXS结合 WXML,可以构建出页面的结构。WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致,虽然他们长得很像。
//js
const app = getApp()
Page({
data: {
motto: 'Hello World',
userInfo: {},
hasUserInfo: false
},
...
})
//wxml
{{tools.bar(motto)}}
{{tools.foo}}
var foo = "'hello world' from comm.wxs";
var bar = function(d) {
return 'hi'+d;
}
module.exports = {
foo: foo,
bar: bar
};
代码:wxs示例
我们看一个wxs示例,如上所示。在这个代码中,tools即是一块wxs代码,在视图wxml代码中,可以直接引用tools模块的变量及方法;同时,视图中还可以绑定js代码中的数据变量。
微信团队宝物,在iOS 中,wxs 会比 js 快 2~20 倍。
这是因为,wxs虽然也是代码,但是它并不运行在小程序的逻辑线程内,而是运行内视图线程内。它操作视图数据,避免了跨线程的通信开销。
因为小程序的双线程架构在数据更新上有瓶颈,所以微信才打造了一个wxs视图脚本。
wxs虽然可以提高视图数据的更新效率,但它也不是没有缺陷。它有这些问题:
1,wxs 的运行环境和其他 js 代码是隔离的,wxs 中不能调用其他 js 文件中定义的函数,也不能调用小程序提供的wx开头的API。
2,wxs 函数不能作为视图模板中的事件回调句柄。
3,由于运行环境的差异,在 iOS 设备上比 js 快 2 ~ 20 倍,但在 android 设备上二者效率几无差异。
三、视图线程
现在,我们单独看一下视图线程,它是如何编译 WXML 与 WXSS 的,是如何完成视图渲染的。
图:小程序视图线程编译图示
小程序有两个与视图有关的编译器:
wcc:wcc *.wxml
wcsc:wcsc *.wxss
wcc是wxml编译器,用于将wxml文件编译为js。编译指令为 wcc .wxml;wcsc是wxss编译器,负责将wxss文件转化为 js。编译指令为 wcsc wxss。
小程序的视图层是在Polymer [ˈpɑːlɪmər]框架的基础上,基于Web Component标准实现的。前面这两步对wxml、wxss的编译完成以后,会在内存中创建一个虚拟DOM,关于运行时在持续更新状态下的数据更新,都是基于虚拟DOM实现的视图渲染。虚拟DOM可以看作是在内存中构建的一个UI组件树,使用虚拟DOM是为了提升渲染效率。
前端开发框架vue也实现了虚拟DOM,并且也是基于Web Component标准实现的。vue将template模板中的每一个节点,都解析为一个个Web Component Node节点,即使是一个独立的像{{matto}}这样的绑定文本,也是解析为一个独立的节点的,这样在数据更新时,更新的是这个节点的文本。
小程序的实现机制,与vue是一样的。
图:小程序一般-原生双层视图层
与vue的实现更进一步的是,小程序除了一般视图组件,还有原生组件。小程序将一般组件放在下面,将解析后生成的原生组件放在一般组件的上面。这是小程序在视图上复杂的地方,在开发时,我们需要注意原生组件与一般组件的遮挡有关系。
四、逻辑线程
看过了视图线程,我们再看一下逻辑线程。
图:小程序页面主要生命周期函数
在小程序的页面生命周期中,有这主要的五个生命周期函数:onLoad、onShow、onReady、onHide、onUnload,这五个周期函数在逻辑线程中都有体现。
图:逻辑线程生命状态图
在从这张图我们可以看到,当一个小程序页面启动时,首先触发的是onLoad事件和onShow,即页面加载完毕了,可以显示了。
当视图初始化装载完成以后,notify通知逻辑线程组件已准备好。
接着逻辑线程将初始的data数据发给视图线程,由视图线程渲染。此时视图进入渲染状态。完成首次渲染以后,视图线程通和逻辑线程完成,派发onReady事件。监听到这个事件,代表页面可以交互了。此时视图进入持续渲染状态。
在运行状态中,如果用户输入了事件,会触发逻辑线程的事件函数,事件函数执行以后,可能又向视图线程通过setData发送更新数据,由视图线程完成更新。
当用户跳转别的小程序时,当前页面进入后台状态,此时逻辑线程派发onHide事件;而当页面由后台切入前台状态时,又会派发onShow事件。如果页面会销毁,则会派发onUnload事件。
这就是与逻辑线程息息相关的6个小程序页面生命周期函数。
从这张图中,我们可以看到视图线程有4个状态,逻辑状态有4个状态。
视图线程的四个状态
1,初始化状态
等初始化完毕,就向服务线程发送初始化完毕信号,然后进入“等待传回初始化数据”状态。
2,首次渲染状态
收到“服务线程”发来的初始化数据后,就开始渲染小程序界面,渲染完毕后,发送“首次渲染完毕信号”给服务线程。此时页面已展示给小程序用户。
3,持续渲染状态
这是用户交互操作的状态。此时界面线程继续一直等待“服务线程”通过setdata函数发送更新数据,只要收到,就进行局部渲染,界面得到更新。
4,结束状态。
逻辑线程的四个状态
1,初始化状态
这个阶段就是启动服务线程所需的基本功能。
系统的初始化工作完毕,就调用自定义的onload和onshow,然后等待界面线程的“界面线程初始化完成”信号。
onload是只会首次渲染的时候执行一次,onshow是每次界面切换都会执行。
2,等待激活状态
接收到“视图线程初始化完成”信号后,将初始化数据发送给“视图线程”,等待界面线程完成初次渲染。
3,激活状态
收到界面线程发送来的“首次渲染完成”信号后,就进入激活状态,即程序的正常运行状态,并调用自定义的onReady函数。
在这个状态下,可以通过setData 函数,发送更新数据给视图线程,以更新页面。
4,后台运行状态
如果界面进入后台,服务线程就进入后台运行状态。在这种情况下,也可以通过setdata函数更新视图。但这其实是应该避免的。
总结
最后总结一下,小程序采用的是双线程架构,一个线程负责视图的渲染,一个线程负责业务逻辑的处理,两个线程都通过WeixinJsBridage与微信native底层进行通信,包括两者之间进行的事件与数据的交换,也是通过它完成的。所有平台能力、硬件能力,也是通过WeixinJsBridage间接提供的。
由于setData在频繁更新和大数据更新上有瓶颈,影响渲染效率,所以微信引入了wxs。一般在开发时,我们这样处理,从后端接口取回来的初始数据,在页面onLoad之前,就塞在data中,用于视图的初始化渲染;后续的视图交互与更新,如果不与后端有关,就使用wxs,直接在视图中完成,以此提高效率。