小程序現(xiàn)在如日中天,各大公司都推出了自己的小程序平臺,目前看來運(yùn)行機(jī)制都差不多,數(shù)據(jù)形成視圖,渲染和邏輯分成兩個線程,交互通過線程通信實(shí)現(xiàn)。
剛開始接觸小程序開發(fā)的時候,看到小程序的語法覺得很奇怪。看著像react和vue的結(jié)合體,疑惑為什么要這么費(fèi)力的實(shí)現(xiàn)這么一套機(jī)制。難道是為了體現(xiàn)技術(shù)nb? 用了一會就發(fā)現(xiàn)問題了,照搬pc開發(fā)的那套思想,特么小程序里不支持dom相關(guān)的api,很不方便。翻了很多遍微信和支付寶小程序的官方文檔,終于有了一點(diǎn)理解。
本文大部分是官方文檔引用加上自己一點(diǎn)總結(jié)。
web開發(fā)渲染線程和腳本線程是互斥的,這也是為什么長時間的腳本運(yùn)行可能會導(dǎo)致頁面失去響應(yīng)。開發(fā)者可以使用到各種瀏覽器暴露出來的 DOM API,進(jìn)行 DOM 選中和操作。 而在小程序中,二者是分開的,分別運(yùn)行在不同的線程中,邏輯層運(yùn)行在 JSCore 中,并沒有一個完整瀏覽器對象,因而缺少相關(guān)的DOM API和BOM API。
由于支付寶官方文檔說明過于簡略,本文結(jié)合了微信和支付寶小程序的特點(diǎn)總結(jié),結(jié)合如有差異,歡迎指正。
這部分在 微信 和支付寶小程序官方文檔都有說明
由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JS線程和GUI線程同時運(yùn)行),那么渲染線程前后獲得的元素數(shù)據(jù)就可能不一致了。
小程序分為 app 和 page 兩層。app 用來描述整個應(yīng)用,page 用來描述各個頁面。
app 由三個文件組成,必須放在項(xiàng)目的根目錄。
文件 | 必需 | 作用 |
---|---|---|
app.js | 是 | 小程序邏輯 |
app.json | 是 | 小程序全局設(shè)置 |
app.acss | 否 | 小程序全局樣式表 |
page 由四個文件組成,分別是:
文件 | 必需 | 作用 |
---|---|---|
app.js | 是 | 頁面邏輯 |
app.axml | 是 | 頁面結(jié)構(gòu) |
app.json | 否 | 頁面配置 |
app.acss | 否 | 頁面樣式 |
小程序的邏輯層和渲染層是分開的兩個線程,小程序的運(yùn)行環(huán)境分成渲染層和邏輯層,其中 WXML 模板和 WXSS 樣式工作在渲染層,JS腳本工作在邏輯層。在渲染層,宿主環(huán)境會把WXML轉(zhuǎn)化成對應(yīng)的JS對象,在邏輯層發(fā)生數(shù)據(jù)變更的時候,我們需要通過宿主環(huán)境提供的setData方法把數(shù)據(jù)從邏輯層傳遞到渲染層,再經(jīng)過對比前后差異,把差異應(yīng)用在原來的Dom樹上,渲染出正確的UI界面。
小程序的渲染層和邏輯層分別由2個線程管理:渲染層的界面使用了WebView 進(jìn)行渲染;邏輯層采用JsCore線程運(yùn)行JS腳本。一個小程序存在多個界面,所以渲染層存在多個WebView線程,這兩個線程的通信會經(jīng)由客戶端做中轉(zhuǎn),邏輯層發(fā)送網(wǎng)絡(luò)請求也經(jīng)由Native轉(zhuǎn)發(fā),小程序的通信模型下圖所示。
從邏輯組成來說,一個小程序是由多個“頁面”組成的“程序”。宿主環(huán)境提供了 App() 構(gòu)造器用來注冊一個程序App,需要留意的是App() 構(gòu)造器必須寫在項(xiàng)目根目錄的app.js里,App實(shí)例是單例對象,在其他JS腳本中可以使用宿主環(huán)境提供的 getApp() 來獲取程序?qū)嵗?/p>
小程序開發(fā)框架的邏輯層使用 JavaScript 引擎為小程序提供開發(fā)者 JavaScript 代碼的運(yùn)行環(huán)境以及微信小程序的特有功能。 邏輯層將數(shù)據(jù)進(jìn)行處理后發(fā)送給視圖層,同時接受視圖層的事件反饋。 開發(fā)者寫的所有代碼最終將會打包成一份 JavaScript 文件,并在小程序啟動的時候運(yùn)行,直到小程序銷毀。這一行為類似 ServiceWorker,所以邏輯層也稱之為 App Service。
const app = getApp();
復(fù)制代碼
參考資料: 微信1 、 微信2 、 微信3
小程序的 JavaScript 代碼分為邏輯層腳本和 sjs/wxs 腳本
支付寶文檔 說sjs和邏輯層運(yùn)行在相同的 JavaScript 引擎的不同線程中。
微信文檔 又表示wxs是運(yùn)行在webview中的,并且提供了更為強(qiáng)大的功能:如果在 iOS 設(shè)備上小程序內(nèi)的 WXS 會比 JavaScript 代碼快 2 ~ 20 倍,在 android 設(shè)備上二者運(yùn)行效率無差異;減少通信的次數(shù),讓事件在視圖層(Webview)響應(yīng);用來響應(yīng)小程序事件,目前只能響應(yīng)內(nèi)置組件的事件,不支持自定義組件事件;還能調(diào)用邏輯層的事件;
對兩者sjs/wxs差別表示吃瓜狀態(tài),支付寶的sjs明顯功能弱了很多,畢竟雞肋,一般用來模擬vue里的compued功能使用,無法作為事件回調(diào)。微信的wxs提供了更強(qiáng)大的支持。
試了下sjs不帶類似computed緩存功能
兩個平臺都會對新預(yù)發(fā)的代碼進(jìn)行 babel 轉(zhuǎn)換,使 JavaScript 引擎支持絕大多數(shù) ES6 的新特性,但是對于內(nèi)置對象未提供完全的Polyfill,具體支持情況可以查閱 支付寶 、 微信。
小程序中,有一些組件其實(shí)是調(diào)用原生組件的,如map、video等,這些復(fù)雜交互的控件,原生能帶來更好的性能與原生體驗(yàn)。
簡單說就是在期望插入原生控件的位置渲染一個HTML元素,拿到此DOM的位置,客戶端在相同的位置上,根據(jù)寬高插入一塊原生區(qū)域,位置或?qū)捀甙l(fā)生變化時,組件會通知客戶端做相應(yīng)的調(diào)整。 可以直接看 官方文檔
微信小程序的官方介紹 很全面了
回到標(biāo)題的問題。因?yàn)檫壿媽覵ervice中的代碼與WebView中的代碼完全隔離,JavaScriptCore中并沒有document,window等對象(ECMAScript標(biāo)準(zhǔn)沒有規(guī)定DOM,這其實(shí)是瀏覽器提供的)。js和視圖(dom所在)沒有運(yùn)行在同一容器中。
很好理解,小程序初次啟動時,客戶端需要從 CDN 下載小程序資源包,此后,如果小程序代碼包未更新且還被保留在緩存中,則下載小程序代碼包的步驟會被跳過??梢宰龅模?/p>
每一次setData都是線程通信
支付寶小程序提供了$batchedUpdates
this.$batchedUpdates(() => {
this.setData({
counter: this.data.counter + 1,
});
this.setData({
counter: this.data.counter + 1,
});
});
復(fù)制代碼
setData是線程通信傳遞數(shù)據(jù),傳輸時數(shù)據(jù)需要序列化,框架提供了指定路徑設(shè)置數(shù)據(jù)的方便,避免一次傳輸完整數(shù)據(jù)。
this.setData({
'array[0]': 1,
'obj.x':2,
});
復(fù)制代碼
針對長列表, 支付寶小程序 提供了優(yōu)化方法$spliceData,使用方式對應(yīng)js數(shù)組的splice
this.$spliceData({ 'a.b': [1, 0, 5, 6] })
復(fù)制代碼
針對長列表做優(yōu)化,避免每次傳遞整個列表,只會從對應(yīng)組件節(jié)點(diǎn)開始做差異比較
小程序中事件響應(yīng)也需要通過線程通信,如果頻繁的觸發(fā)可能會造成卡頓。例如頁面有 2 個元素 A 和 B,用戶在 A 上做 touchmove 手勢,要求 B 也跟隨移動。一次 touchmove 事件的響應(yīng)過程為:
a、touchmove 事件從視圖層(Webview)拋到邏輯層(App Service)
b、邏輯層(App Service)處理 touchmove 事件,再通過 setData 來改變 B 的位置
微信小程序里可以使用wxs響應(yīng)事件優(yōu)化,wxs是運(yùn)行在webview中的,不需要跨線程通信。WXS 函數(shù)的除了純邏輯的運(yùn)算,還可以通過封裝好的ComponentDescriptor 實(shí)例來訪問以及設(shè)置組件的 class 和樣式,對于交互動畫,設(shè)置 style 和 class 足夠了:
const wxsFunction = function(event, ownerInstance) {
const instance = ownerInstance.selectComponent('.classSelector') // 返回組件的實(shí)例
instance.setStyle({
"font-size": "14px" // 支持rpx
})
instance.getDataset()
instance.setClass(className)
// ...
return false // 不往上冒泡,相當(dāng)于調(diào)用了同時調(diào)用了stopPropagation和preventDefault
}
復(fù)制代碼
一次 touchmove 的響應(yīng)需要經(jīng)過 2 次的邏輯層和渲染層的通信以及一次渲染,通信的耗時比較大。此外 setData 渲染也會阻塞其它腳本執(zhí)行,導(dǎo)致了整個用戶交互的動畫過程會有延遲。 官方性能優(yōu)化文檔: 微信 、 支付寶 ;
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)