小程序现在如日中天,各大公司都推出了自己的小程序平台,目前看来运行机制都差不多,数据形成视图,渲染和逻辑分成两个线程,交互通过线程通信实现。
刚开始接触小程序开发的时候,看到小程序的语法觉得很奇怪。看着像react和vue的结合体,疑惑为什么要这么费力的实现这么一套机制。难道是为了体现技术nb? 用了一会就发现问题了,照搬pc开发的那套思想,特么小程序里不支持dom相关的api,很不方便。翻了很多遍微信和支付宝小程序的官方文档,终于有了一点理解。
本文大部分是官方文档引用加上自己一点总结。
web开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应。开发者可以使用到各种浏览器暴露出来的 DOM API,进行 DOM 选中和操作。 而在小程序中,二者是分开的,分别运行在不同的线程中,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。
由于支付宝官方文档说明过于简略,本文结合了微信和支付宝小程序的特点总结,结合如有差异,欢迎指正。
这部分在 微信 和支付宝小程序官方文档都有说明
由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和GUI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
小程序分为 app 和 page 两层。app 用来描述整个应用,page 用来描述各个页面。
app 由三个文件组成,必须放在项目的根目录。
文件 | 必需 | 作用 |
---|---|---|
app.js | 是 | 小程序逻辑 |
app.json | 是 | 小程序全局设置 |
app.acss | 否 | 小程序全局样式表 |
page 由四个文件组成,分别是:
文件 | 必需 | 作用 |
---|---|---|
app.js | 是 | 页面逻辑 |
app.axml | 是 | 页面结构 |
app.json | 否 | 页面配置 |
app.acss | 否 | 页面样式 |
小程序的逻辑层和渲染层是分开的两个线程,小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS脚本工作在逻辑层。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。
小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由客户端做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型下图所示。
从逻辑组成来说,一个小程序是由多个“页面”组成的“程序”。宿主环境提供了 App() 构造器用来注册一个程序App,需要留意的是App() 构造器必须写在项目根目录的app.js里,App实例是单例对象,在其他JS脚本中可以使用宿主环境提供的 getApp() 来获取程序实例。
小程序开发框架的逻辑层使用 JavaScript 引擎为小程序提供开发者 JavaScript 代码的运行环境以及微信小程序的特有功能。 逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。 开发者写的所有代码最终将会打包成一份 JavaScript 文件,并在小程序启动的时候运行,直到小程序销毁。这一行为类似 ServiceWorker,所以逻辑层也称之为 App Service。
const app = getApp();
复制代码
参考资料: 微信1 、 微信2 、 微信3
小程序的 JavaScript 代码分为逻辑层脚本和 sjs/wxs 脚本
支付宝文档 说sjs和逻辑层运行在相同的 JavaScript 引擎的不同线程中。
微信文档 又表示wxs是运行在webview中的,并且提供了更为强大的功能:如果在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍,在 android 设备上二者运行效率无差异;减少通信的次数,让事件在视图层(Webview)响应;用来响应小程序事件,目前只能响应内置组件的事件,不支持自定义组件事件;还能调用逻辑层的事件;
对两者sjs/wxs差别表示吃瓜状态,支付宝的sjs明显功能弱了很多,毕竟鸡肋,一般用来模拟vue里的compued功能使用,无法作为事件回调。微信的wxs提供了更强大的支持。
试了下sjs不带类似computed缓存功能
两个平台都会对新预发的代码进行 babel 转换,使 JavaScript 引擎支持绝大多数 ES6 的新特性,但是对于内置对象未提供完全的Polyfill,具体支持情况可以查阅 支付宝 、 微信。
小程序中,有一些组件其实是调用原生组件的,如map、video等,这些复杂交互的控件,原生能带来更好的性能与原生体验。
简单说就是在期望插入原生控件的位置渲染一个HTML元素,拿到此DOM的位置,客户端在相同的位置上,根据宽高插入一块原生区域,位置或宽高发生变化时,组件会通知客户端做相应的调整。 可以直接看 官方文档
微信小程序的官方介绍 很全面了
回到标题的问题。因为逻辑层Service中的代码与WebView中的代码完全隔离,JavaScriptCore中并没有document,window等对象(ECMAScript标准没有规定DOM,这其实是浏览器提供的)。js和视图(dom所在)没有运行在同一容器中。
很好理解,小程序初次启动时,客户端需要从 CDN 下载小程序资源包,此后,如果小程序代码包未更新且还被保留在缓存中,则下载小程序代码包的步骤会被跳过。可以做的:
每一次setData都是线程通信
支付宝小程序提供了$batchedUpdates
this.$batchedUpdates(() => {
this.setData({
counter: this.data.counter + 1,
});
this.setData({
counter: this.data.counter + 1,
});
});
复制代码
setData是线程通信传递数据,传输时数据需要序列化,框架提供了指定路径设置数据的方便,避免一次传输完整数据。
this.setData({
'array[0]': 1,
'obj.x':2,
});
复制代码
针对长列表, 支付宝小程序 提供了优化方法$spliceData,使用方式对应js数组的splice
this.$spliceData({ 'a.b': [1, 0, 5, 6] })
复制代码
针对长列表做优化,避免每次传递整个列表,只会从对应组件节点开始做差异比较
小程序中事件响应也需要通过线程通信,如果频繁的触发可能会造成卡顿。例如页面有 2 个元素 A 和 B,用户在 A 上做 touchmove 手势,要求 B 也跟随移动。一次 touchmove 事件的响应过程为:
a、touchmove 事件从视图层(Webview)抛到逻辑层(App Service)
b、逻辑层(App Service)处理 touchmove 事件,再通过 setData 来改变 B 的位置
微信小程序里可以使用wxs响应事件优化,wxs是运行在webview中的,不需要跨线程通信。WXS 函数的除了纯逻辑的运算,还可以通过封装好的ComponentDescriptor 实例来访问以及设置组件的 class 和样式,对于交互动画,设置 style 和 class 足够了:
const wxsFunction = function(event, ownerInstance) {
const instance = ownerInstance.selectComponent('.classSelector') // 返回组件的实例
instance.setStyle({
"font-size": "14px" // 支持rpx
})
instance.getDataset()
instance.setClass(className)
// ...
return false // 不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault
}
复制代码
一次 touchmove 的响应需要经过 2 次的逻辑层和渲染层的通信以及一次渲染,通信的耗时比较大。此外 setData 渲染也会阻塞其它脚本执行,导致了整个用户交互的动画过程会有延迟。 官方性能优化文档: 微信 、 支付宝 ;