小程式內部實現原理概覽
對於小程式框架實現原理,在支付寶小程式官方文件上有這樣一段描述:
與傳統的 H5 應用不同,小程式執行架構分為 webview 和 worker 兩個部分。webview 負責渲染,worker 則負責儲存資料和執行業務邏輯。 1.webview 和 worker 之間的通訊是非同步的。這意味著當我們呼叫
setData
時,我們的資料並不會立即渲染,而是需要從 worker 非同步傳輸到 webview。 2.資料傳輸時需要序列化為字串,然後通過 evaluateJavascript 方式傳輸,資料大小會影響效能。
概括一下,大致意思是小程式框架核心是通過2個執行緒來完成的,主執行緒負責webView的渲染工作,worker執行緒負責js執行。說到這裡,你是不是會產生一個疑問:為什麼多執行緒通訊損耗效能還要搞多執行緒呢? 可能大多數人都知道因為Web技術實在是太開放了,開發者可以為所欲為。這種情況在小程式中是不允許的,不允許使用<iframe>
、不允許 <a>
直接外跳到其他線上網頁、不允許開發者觸碰DOM
、不允許使用某些未知的危險API等。但是,仔細想想其實單執行緒也有能力來阻止使用者操作這些危險動作,比如通過全域性配置黑名單API、改寫框架內部編譯機制,遮蔽危險操作...
但是卻始終無法解決一個問題:如何防止開發者做一些我們想禁用的功能。因為是一個網頁,開發者可以執行JS,可以操作DOM,可以操作BOM,可以做一切事情。so,我們需要一個沙箱環境,來執行我們的js,這個沙箱環境需要可以遮蔽掉所有的危險動作。說了這麼多,大致想法如下:
關於UI層的渲染,有很多實現方式,比如通過類似VNode -> diff的自定義渲染方式來實現了一個簡易的小程式框架:function App (props) {
const {msg} = props;
return () => (
<div class="main">
{msg}
</div>
)
}
render(<App msg="hello world" />)
複製程式碼
核心就是通過定義@babel/plugin-transform-react-jsx
外掛來轉換 jsx,生成Vnode,再交給Worker通過Diff,最後通過worker postmsg 來通知渲染程式更新:
let index = 0;
// 得到diff差異
let diffData = diff(0, index, oldVnode, newVnode);
// 通知渲染程式更新
self.postMessage(JSON.stringify(diffData));
複製程式碼
有點麻煩?能不能繼承現有框架能力?比如Vue、React。當然可以,我們下面就來介紹基於Vue來實現的demo.
實現一個基於Vue的小程式框架
有了上面的知識,我們先不著急寫程式碼,先來捋一下我們需要什麼,首先我們需要實現這樣一個能力:渲染層和邏輯層分離,emmm。。。大致我們的小程式是這樣的
// page.js 邏輯層
export default {
data: {
msg: 'hello Vox',
},
create() {
console.log(window);
setTimeout(() => {
this.setData({
msg: 'setData',
})
}, 1000);
},
}
複製程式碼
// page.vxml.js 渲染層
export default () => {
return '<div>{{msg}}</div>';
}
複製程式碼
這裡的渲染層為啥不是類似於微信或者支付寶小程式 wxml,axml
這樣的呢?當然可以,其實我只是為了方便而已,我們可以手寫一個webpack loader 來處理一下我們自定義的檔案。這裡有興趣的小夥伴可以嘗試一下。不是本次介紹的核心。
好了,上面是我們想要的功能,我們核心是框架,框架層要乾的事核心有2個:構造worker初始化引擎;構造渲染引擎。
// index.worker.js 構造worker
const voxWorker = options => {
const {config} = options;
// Vue生命週期收集
const lifeCircleMap = {
'lifeCircle:create': [config.create],
};
// 定義setData方法用於通知UI層渲染更新
self.setData = (data) => {
console.log('setData called');
self.postMessage(
JSON.stringify({
type: 'update',
data,
})
,
null
);
};
// worker構建完成,通知渲染層初始化
self.postMessage(
JSON.stringify({
type: 'init',
data: config.data,
})
,
null
);
// 執行生命週期函式
self.onmessage = e => {
const {type} = JSON.parse(e.data);
lifeCircleMap[type].forEach(lifeCircle => lifeCircle.call(self))
}
}
export default voxWorker;
複製程式碼
上面程式碼核心乾的事其實並不複雜,也就是:
- 收集需要用到的生命週期
- 定義setData函式,提供給使用者層更新UI
- 定義監聽函式,處理生命週期函式執行
- 通知UI程式開啟渲染。
當我們通知UI程式開始渲染的時候,UI程式也就是需要構造Vue例項,進行頁面render:
worker.onmessage = e => {
const {type, data} = JSON.parse(e.data);
if (type === 'init') {
const mountNode = document.createElement('div');
document.body.appendChild(mountNode);
target = new Vue({
el: mountNode,
data: () => data,
template: template(),
created(){
worker.postMessage(JSON.stringify({
type: 'lifeCircle:create',
})
,
null);
}
});
}
}
複製程式碼
可以看到UI執行緒在初始化的時候,一併初始化了 worker層傳遞來的data,並對生命週期進行了宣告。當生命週期函式在UI層觸發的時候,會通知 worker。在我們的例子中,create 鉤子通過setData 進行了一個更新data的動作。我們知道 setData 就是拿到資料進行通知更新:
// UI執行緒接收到通知訊息,更新UI
if (type === 'update') {
Object.keys(data).map(key => {
target[key] = data[key];
});
}
複製程式碼
說到這裡,似乎一切都感覺清楚多了,我們用worker執行使用者js邏輯,worker內無法操作dom,無法訪問window。當我們需要更新dom,可以去通知渲染執行緒做更新。我們也很容易想到不斷地資料傳輸對效能的損耗,所以我們當然可以做進一步的優化,多個setData可以組合一起傳送?就是建立一個通訊。其次再看一些,我們的worker再通訊傳輸資料的過程中不斷通過字串的parse
和stringify
綠色部分時原生JSON.stringify(), 關於這一塊如何提升效能一方面可以通過減少資料傳輸量,其他的優化也可以參考這裡如何提升JSON.stringify()的效能
最後你可能會問,小程式用法都是 <view>, <text>
之類的標籤,為啥我這裡直接用了 <div>
。其實吧,
view
是不是就可以了呢?
這裡為大家介紹的也只是小程式的冰山一角,內部的容器開放jsBridge能力,離線機制,跨webview通訊機制等等有興趣的可以去探索,當然這裡也只是拋磚引玉。
有興趣的可以檢視原始碼Vox
結語
引用知乎上的一段話:
其實,大家對小程式的底層實現都是使用雙執行緒模型,大家對外宣稱都會說是為了:方便多個頁面之間資料共享和互動為native開發者提供更好的編碼體驗為了效能(防止使用者的JS執行卡住UI執行緒)其他好處但其實真正的原因其實是:“安全”和“管控”,其他原因都是附加上去的。因為Web技術是非常開放的,JavaScript可以做任何事。但在小程式這個場景下,它不會給開發者那麼高的許可權:不允許開發者把頁面跳轉到其他線上網頁不允許開發者直接訪問DOM不允許開發者隨意使用window上的某些未知的可能有危險的API,當然,想解決這些問題不一定非要使用雙執行緒模型,但雙執行緒模型無疑是最合適的技術方案。
經過上面的介紹,是不是發現小程式其實也就那麼回事,並沒有多麼....這邊文章主要希望能讓你對經常使用的框架有一個原理性的初步認識,至少我們再用的時候可以規避掉一些坑,或者效能問題。
參考文章: