双线程架构
在这之前,我们先来思考一个问题,小程序在架构上为什么会选择双线程?
为什么是双线程?
加载及渲染性能
小程序的设计之初就是要求快速,这里的快指的是加载以及渲染。
目前主流的渲染方式有以下3种:
- Web技术渲染
- Native技术渲染
- Hybrid技术渲染(同时使用了webview和原生来渲染)
从小程序的定位来讲,它就不可能用纯原生技术来进行开发,因为那样它的编译以及发版都得跟随微信,所以需要像Web技术那样,有一份随时可更新的资源包放在远程,通过下载到本地,动态执行后即可渲染出界面。
但如果用纯web技术来开发的话,会有一个很致命的缺点那就是在 Web 技术中,UI渲染跟 JavaScript 的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源,这也就跟设计之初要求的快相违背了。
因此微信小程序选择了Hybrid 技术,界面主要由成熟的 Web 技术渲染,辅之以大量的接口提供丰富的客户端原生能力。同时,每个小程序页面都是用不同的WebView去渲染,这样可以提供更好的交互体验,更贴近原生体验,也避免了单个WebView的任务过于繁重。
微信小程序是以webview渲染为主,原生渲染为辅的混合渲染方式。
管控安全
由于web技术的灵活开放特点,如果基于纯web技术来渲染小程序的话,势必会存在一些不可控因素和安全风险。
为了解决安全管控的问题,小程序从设计上就阻止了开发者去使用一些浏览器提供的开放性api,比如说跳转页面、操作DOM等等。如果把这些东西一个一个地去加入到黑名单,那么势必会陷入一个非常糟糕的循环,因为浏览器的接口也非常丰富,那么就很容易遗漏一些危险的接口,而且就算是禁用掉了所有的接口,也防不住浏览器内核的下次更新。
所以要彻底解决这个问题,必须提供一个沙箱环境来运行开发者的JavaScript 代码。这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。那么像HTML5中的ServiceWorker、WebWorker特性就符合这样的条件,这两者都是启用另一线程来执行 javaScript。
这就是小程序双线程模型的由来:
- 渲染层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以渲染层存在多个 WebView。
- 逻辑层:创建一个单独的线程去执行 JavaScript,在这个环境下执行的都是有关小程序业务逻辑的代码。
双线程模型
小程序的架构模型有别与传统web单线程架构,小程序为双线程架构。
微信小程序的渲染层与逻辑层分别由两个线程管理,渲染层的界面使用 webview 进行渲染;逻辑层采用 JSCore运行JavaScript代码。
webview渲染线程
如何找到渲染层?
1.我们可以通过调试微信开发者工具:微信开发者工具 ->调试 ->调试微信开发者工具
2.然后我们会再看到一个调试界面,看起来跟我们平时用的浏览器调试界面几乎一摸一样
但这并不是小程序的渲染层,而是开发者工具的结构。但我们在里面可以发现有一些webview标签,在第一个webview上的src属性看着是不是有点眼熟,没猜错的话它就是我们当前小程序打开页面的路径。所以这个webview才是小程序真正的渲染层。这里你会发现它里面并不只有一个webview,其实里面包含着视图层的webview,业务逻辑层webview,开发者工具的webview。
开发者工具的逻辑层跑在webview中主要是为了模拟真机上的双线程
3.打开渲染层一探究竟
通过showdevTools方法来打开调试此webview界面的调试器
这里我们看到的才真正是小程序的渲染层,也就是小程序代码编译后的样子,我们会发现这里的标签都与我们开发时写的不一样,都统一加了wx-前缀。了解过webComponent的同学相信一眼就能看出他们非常相似,但小程序并没有直接使用webComponent,而是自行搭建了一套组件系统Exparser。
Exparser的组件模型与WebComponents标准中的Shadow DOM高度相似。Exparser会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的Shadow DOM实现。
为什么不直接使用webComponent,而是选择自行搭建一套组件系统?
点击查看:
- 管控与安全:web技术可以通过脚本获取修改页面敏感内容或者随意跳转其它页面
- 能力有限:会限制小程序的表现形式
- 标签众多:增加理解成本
JSCore逻辑线程
逻辑层我们直接在小程序开发者工具的调试器中输入document就能看到。
小程序将所有业务代码置于同一个线程中运行,在小程序开发者工具中逻辑线程同样是跑在一个webview中;webview中的appservice.html除了引入业务代码js之外,还有后台服务内嵌的一些基础功能代码。
编译原理
了解完小程序的双线程架构,我们再来看一下小程序的代码是如何编译运行的,微信开者工具模拟器运行的代码是经过本地预处理、本地编译,而微信客户端运行的代码是额外经过服务器编译的。这里我们还是以微信开发者工具为例来探索一番。
在开发者工具输入openVendor(),会帮我们打开微信开发者工具的WeappVendor文件夹
在这里我们我们会看到一些wxvpkg文件,这是小程序的各个版本的基础库文件,还有两个值得我们注意的文件:wcc、wcsc,这两个文件是小程序的编译器,分别用来编译wxml和wxss文件。
编译wxml
这里我们可以将开发者工具中的wcc编译器拷贝一份出来,尝试去用它编译一下wxml文件,看看最后的产物是什么?
我们在终端执行一下以下命令
然后它会在当前目录下生成一个wxml_output.js文件,文件中有一个非常重要的方法$gwx,该方法会返回一个函数。该函数的具体作用我们可以尝试执行一下看看结果。
我们打开渲染层webview搜索一下该方法(为了方便查看,这里会用个小项目来演示)
从这里我们可以看到该方法会传入一个小程序页面的路径,返回的依然是一个函数
我们尝试按这里流程执行一下$gwx返回的函数,看看返回的内容是什么?
没错,这个函数正是用来生成Virtual DOM。
编译wxss
我们同样可以用微信开发者工具中的wcsc来编译一下wxss文件。
(大家认为这里应该是会生成css文件还是js文件呢?)
我们在终端执行一下以下命令来编译wxss文件
相比之前的wcc编译wxml文件来说,这次的编译相对来说比较简单,它主要完成了以下内容:
- rpx单位的换算,转换成px
- 提供setCssToHead方法将转换好的css添加到head中
rpx动态适配
小程序提供rpx单位来适配各种尺寸的设备。
比如:
经过编译之后会生成setCssToHead方法并执行
里面会调用transformRPX方法将rpx转成px
渲染流程
上面了解完wxml与wxss的编译过程,我们再来整体了解一下页面的渲染流程。
先来了解渲染层模版
从上面的渲染层webview我们可以找到这两个webview
第一个index/indexwebview我们上面说了它就是对应我们的小程序的渲染层,也就是真正的小程序页面。
那么下面这个instanceframe.html是什么呢?
这个webview其实是小程序渲染模版,打开查看一番
它其实就是提前注入了一些页面所需要的公共文件,以及红框内的一些页面独立的文件占位符,这些占位符会等小程序对应页面文件编译完成后注入进来。
如何保证代码的注入是在渲染层webview的初始化之后执行?
在刚刚渲染模版webview的下方有这样一段脚本:
很明显,这里在页面初始化完成后,通过alert来进行通知。此时的native/nw.js会拦截这个alert,从而知道此时的webview已经初始化完成。
整体渲染流程
了解了上面这个重要过程,我们就可以将整个流程串联起来了。
1.打开小程序,创建视图层页的webview时,此时会初始化渲染层webview,并且会将该web view地址设置为instanceframe.html,也就是我们的渲染层模版。
2.然后进入页面/index/index,等instanceframewebview初始化完成,会将页面index/index编译好的代码注入进来并执行。
3.此时通过history.pushState方法修改webview的src但是webview并不会发送页面请求,并且将调用$gwx为生成一个generateFun方法,前面我们了解到该方法是用来生成虚拟dom的。
4.然后会判断该方法存在时,通过document.dispatchEvent 派发发自定义事件generateFuncReady 将generateFunc当作参数传递给底层渲染库。
5.然后在底层渲染库WAWebview.js中会监听自定义事件generateFuncReady ,然后通过 WeixinJSBridge 通知 JS 逻辑层视图已经准备好()。
6.最后 JS 逻辑层将数据给 Webview 渲染层,WAWebview.js在通过virtual dom生成真实dom过程中,它会挂载到页面的document.body上,至此一个页面的渲染流程就结束了。
数据更新
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。
在架构上,WebView 和 JS Core 都是独立的模块,并不具备数据直接共享的通道。所以在更新数据时必须调用setData来通知渲染层做更新。
setData
- 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
- 将 data 从逻辑层传输到视图层;
- 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新。
这里第二步由于WebView 和 JS Core 都是独立的模块,数据传输是通过 evaluateJavascript 实现的,还会有额外 JS 脚本解析和执行的耗时因此数据到达渲染层是异步的。
因此切记:
- 不要频繁的去setData
- 不要每次 setData 都传递大量新数据(单次stringify后不超过256kb)
- 不要对后台态页面进行setData,会抢占正在执行的前台页面的资源
与Vue对比(再来看看Vue)
整体来讲,小程序身上或多或少都有着vue的影子...(模版文件,data,指令,虚拟dom,生命周期等)
但在数据更新这里,小程序却与Vue表现的截然不同。
1.页面更新DOM是同步的还是异步的?
2.既然更新DOM是个同步的过程,为什么Vue中还会有nextTick钩子?