双线程架构
在这之前,咱们先来思考一个问题,小程序在架构上为什么会抉择双线程?
为什么是双线程?
加载及渲染性能
小程序的设计之初就是要求疾速,这里的快指的是加载以及渲染。
目前支流的渲染形式有以下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渲染线程
如何找到渲染层?
- 咱们能够通过调试微信开发者工具:
微信开发者工具 ->调试 ->调试微信开发者工具
- 而后咱们会再看到一个调试界面,看起来跟咱们平时用的浏览器调试界面简直一摸一样
但这并不是小程序的渲染层,而是开发者工具的构造。但咱们在外面能够发现有一些webview
标签,在第一个webview
上的src属性看着是不是有点眼生,没猜错的话它就是咱们以后小程序关上页面的门路。所以这个webview
才是小程序真正的渲染层。这里你会发现它外面并不只有一个webview
,其实外面蕴含着视图层的webview
,业务逻辑层webview
,开发者工具的webview
开发者工具的逻辑层跑在webview
中次要是为了模仿真机上的双线程
- 关上渲染层一探到底
通过showdevTools
办法来关上调试此webview界面的调试器
document.querySelectorAll('webview')[0].showDevTools(true)
这里咱们看到的才真正是小程序的渲染层,也就是小程序代码编译后的样子,咱们会发现这里的标签都与咱们开发时写的不一样,都对立加了wx-
前缀。理解过webComponent
的同学置信一眼就能看出他们十分类似,但小程序并没有间接应用webComponent
,而是自行搭建了一套组件零碎Exparser
。
Exparser
的组件模型与WebComponents
规范中的Shadow DOM
高度类似。Exparser
会保护整个页面的节点树相干信息,包含节点的属性、事件绑定等,相当于一个简化版的Shadow DOM
实现。
为什么不间接应用webComponent
,而是抉择自行搭建一套组件零碎?
<details>
<summary>点击查看</summary>
- 管控与平安:web技术能够通过脚本获取批改页面敏感内容或者随便跳转其它页面<br/>
- 能力无限:会限度小程序的表现形式<br/>
- 标签泛滥:减少了解老本 </details>
JSCore逻辑线程
逻辑层咱们间接在小程序开发者工具的调试器中输出document
就能看到
小程序将所有业务代码置于同一个线程中运行,在小程序开发者工具中逻辑线程同样是跑在一个webview中;webview中的appservice.html除了引入业务代码js之外,还有后盾服务内嵌的一些根底性能代码。
编译原理
理解完小程序的双线程架构,咱们再来看一下小程序的代码是如何编译运行的,微信开者工具模拟器运行的代码是通过本地预处理、本地编译,而微信客户端运行的代码是额定通过服务器编译的。这里咱们还是以微信开发者工具为例来摸索一番。
在开发者工具输出openVendor()
,会帮咱们关上微信开发者工具的WeappVendor
文件夹
在这里咱们咱们会看到一些wxvpkg
文件,这是小程序的各个版本的根底库文件,还有两个值得咱们留神的文件:wcc
、wcsc
,这两个文件是小程序的编译器,别离用来编译wxml
和wxss
文件。
编译wxml
这里咱们能够将开发者工具中的wcc
编译器拷贝一份进去,尝试去用它编译一下wxml
文件,看看最初的产物是什么?
咱们在终端执行一下以下命令
./wcc -b index.wxml >> wxml_output.js
而后它会在当前目录下生成一个wxml_output.js
文件,文件中有一个十分重要的办法$gwx
,该办法会返回一个函数。该函数的具体作用咱们能够尝试执行一下看看后果。
咱们关上渲染层webview
搜寻一下该办法(为了不便查看,这里会用个小我的项目来演示)
从这里咱们能够看到该办法会传入一个小程序页面的门路,返回的仍然是一个函数
var decodeName = decodeURI("./index/index.wxml")var generateFunc = $gwx(decodeName)
咱们尝试按这里流程执行一下$gwx
返回的函数,看看返回的内容是什么?
<!--compiler-test/index.wxml--><view class="qd_container" > <text name="title">wxml编译</text> <view >{{ name }}</view></view>
const func = $gwx(decodeURI('index.wxml'))console.log(func())
没错,这个函数正是用来生成Virtual DOM
思考:为什么$gwx
不间接生成Virtual DOM
?
<details>
<summary>点击查看</summary>
- 双线程,须要动静注入数据
</details>
编译wxss
咱们同样能够用微信开发者工具中的wcsc
来编译一下wxss
文件。
(大家认为这里应该是会生成css
文件还是js
文件呢?)
咱们在终端执行一下以下命令来编译wxss文件
./wcsc -js index.wxss >> wxss_output.js
相比之前的wcc
编译wxml
文件来说,这次的编译相对来说比较简单,它次要实现了以下内容:
- rpx单位的换算,转换成px
- 提供
setCssToHead
办法将转换好的css增加到head中
rpx动静适配
小程序提供rpx
单位来适配各种尺寸的设施
比方:
/*index.wxss */.qd_container { width: 100rpx; background: skyblue; border: 1rpx solid salmon;}.qd_reader { font-size: 20rpx; color: #191919; font-weight: 400;}
通过编译之后会生成setCssToHead
办法并执行
setCssToHead([".",[1],"qd_container { width: ",[0,100],"; background: skyblue; border: ",[0,1]," solid salmon; }\n.",[1],"qd_reader { font-size: ",[0,20],"; color: #191919; font-weight: 400; }\n",])( typeof __wxAppSuffixCode__ == "undefined"? undefined : __wxAppSuffixCode__ );
外面会调用transformRPX
办法将rpx
转成px
var transformRPX = window.__transformRpx__ || function(number, newDeviceWidth) {if ( number === 0 ) return 0;number = number / BASE_DEVICE_WIDTH * ( newDeviceWidth || deviceWidth );number = Math.floor(number + eps);if (number === 0) {if (deviceDPR === 1 || !isIOS) {return 1;} else {return 0.5;}}return number;}
// 次要公式number = number / BASE_DEVICE_WIDTH * (newDeviceWidth || deviceWidth);number = Math.floor(number + eps); //为了准确// rpx值 / 根底设施宽750 * 实在设施宽
渲染流程
下面理解完wxml
与wxss
的编译过程,咱们再来整体理解一下页面的渲染流程。
先来理解渲染层模版
从下面的渲染层webview
咱们能够找到这两个webview
第一个index/index
webview咱们下面说了它就是对应咱们的小程序的渲染层,也就是真正的小程序页面。
那么上面这个instanceframe.html
是什么呢?
这个webview其实是小程序渲染模版,关上查看一番
它其实就是提前注入了一些页面所须要的公共文件,以及红框内的一些页面独立的文件占位符,这些占位符会等小程序对应页面文件编译实现后注入进来。
如何保障代码的注入是在渲染层webview的初始化之后执行?
在刚刚渲染模版webview
的下方有这样一段脚本:
if (document.readyState === 'complete') { alert("DOCUMENT_READY") } else { const fn = () => { alert("DOCUMENT_READY") window.removeEventListener('load', fn) } window.addEventListener('load', fn) }
很显著,这里在页面初始化实现后,通过alert
来进行告诉。此时的native/nw.js
会拦挡这个alert
,从而晓得此时的webview曾经初始化实现。
整体渲染流程
理解了下面这个重要过程,咱们就能够将整个流程串联起来了
- 关上小程序,创立视图层页的webview时,此时会初始化渲染层
webview
,并且会将该web view地址设置为instanceframe.html
,也就是咱们的渲染层模版 - 而后进入页面
/index/index
,等instanceframe
webview初始化实现,会将页面index/index
编译好的代码注入进来并执行
// 将webview src门路批改为页面门路history.pushState('', '', 'http://127.0.0.1:26444/__pageframe__/index/index')/*... 这里还有一些 wx config及wxss编译后的代码*/// 这里是var decodeName = decodeURI("./index/index.wxml")var generateFunc = $gwx(decodeName)if (decodeName === './__wx__/functional-page.wxml') { generateFunc = function () { return { tag: 'wx-page', children: [], } }}if (generateFunc) { var CE = (typeof __global === 'object') ? (window.CustomEvent || __global.CustomEvent) : window.CustomEvent; document.dispatchEvent(new CE("generateFuncReady", { detail: { generateFunc: generateFunc } })) __global.timing.addPoint('PAGEFRAME_GENERATE_FUNC_READY', Date.now())} else { document.body.innerText = decodeName + " not found" console.error(decodeName + " not found")}
- 此时通过
history.pushState
办法批改webview的src然而webview并不会发送页面申请,并且将调用$gwx
为生成一个generateFun
办法,后面咱们理解到该办法是用来生成虚构dom的 - 而后会判断该办法存在时,通过
document.dispatchEvent
派发发自定义事件generateFuncReady
将generateFunc当作参数传递给底层渲染库 - 而后在底层渲染库
WAWebview.js
中会监听自定义事件generateFuncReady
,而后通过 WeixinJSBridge 告诉 JS 逻辑层视图曾经筹备好()
- 最初 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钩子?
mounted() { this.name = '前端南玖' console.log('sync',this.$refs.title.innerText) // 旧文案 // 新文案 Promise.resolve().then(() => { console.log('微工作',this.$refs.title.innerText) }) setTimeout(() => { console.log('宏工作',this.$refs.title.innerText) }, 0) this.$nextTick(() => { console.log('nextTick',this.$refs.title.innerText) })}
这里举荐浏览这篇理解更多:Vue异步更新机制以及$nextTick原理
然而小程序却没有这个队列概念,频繁调用,视图会始终更新,阻塞用户交互,引发性能问题。
而Vue 每次赋值操作并不会间接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,在同一个tick
内屡次赋值,也只会渲染一次。
原文首发地址点这里,欢送大家关注公众号 「前端南玖」,如果你想进前端交换群一起学习,请点这里
我是南玖,咱们下期见!!!