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