新聞中心
雙線程架構(gòu)
在這之前,我們先來思考一個(gè)問題,小程序在架構(gòu)上為什么會選擇雙線程?

成都創(chuàng)新互聯(lián)公司成立于2013年,先為岳陽等服務(wù)建站,岳陽等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為岳陽企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
為什么是雙線程?
加載及渲染性能
小程序的設(shè)計(jì)之初就是要求快速,這里的快指的是加載以及渲染。
目前主流的渲染方式有以下3種:
- Web技術(shù)渲染
- Native技術(shù)渲染
- Hybrid技術(shù)渲染(同時(shí)使用了webview和原生來渲染)
從小程序的定位來講,它就不可能用純原生技術(shù)來進(jìn)行開發(fā),因?yàn)槟菢铀木幾g以及發(fā)版都得跟隨微信,所以需要像Web技術(shù)那樣,有一份隨時(shí)可更新的資源包放在遠(yuǎn)程,通過下載到本地,動態(tài)執(zhí)行后即可渲染出界面。
但如果用純web技術(shù)來開發(fā)的話,會有一個(gè)很致命的缺點(diǎn)那就是在 Web 技術(shù)中,UI渲染跟 JavaScript 的腳本執(zhí)行都在一個(gè)單線程中執(zhí)行,這就容易導(dǎo)致一些邏輯任務(wù)搶占UI渲染的資源,這也就跟設(shè)計(jì)之初要求的快相違背了。
因此微信小程序選擇了Hybrid 技術(shù),界面主要由成熟的 Web 技術(shù)渲染,輔之以大量的接口提供豐富的客戶端原生能力。同時(shí),每個(gè)小程序頁面都是用不同的WebView去渲染,這樣可以提供更好的交互體驗(yàn),更貼近原生體驗(yàn),也避免了單個(gè)WebView的任務(wù)過于繁重。
微信小程序是以webview渲染為主,原生渲染為輔的混合渲染方式。
管控安全
由于web技術(shù)的靈活開放特點(diǎn),如果基于純web技術(shù)來渲染小程序的話,勢必會存在一些不可控因素和安全風(fēng)險(xiǎn)。
為了解決安全管控的問題,小程序從設(shè)計(jì)上就阻止了開發(fā)者去使用一些瀏覽器提供的開放性api,比如說跳轉(zhuǎn)頁面、操作DOM等等。如果把這些東西一個(gè)一個(gè)地去加入到黑名單,那么勢必會陷入一個(gè)非常糟糕的循環(huán),因?yàn)闉g覽器的接口也非常豐富,那么就很容易遺漏一些危險(xiǎn)的接口,而且就算是禁用掉了所有的接口,也防不住瀏覽器內(nèi)核的下次更新。
所以要徹底解決這個(gè)問題,必須提供一個(gè)沙箱環(huán)境來運(yùn)行開發(fā)者的JavaScript 代碼。這個(gè)沙箱環(huán)境只提供純 JavaScript 的解釋執(zhí)行環(huán)境,沒有任何瀏覽器相關(guān)接口。那么像HTML5中的ServiceWorker、WebWorker特性就符合這樣的條件,這兩者都是啟用另一線程來執(zhí)行 javaScript。
這就是小程序雙線程模型的由來:
- 渲染層:界面渲染相關(guān)的任務(wù)全都在 WebView 線程里執(zhí)行,通過邏輯層代碼去控制渲染哪些界面。一個(gè)小程序存在多個(gè)界面,所以渲染層存在多個(gè) WebView。
- 邏輯層:創(chuàng)建一個(gè)單獨(dú)的線程去執(zhí)行 JavaScript,在這個(gè)環(huán)境下執(zhí)行的都是有關(guān)小程序業(yè)務(wù)邏輯的代碼。
雙線程模型
小程序的架構(gòu)模型有別與傳統(tǒng)web單線程架構(gòu),小程序?yàn)殡p線程架構(gòu)。
微信小程序的渲染層與邏輯層分別由兩個(gè)線程管理,渲染層的界面使用 webview 進(jìn)行渲染;邏輯層采用 JSCore運(yùn)行JavaScript代碼。
webview渲染線程
如何找到渲染層?
1.我們可以通過調(diào)試微信開發(fā)者工具:微信開發(fā)者工具 ->調(diào)試 ->調(diào)試微信開發(fā)者工具
2.然后我們會再看到一個(gè)調(diào)試界面,看起來跟我們平時(shí)用的瀏覽器調(diào)試界面幾乎一摸一樣
但這并不是小程序的渲染層,而是開發(fā)者工具的結(jié)構(gòu)。但我們在里面可以發(fā)現(xiàn)有一些webview標(biāo)簽,在第一個(gè)webview上的src屬性看著是不是有點(diǎn)眼熟,沒猜錯的話它就是我們當(dāng)前小程序打開頁面的路徑。所以這個(gè)webview才是小程序真正的渲染層。這里你會發(fā)現(xiàn)它里面并不只有一個(gè)webview,其實(shí)里面包含著視圖層的webview,業(yè)務(wù)邏輯層webview,開發(fā)者工具的webview。
開發(fā)者工具的邏輯層跑在webview中主要是為了模擬真機(jī)上的雙線程
3.打開渲染層一探究竟
通過showdevTools方法來打開調(diào)試此webview界面的調(diào)試器
document.querySelectorAll('webview')[0].showDevTools(true)這里我們看到的才真正是小程序的渲染層,也就是小程序代碼編譯后的樣子,我們會發(fā)現(xiàn)這里的標(biāo)簽都與我們開發(fā)時(shí)寫的不一樣,都統(tǒng)一加了wx-前綴。了解過webComponent的同學(xué)相信一眼就能看出他們非常相似,但小程序并沒有直接使用webComponent,而是自行搭建了一套組件系統(tǒng)Exparser。
Exparser的組件模型與WebComponents標(biāo)準(zhǔn)中的Shadow DOM高度相似。Exparser會維護(hù)整個(gè)頁面的節(jié)點(diǎn)樹相關(guān)信息,包括節(jié)點(diǎn)的屬性、事件綁定等,相當(dāng)于一個(gè)簡化版的Shadow DOM實(shí)現(xiàn)。
為什么不直接使用webComponent,而是選擇自行搭建一套組件系統(tǒng)?
點(diǎn)擊查看:
- 管控與安全:web技術(shù)可以通過腳本獲取修改頁面敏感內(nèi)容或者隨意跳轉(zhuǎn)其它頁面
- 能力有限:會限制小程序的表現(xiàn)形式
- 標(biāo)簽眾多:增加理解成本
JSCore邏輯線程
邏輯層我們直接在小程序開發(fā)者工具的調(diào)試器中輸入document就能看到。
小程序?qū)⑺袠I(yè)務(wù)代碼置于同一個(gè)線程中運(yùn)行,在小程序開發(fā)者工具中邏輯線程同樣是跑在一個(gè)webview中;webview中的appservice.html除了引入業(yè)務(wù)代碼js之外,還有后臺服務(wù)內(nèi)嵌的一些基礎(chǔ)功能代碼。
編譯原理
了解完小程序的雙線程架構(gòu),我們再來看一下小程序的代碼是如何編譯運(yùn)行的,微信開者工具模擬器運(yùn)行的代碼是經(jīng)過本地預(yù)處理、本地編譯,而微信客戶端運(yùn)行的代碼是額外經(jīng)過服務(wù)器編譯的。這里我們還是以微信開發(fā)者工具為例來探索一番。
在開發(fā)者工具輸入openVendor(),會幫我們打開微信開發(fā)者工具的WeappVendor文件夾
在這里我們我們會看到一些wxvpkg文件,這是小程序的各個(gè)版本的基礎(chǔ)庫文件,還有兩個(gè)值得我們注意的文件:wcc、wcsc,這兩個(gè)文件是小程序的編譯器,分別用來編譯wxml和wxss文件。
編譯wxml
這里我們可以將開發(fā)者工具中的wcc編譯器拷貝一份出來,嘗試去用它編譯一下wxml文件,看看最后的產(chǎn)物是什么?
我們在終端執(zhí)行一下以下命令
./wcc -b index.wxml >> wxml_output.js
然后它會在當(dāng)前目錄下生成一個(gè)wxml_output.js文件,文件中有一個(gè)非常重要的方法$gwx,該方法會返回一個(gè)函數(shù)。該函數(shù)的具體作用我們可以嘗試執(zhí)行一下看看結(jié)果。
我們打開渲染層webview搜索一下該方法(為了方便查看,這里會用個(gè)小項(xiàng)目來演示)
從這里我們可以看到該方法會傳入一個(gè)小程序頁面的路徑,返回的依然是一個(gè)函數(shù)
var decodeName = decodeURI("./index/index.wxml")
var generateFunc = $gwx(decodeName)我們嘗試按這里流程執(zhí)行一下$gwx返回的函數(shù),看看返回的內(nèi)容是什么?
wxml編譯
{{ name }}
const func = $gwx(decodeURI('index.wxml'))
console.log(func())沒錯,這個(gè)函數(shù)正是用來生成Virtual DOM。
編譯wxss
我們同樣可以用微信開發(fā)者工具中的wcsc來編譯一下wxss文件。
(大家認(rèn)為這里應(yīng)該是會生成css文件還是js文件呢?)
我們在終端執(zhí)行一下以下命令來編譯wxss文件
./wcsc -js index.wxss >> wxss_output.js
相比之前的wcc編譯wxml文件來說,這次的編譯相對來說比較簡單,它主要完成了以下內(nèi)容:
- rpx單位的換算,轉(zhuǎn)換成px
- 提供setCssToHead方法將轉(zhuǎn)換好的css添加到head中
rpx動態(tài)適配
小程序提供rpx單位來適配各種尺寸的設(shè)備。
比如:
/*index.wxss */
.qd_container {
width: 100rpx;
background: skyblue;
border: 1rpx solid salmon;
}
.qd_reader {
font-size: 20rpx;
color: #191919;
font-weight: 400;
}
經(jīng)過編譯之后會生成setCssToHead方法并執(zhí)行
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__ );里面會調(diào)用transformRPX方法將rpx轉(zhuǎn)成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值 / 基礎(chǔ)設(shè)備寬750 * 真實(shí)設(shè)備寬
渲染流程
上面了解完wxml與wxss的編譯過程,我們再來整體了解一下頁面的渲染流程。
先來了解渲染層模版
從上面的渲染層webview我們可以找到這兩個(gè)webview
第一個(gè)index/indexwebview我們上面說了它就是對應(yīng)我們的小程序的渲染層,也就是真正的小程序頁面。
那么下面這個(gè)instanceframe.html是什么呢?
這個(gè)webview其實(shí)是小程序渲染模版,打開查看一番
它其實(shí)就是提前注入了一些頁面所需要的公共文件,以及紅框內(nèi)的一些頁面獨(dú)立的文件占位符,這些占位符會等小程序?qū)?yīng)頁面文件編譯完成后注入進(jìn)來。
如何保證代碼的注入是在渲染層webview的初始化之后執(zhí)行?
在剛剛渲染模版webview的下方有這樣一段腳本:
if (document.readyState === 'complete') {
alert("DOCUMENT_READY")
} else {
const fn = () => {
alert("DOCUMENT_READY")
window.removeEventListener('load', fn)
}
window.addEventListener('load', fn)
}很明顯,這里在頁面初始化完成后,通過alert來進(jìn)行通知。此時(shí)的native/nw.js會攔截這個(gè)alert,從而知道此時(shí)的webview已經(jīng)初始化完成。
整體渲染流程
了解了上面這個(gè)重要過程,我們就可以將整個(gè)流程串聯(lián)起來了。
1.打開小程序,創(chuàng)建視圖層頁的webview時(shí),此時(shí)會初始化渲染層webview,并且會將該web view地址設(shè)置為instanceframe.html,也就是我們的渲染層模版。
2.然后進(jìn)入頁面/index/index,等instanceframewebview初始化完成,會將頁面index/index編譯好的代碼注入進(jìn)來并執(zhí)行。
// 將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")
}
3.此時(shí)通過history.pushState方法修改webview的src但是webview并不會發(fā)送頁面請求,并且將調(diào)用$gwx為生成一個(gè)generateFun方法,前面我們了解到該方法是用來生成虛擬dom的。
4.然后會判斷該方法存在時(shí),通過document.dispatchEvent 派發(fā)發(fā)自定義事件generateFuncReady 將generateFunc當(dāng)作參數(shù)傳遞給底層渲染庫。
5.然后在底層渲染庫WAWebview.js中會監(jiān)聽自定義事件generateFuncReady ,然后通過 WeixinJSBridge 通知 JS 邏輯層視圖已經(jīng)準(zhǔn)備好()。
6.最后 JS 邏輯層將數(shù)據(jù)給 Webview 渲染層,WAWebview.js在通過virtual dom生成真實(shí)dom過程中,它會掛載到頁面的document.body上,至此一個(gè)頁面的渲染流程就結(jié)束了。
數(shù)據(jù)更新
小程序的視圖層目前使用 WebView 作為渲染載體,而邏輯層是由獨(dú)立的 JavascriptCore 作為運(yùn)行環(huán)境。
在架構(gòu)上,WebView 和 JS Core 都是獨(dú)立的模塊,并不具備數(shù)據(jù)直接共享的通道。所以在更新數(shù)據(jù)時(shí)必須調(diào)用setData來通知渲染層做更新。
setData
- 邏輯層虛擬 DOM 樹的遍歷和更新,觸發(fā)組件生命周期和 observer 等;
- 將 data 從邏輯層傳輸?shù)揭晥D層;
- 視圖層虛擬 DOM 樹的更新、真實(shí) DOM 元素的更新并觸發(fā)頁面渲染更新。
這里第二步由于WebView 和 JS Core 都是獨(dú)立的模塊,數(shù)據(jù)傳輸是通過 evaluateJavascript 實(shí)現(xiàn)的,還會有額外 JS 腳本解析和執(zhí)行的耗時(shí)因此數(shù)據(jù)到達(dá)渲染層是異步的。
因此切記:
- 不要頻繁的去setData
- 不要每次 setData 都傳遞大量新數(shù)據(jù)(單次stringify后不超過256kb)
- 不要對后臺態(tài)頁面進(jìn)行setData,會搶占正在執(zhí)行的前臺頁面的資源
與Vue對比(再來看看Vue)
整體來講,小程序身上或多或少都有著vue的影子...(模版文件,data,指令,虛擬dom,生命周期等)
但在數(shù)據(jù)更新這里,小程序卻與Vue表現(xiàn)的截然不同。
1.頁面更新DOM是同步的還是異步的?
2.既然更新DOM是個(gè)同步的過程,為什么Vue中還會有nextTick鉤子?
mounted() {
this.name = '前端南玖'
console.log('sync',this.$refs.title.innerText) // 舊文案
// 新文案
Promise.resolve().then(() => {
console.log('微任務(wù)',this.$refs.title.innerText)
})
setTimeout(() => {
console.log('宏任務(wù)',this.$refs.title.innerText)
}, 0)
this.$nextTick(() => {
console.log('nextTick',this.$refs.title.innerText)
})
} 當(dāng)前標(biāo)題:探索小程序底層架構(gòu)原理
文章網(wǎng)址:http://fisionsoft.com.cn/article/dhddjee.html


咨詢
建站咨詢
