對於小程序框架實現原理,在支付寶小程序官方文檔上有這樣一段描述:vue
與傳統的 H5 應用不一樣,小程序運行架構分爲 webview 和 worker 兩個部分。webview 負責渲染,worker 則負責存儲數據和執行業務邏輯。 1.webview 和 worker 之間的通訊是異步的。這意味着當咱們調用
setData
時,咱們的數據並不會當即渲染,而是須要從 worker 異步傳輸到 webview。 2.數據傳輸時須要序列化爲字符串,而後經過 evaluateJavascript 方式傳輸,數據大小會影響性能。node
歸納一下,大體意思是小程序框架核心是經過2個線程來完成的,主線程負責webView的渲染工做,worker線程負責js執行。說到這裏,你是否是會產生一個疑問:爲何多線程通訊損耗性能還要搞多線程呢? 可能大多數人都知道由於Web技術實在是太開放了,開發者能夠隨心所欲。這種狀況在小程序中是不容許的,不容許使用<iframe>
、不容許 <a>
直接外跳到其餘在線網頁、不容許開發者觸碰DOM
、不容許使用某些未知的危險API等。可是,仔細想一想其實單線程也有能力來阻止用戶操做這些危險動做,好比經過全局配置黑名單API、改寫框架內部編譯機制,屏蔽危險操做...react
可是卻始終沒法解決一個問題:如何防止開發者作一些咱們想禁用的功能。由於是一個網頁,開發者能夠執行JS,能夠操做DOM,能夠操做BOM,能夠作一切事情。so,咱們須要一個沙箱環境,來運行咱們的js,這個沙箱環境須要能夠屏蔽掉全部的危險動做。說了這麼多,大體想法以下: webpack
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 來通知渲染進程更新:git
let index = 0;
// 獲得diff差別
let diffData = diff(0, index, oldVnode, newVnode);
// 通知渲染進程更新
self.postMessage(JSON.stringify(diffData));
複製代碼
有點麻煩?能不能繼承現有框架能力?好比Vue、React。固然能夠,咱們下面就來介紹基於Vue來實現的demo.github
有了上面的知識,咱們先不着急寫代碼,先來捋一下咱們須要什麼,首先咱們須要實現這樣一個能力:渲染層和邏輯層分離,emmm。。。大體咱們的小程序是這樣的web
// 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;
複製代碼
上面代碼核心乾的事其實並不複雜,也就是:
當咱們通知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,固然,想解決這些問題不必定非要使用雙線程模型,但雙線程模型無疑是最合適的技術方案。
通過上面的介紹,是否是發現小程序其實也就那麼回事,並無多麼....這邊文章主要但願能讓你對常用的框架有一個原理性的初步認識,至少咱們再用的時候能夠規避掉一些坑,或者性能問題。
參考文章: