分享开始之前呢,我们先来了解一下DSL,因为小程序就是一种DSL语言。
DSL
DSL
其实是 Domain Specific Language
的缩写,中文翻译为领域特定语言 (下简称 DSL);而与 DSL
相对的就是 GPL
,这里的 GPL
并不是我们知道的开源许可证,而是 General Purpose Language
的简称,即通用编程语言 ,也就是我们非常熟悉的 Objective-C
、Java
、Python
以及 C 语言
等等。
实现 DSL 总共有这么两个需要完成的工作:
- 设计语法和语义,定义
DSL
中的元素是什么样的,元素代表什么意思 - 实现
parser
,对 DSL 解析,最终通过解释器来执行
DSL
最大的好处是简单,通过简化语言中的元素,降低使用者的负担;无论是 Regex、SQL 还是 HTML 以及 CSS,其说明文档往往只有几页,非常易于学习和掌握。但是,由此带来的问题就是,DSL 中缺乏抽象的概念,比如:模块化、变量以及方法等。
双线程模型
小程序的渲染层和逻辑层分别由2个线程管理:
- 渲染层 :界面渲染相关的任务全都在
WebView
里执行。一个小程序存在多个界面,所以渲染层存在多个WebView线程。 - 逻辑层 :采用
JsCore
线程运行JS脚本。
视图层和逻辑层通过系统层的JsBridge
进行通信:逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
(注:图片来自网络)
小程序运行的3大环境区别
运行环境 | 逻辑层 | 渲染层 |
---|---|---|
IOS | JavascriptCore | WKWebview |
Android | V8 | chromium定制内核 |
IDE | NWJS | Chrome WebView |
为什么用双线程设计
因为在用web开发时,遇到了以下3种困境
- 原生能力不足
早在2015年,微信发布了一整套网页开发工具包JS-SDK, 通过开放原生功能API使得 Web 开发者都可以使用到微信的原生能力,例如拍摄、录音、语音识别、二维码等。但是相比原生,交互体验上还是有差距。 - 性能体验不佳
用户在访问网页的时候,在浏览器开始显示之前都会有一个白屏的过程,受限于设备性能和网络速度,白屏会更加明显。为了解决体验问题,微信推出"微信 Web 资源离线存储",类似 HTML5 的 Application Cache,能解决部分白屏问题,但是web的体验上还是和原生有着很大落差,例如页面的切换,页面加载条等。 - 难以管控
web页面自由度很高,需要花很大的人力去检查页面是否存在违规等操作。
双线程主要解决2个问题
体验:
一方面能够保证运行在逻辑线程沙箱内的 JavaScript 代码是线程安全的,另一方面由于渲染线程的计算量非常小从而保证了对用户交互行为的快速响应,提高了用户体验。
管控:
阻止开发者使用浏览器的开发性接口,通过提供一个沙盒环境来运行开发者的js代码,只能使用微信提供开放的方法来获取元素的一些信息。这样就避免开发者的操作不在管控范围。
除了JS用沙盒环境管控,html也改用了封装过的wxml,css改为wxss,为了管控,同时也是为了提供更多功能,例如封装了播放直播的live-player、滚动选择器picker-view。
另外,也提供了wxs(WeiXin Script)让wxml在渲染的时候也可以做一些逻辑处理。
优化setData逻辑
知道了双线程设计后,我们就可以针对这样的数据传输进行一些优化,以下优化摘自支付宝小程序官方文档
任何页面变化都会触发 setData ,同一时间可能会有多个 setData 触发页面进行重新渲染。如下四个接口都会触发 webview 页面重新渲染。
- Page.prototype.setData: 触发整个页面做差异比较
- Page.prototype.$spliceData: 针对长列表做优化,避免每次传递整个列表,触发整个页面做差异比较
- Component.prototype.setData: 只会从对应组件节点开始做差异比较
- Component.prototype.$spliceData: 针对长列表做优化,避免每次传递整个列表,只会从对应组件节点开始做差异比较
优化建议
- 避免频繁触发 setData 或者 $spliceData,不管是页面级别还是组件级别。在我们分析的案例中,有些页面有倒计时逻辑,但是有的倒计时过于频繁触发(ms 级别的触发)。
- 需要频繁触发重新渲染时,避免使用页面级别的 setData 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> s p l i c e D a t a ,将这一块封装成自定义组件,然后使用组件级别的 s e t D a t a 或 spliceData, 将这一块封装成自定义组件,然后使用组件级别的 setData 或 </math>spliceData,将这一块封装成自定义组件,然后使用组件级别的setData或spliceData 触发组件重新渲染。
- 长列表数据触发渲染时,使用 $spliceData多次追加数据,而不用传递整个列表。
- 复杂页面建议封装成自定义组件,减少页面级别的 setData。
推荐
- 指定路径设置数据
kotlin
this.setData({
'array[0]': 1,
'obj.x':2,
});
- 在 for 中使用 key 来提高性能。 注意 key 不能设置在 block 上。
xml
<view a:for="{{array}}" key="{{item.id}}"></view>
<block a:for="{{array}}"><view key="{{item.id}}"></view></block>
不推荐
不推荐如下用法(虽然拷贝了 this.data, 仍然直接更改了其属性)
ini
const array1 = this.data.array.concat();
array[0] = 1;
const obj1={...this.data.obj};
obj.x=2;
this.setData({array1,obj1});
更不推荐
更不推荐直接更改 this.data(违反不可变数据原则)
kotlin
this.data.array[0]=1;
this.data.obj.x=2;
this.setData(this.data)
实现一个简易的小程序DSL
我们根据前面的理论知识,用vue实现一个小程序框架,可以更好的体现生命周期的概念
小程序
javascript
// page.js 逻辑层
export default {
data: {
msg: 'hello Josie',
},
create() {
// 这里的setTimeout是为了可以明显的看到数据变化
setTimeout(() => {
this.setData({
msg: 'setData',
})
}, 1000);
},
}
这里为了方便,先用js代替vxml文件,实际上,你可以通过配置一个webpack loader来处理自定义文件,有兴趣的小伙伴可以尝试一下。
这里,为了更还原小程序代码,你可以自定义组件为view和text来代替div。
dart
// page.vxml.js 渲染层
export default () => {
return '<div>{{msg}}</div>';
}
框架层
构造worker初始化引擎
typescript
// index.worker.js 构造worker
const voxWorker = options => {
const {config} = options;
// Vue生命周期收集
const lifeCircleMap = {
'lifeCircle:create': [config.create],
};
// 定义setData方法用于通知UI层渲染更新
self.setData = (data) => {
console.log('setData called');
self.postMessage(
JSON.stringify({
type: 'update',
data,
})
,
null
);
};
// worker构建完成,通知渲染层初始化
self.postMessage(
JSON.stringify({
type: 'init',
data: config.data,
})
,
null
);
// 执行生命周期函数
self.onmessage = e => {
const {type} = JSON.parse(e.data);
lifeCircleMap[type].forEach(lifeCircle => lifeCircle.call(self))
}
}
export default voxWorker;
上面代码的核心功能是:
- 收集需要用到的生命周期
- 定义setData函数,提供给用户层更新UI
- 定义监听函数,处理生命周期函数执行
- 通知UI进程开启渲染
构造渲染引擎
当我们通知UI进程开始渲染的时候,UI进程也就是需要构造Vue实例,进行页面render:
typescript
worker.onmessage = e => {
const {type, data} = JSON.parse(e.data);
if (type === 'init') {
console.log(data, '---------------init收到消息--------------');
const mountNode = document.createElement('div');
document.getElementById("mini").appendChild(mountNode);
target = new Vue({
el: mountNode,
data: () => data,
template: template(),
created(){
worker.postMessage(JSON.stringify({
type: 'lifeCircle:create',
})
,
null);
}
});
}
}
可以看到UI线程在初始化的时候,一并初始化了 worker
层传递来的data,并对生命周期进行了声明。
当生命周期函数在UI层触发的时候,会通知 worker。
在我们的例子中,create 钩子通过setData 进行了一个更新data的动作。我们知道 setData 就是拿到数据进行通知更新:
ini
// UI线程接收到通知消息,更新UI
if (type === 'update') {
Object.keys(data).map(key => {
target[key] = data[key];
});
}
至此,一个简易的小程序框架已完成,核心实现大概就是这样,后续还可以继续完成,批量更新,传输过程的优化,虚拟DOM Diff等功能。
参考文献:
yuque.antfin.com/jianfang.rj...
yuque.antfin.com/yq01104425/...