axure 小程序 lib_详细揭秘微信小程序框架技术——Mpx
與目前業內的幾個小程序框架相比較而言,mpx 開發設計的出發點就是基于原生的小程序去做功能增強。所以從開發框架的角度來說,是沒有任何“包袱”,圍繞著原生小程序這個 core 去做不同功能的 patch 工作,使得開發小程序的體驗更好。
于是我挑了一些我非常感興趣的點去學習了下 mpx 在相關功能上的設計與實現。
編譯環節
動態入口編譯
不同于 web 規范,我們都知道小程序每個 page/component 需要被最終在 webview 上渲染出來的內容是需要包含這幾個獨立的文件的:js/json/wxml/wxss。為了提升小程序的開發體驗,mpx 參考 vue 的 SFC(single file component)的設計思路,采用單文件的代碼組織方式進行開發。既然采用這種方式去組織代碼的話,那么模板、邏輯代碼、json配置文件、style樣式等都放到了同一個文件當中。那么 mpx 需要做的一個工作就是如何將 SFC 在代碼編譯后拆分為 js/json/wxml/wxss 以滿足小程序技術規范。熟悉 vue 生態的同學都知道,vue-loader 里面就做了這樣一個編譯轉化工作。具體有關 vue-loader 的工作流程可以參見我寫的文章。
這里會遇到這樣一個問題,就是在 vue 當中,如果你要引入一個頁面/組件的話,直接通過import語法去引入對應的 vue 文件即可。但是在小程序的標準規范里面,它有自己一套組件系統,即如果你在某個頁面/組件里面想要使用另外一個組件,那么需要在你的 json 配置文件當中去聲明usingComponents這個字段,對應的值為這個組件的路徑。
在 vue 里面 import 一個 vue 文件,那么這個文件會被當做一個 dependency 去加入到 webpack 的編譯流程當中。但是 mpx 是保持小程序原有的功能,去進行功能的增強。因此一個 mpx 文件當中如果需要引入其他頁面/組件,那么就是遵照小程序的組件規范需要在usingComponents定義好組件名:路徑即可,mpx 提供的 webpack 插件來完成確定依賴關系,同時將被引入的頁面/組件加入到編譯構建的環節當中。
接下來就來看下具體的實現,mpx webpack-plugin 暴露出來的插件上也提供了靜態方法去使用 loader。這個 loader 的作用和 vue-loader 的作用類似,首先就是拿到 mpx 原始的文件后轉化一個 js 文本的文件。例如一個 list.mpx 文件里面有關 json 的配置會被編譯為:
require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=json&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/json-compiler/index?root=!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=json&index=0!./list.mpx")復制代碼這樣可以清楚的看到 list.mpx 這個文件首先 selector(抽離list.mpx當中有關 json 的配置,并傳入到 json-compiler 當中) --->>> json-compiler(對 json 配置進行處理,添加動態入口等) --->>> extractor(利用 child compiler 單獨生成 json 配置文件)
其中動態添加入口的處理流程是在 json-compiler 當中去完成的。例如在你的 page/home.mpx 文件當中的json配置中使用了局部組件 components/list.mpx:
在 json-compiler 當中:
這里需要解釋說明下有關 webpack 提供的 SingleEntryPlugin 插件。這個插件是 webpack 提供的一個內置插件,當這個插件被掛載到 webpack 的編譯流程的過程中是,會綁定compiler.hooks.make.tapAsynchook,當這個 hook 觸發后會調用這個插件上的 SingleEntryPlugin.createDependency 靜態方法去創建一個入口依賴,然后調用compilation.addEntry將這個依賴加入到編譯的流程當中,這個是單入口文件的編譯流程的最開始的一個步驟。
Mpx 正是利用了 webpack 提供的這樣一種能力,在遵照小程序的自定義組件的規范的前提下,解析 mpx json 配置文件的過程中,手動的調用 SingleEntryPlugin 相關的方法去完成動態入口的添加工作。這樣也就串聯起了所有的 mpx 文件的編譯工作。
Render Function
Render Function 這塊的內容我覺得是 Mpx 設計上的一大亮點內容。Mpx 引入 Render Function 主要解決的問題是性能優化方向相關的,因為小程序的架構設計,邏輯層和渲染層是2個獨立的。
這里直接引用 Mpx 有關 Render Function 對于性能優化相關開發工作的描述:
作為一個接管了小程序setData的數據響應開發框架,我們高度重視Mpx的渲染性能,通過小程序官方文檔中提到的性能優化建議可以得知,setData對于小程序性能來說是重中之重,setData優化的方向主要有兩個:
盡可能減少setData調用的頻次
盡可能減少單次setData傳輸的數據 為了實現以上兩個優化方向,我們做了以下幾項工作:
將組件的靜態模板編譯為可執行的render函數,通過render函數收集模板數據依賴,只有當render函數中的依賴數據發生變化時才會觸發小程序組件的setData,同時通過一個異步隊列確保一個tick中最多只會進行一次setData,這個機制和Vue中的render機制非常類似,大大降低了setData的調用頻次;
將模板編譯render函數的過程中,我們還記錄輸出了模板中使用的數據路徑,在每次需要setData時會根據這些數據路徑與上一次的數據進行diff,僅將發生變化的數據通過數據路徑的方式進行setData,這樣確保了每次setData傳輸的數據量最低,同時避免了不必要的setData操作,進一步降低了setData的頻次。
接下來我們看下 Mpx 是如何實現 Render Function 的。這里我們從一個簡單的 demo 來說起:
.mpx 文件經過 loader 編譯轉換的過程中。對于 template 模塊的處理和 vue 類似,首先將 template 轉化為 AST,然后再將 AST 轉化為 code 的過程中做相關轉化的工作,最終得到我們需要的 template 模板代碼。
packages/webpack-plugin/lib/template-compiler.js模板處理 loader 當中:
在render方法內部,創建renderData局部變量,調用compiler.genNode(ast)方法完成 Render Function 核心代碼的生成工作,最終將這個 renderData 返回。例如在上面給出來的 demo 實例當中,通過compiler.genNode(ast)方法最終生成的代碼為:
mpx 文件當中的 template 模塊被初步處理成上面的代碼后,可以看到這是一段可執行的js代碼。那么這段 js 代碼到底是用作何處呢?可以看到compiler.genNode方法是被包裹至bindThis方法當中的。即這段 js 代碼還會被bindThis方法做進一步的處理。打開 bind-this.js 文件可以看到內部的實現其實就是一個 babel 的 transform plugin。在處理上面這段 js 代碼的 AST 的過程中,通過這個插件對 js 代碼做進一步的處理。
bindThis 方法對于 js 代碼的轉化規則就是:
- 一個變量的訪問形式,改造成 this.xxx 的形式;
- 對象屬性的訪問形式,改造成 this.__get(object, property) 的形式(this.__get方法為運行時 mpx runtime 提供的方法)
這里的 this 為 mpx 構造的一個代理對象,在你業務代碼當中調用 createComponent/createPage 方法傳入的配置項,例如 data,都會通過這個代理對象轉化為響應式的數據。
需要注意的是不管哪種數據形式的改造,最終需要達到的效果就是確保在 Render Function 執行的過程當中,這些被模板使用到的數據能被正常的訪問到,在訪問的階段中,這些被訪問到的數據即被加入到 mpx 構建的整個響應式的系統當中。
只要在 template 當中使用到的 data 數據(包括衍生的 computed 數據),最終都會被 renderData 所記錄,而記錄的數據形式是例如:
以上就是 mpx 生成 Render Function 的整個過程。總結下 Render Function 所做的工作:
- 執行 render 函數,將渲染模板使用到的數據加入到響應式的系統當中;
- 返回 renderData 用以接下來的數據 diff 以及調用小程序的 setData 方法來完成視圖的更新
Wxs Module
Wxs 是小程序自己推出的一套腳本語言。官方文檔給出的示例,wxs 模塊必須要聲明式的被 wxml 引用。和 js 在 jsCore 當中去運行不同的是 wxs 是在渲染線程當中去運行的。因此 wxs 的執行便少了一次從 jsCore 執行的線程和渲染線程的通訊,從這個角度來說是對代碼執行效率和性能上的比較大的一個優化手段。
有關官方提到的有關 wxs 的運行效率的問題還有待論證:
“在 android 設備中,小程序里的 wxs 與 js 運行效率無差異,而在 ios 設備中,小程序里的 wxs 會比 js 快 2~20倍。”
因為mpx 是對小程序做漸進增強,因此 wxs 的使用方式和原生的小程序保持一致。在你的.mpx文件當中的 template block 內通過路徑直接去引入 wxs 模塊即可使用:
在template模塊經過template-compiler 處理的過程中。模板編譯器 compiler 在解析模板的 AST 過程中會針對 wxs 標簽緩存一份 wxs 模塊的映射表:
當 compiler 對 template 模板解析完后,template-compiler 接下來就開始處理 wxs 模塊相關的內容:
template/script/style/json 模塊單文件的生成:
不同于 Vue 借助 webpack 是將 Vue 單文件最終打包成單獨的 js chunk 文件。而小程序的規范是每個頁面/組件需要對應的 wxml/js/wxss/json 4個文件。因為 mpx 使用單文件的方式去組織代碼,所以在編譯環節所需要做的工作之一就是將 mpx 單文件當中不同 block 的內容拆解到對應文件類型當中。在動態入口編譯的小節里面我們了解到 mpx 會分析每個 mpx 文件的引用依賴,從而去給這個文件創建一個 entry 依賴(SingleEntryPlugin)并加入到 webpack 的編譯流程當中。
接下來可以看下 styles/json/template 這3個 block 的處理流程是什么樣。
首先來看下 json block 的處理流程:list.mpx -> json-compiler -> extractor。第一個階段 list.mpx 文件經由 json-compiler 的處理流程在前面的章節已經講過,主要就是分析依賴增加動態入口的編譯過程。當所有的依賴分析完后,調用 json-compiler loader 的異步回調函數:
這里我們可以看到經由 json-compiler 處理后,通過nativeCallback方法傳入下一個 loader 的文本內容形如:
即這段文本內容會傳遞到下一個 loader 內部進行處理,即 extractor。接下來我們來看下 extractor 里面主要是實現了哪些功能:
稍微總結下上面的處理流程:
所以上面的示例 demo 最終會輸出一個 json 文件,里面包含的內容即為:
以上幾個章節主要是分析了幾個 Mpx 在編譯構建環節所做的工作。接下來我們來看下 Mpx 在運行時環節做了哪些工作。
響應式系統
小程序也是通過數據去驅動視圖的渲染,需要手動的調用setData去完成這樣一個動作。同時小程序的視圖層也提供了用戶交互的響應事件系統,在 js 代碼中可以去注冊相關的事件回調并在回調中去更改相關數據的值。Mpx 使用 Mobx 作為響應式數據工具并引入到小程序當中,使得小程序也有一套完成的響應式的系統,讓小程序的開發有了更好的體驗。
還是從組件的角度開始分析 mpx 的整個響應式的系統。每次通過createComponent方法去創建一個新的組件,這個方法將原生的小程序創造組件的方法Component做了一層代理,例如在 attched 的生命周期鉤子函數內部會注入一個 mixin:
在這個方法內部首先調用transformApiForProxy方法對組件實例上下文this做一層代理工作,在 context 上下文上去重置小程序的 setData 方法,同時拓展 context 相關的屬性內容:
接下來實例化一個 mpxProxy 實例并掛載至 context 上下文的 $mpxProxy 屬性上,并調用 mpxProxy 的 created 方法完成這個代理對象的初始化的工作。在 created 方法內部主要是完成了以下的幾個工作:
- initApi,在組件實例this上掛載$watch,$forceUpdate,$updated,$nextTick等方法,這樣在你的業務代碼當中即可直接訪問實例上部署好的這些方法;
- initData
- initComputed,將 computed 計算屬性字段全部代理至組件實例 this 上;
- 通過 Mobx observable 方法將 data 數據轉化為響應式的數據;
- initWatch,初始化所有的 watcher 實例;
- initRender,初始化一個 renderWatcher 實例;
這里我們具體的來看下 initRender 方法內部是如何進行工作的:
在 initRender 方法內部非常清楚的看到,首先判斷這個 page/component 是否具有 renderFunction,如果有的話那么就直接實例化一個 renderWatcher:
Watcher 觀察者核心實現的工作流程就是:
mpx 在構建這個響應式的系統當中,主要有2個大的環節:其一為在構建編譯的過程中,將 template 模塊轉化為 renderFunction,提供了渲染模板時所需響應式數據的訪問機制,并將 renderFunction 注入到運行時代碼當中。
其二就是在運行環節,mpx 通過構建一個小程序實例的代理對象,將小程序實例上的數據訪問全部代理至 MPXProxy 實例上,而 MPXProxy 實例即 mpx 基于 Mobx 去構建的一套響應式數據對象,首先將 data 數據轉化為響應式數據,其次提供了 computed 計算屬性,watch 方法等一系列增強的拓展屬性/方法,雖然在你的業務代碼當中 page/component 實例 this 都是小程序提供的,但是最終經過代理機制,實際上訪問的是 MPXProxy 所提供的增強功能,所以 mpx 也是通過這樣一個代理對象去接管了小程序的實例。
需要特別指出的是,mpx 將小程序官方提供的 setData 方法同樣收斂至內部,這也是響應式系統提供的基礎能力,即開發者只需要關注業務開發,而有關小程序渲染運行在 mpx 內部去幫你完成。
性能優化
由于小程序的雙線程的架構設計,邏輯層和視圖層之間需要橋接 native bridge。如果要完成視圖層的更新,那么邏輯層需要調用 setData 方法,數據經由 native bridge,再到渲染層,這個工程流程為:
小程序邏輯層調用宿主環境的 setData 方法;
邏輯層執行 JSON.stringify 將待傳輸數據轉換成字符串并拼接到特定的JS腳本,并通過evaluateJavascript 執行腳本將數據傳輸到渲染層;
渲染層接收到后, WebView JS 線程會對腳本進行編譯,得到待更新數據后進入渲染隊列等待 WebView 線程空閑時進行頁面渲染;
WebView 線程開始執行渲染時,待更新數據會合并到視圖層保留的原始 data 數據,并將新數據套用在WXML片段中得到新的虛擬節點樹。經過新虛擬節點樹與當前節點樹的 diff 對比,將差異部分更新到UI視圖。同時,將新的節點樹替換舊節點樹,用于下一次重渲染。
而 setData 作為邏輯層和視圖層之間通訊的核心接口,那么對于這個接口的使用遵照一些準則將有助于性能方面的提升。
盡可能的減少 setData 傳輸的數據
Mpx 在這個方面所做的工作之一就是基于數據路徑的 diff。這也是官方所推薦的 setData 的方式。每次響應式數據發生了變化,調用 setData 方法的時候確保傳遞的數據都為 diff 過后的最小數據集,這樣來減少 setData 傳輸的數據。
接下來我們就來看下這個優化手段的具體實現思路,首先還是從一個簡單的 demo 來看:
在示例 demo 當中,聲明了一個 obj 對象(這個對象里面的內容在模塊當中被使用到了)。然后經過 200ms 后,手動修改 obj.a 的值,因為對于 c 字段來說它的值沒有發生改變,而 d 字段發生了改變。因此在 setData 方法當中也應該只更新 obj.a.d 的值,即:
因為 mpx 是整體接管了小程序當中有關調用 setData 方法并驅動視圖更新的機制。所以當你在改變某些數據的時候,mpx 會幫你完成數據的 diff 工作,以保證每次調用 setData 方法時,傳入的是最小的更新數據集。
這里也簡單的分析下 mpx 是如何去實現這樣的功能的。在上文的編譯構建階段有分析到 mpx 生成的 Render Function,這個 Render Function 每次執行的時候會返回一個 renderData,而這個 renderData 即用以接下來進行 setData 驅動視圖渲染的原始數據。renderData 的數據組織形式是模板當中使用到的數據路徑作為 key 鍵值,對應的值使用一個數組組織,數組第一項為數據的訪問路徑(可獲取到對應渲染數據),第二項為數據路徑的第一個鍵值,例如在 demo 示例當中的 renderData 數據如下:
當頁面第一次渲染,或者是響應式輸出發生變化的時候,Render Function 都會被執行一次用以獲取最新的 renderData 來進行接下來的頁面渲染過程。
其中在 processRenderData 方法內部調用了 diffAndCloneA 方法去完成數據的 diff 工作。在這個方法內部判斷新、舊值是否發生變化,返回的 diff 字段即表示是否發生了變化,clone 為 diffAndCloneA 接受到的第一個數據的深拷貝值。
這里大致的描述下相關流程:
- 響應式的數據發生了變化,觸發 Render Function 重新執行,獲取最新的 renderData;
- renderData 的預處理,主要是用以剔除通過路徑訪問時同時有父、子路徑情況下的子路徑的 key;
- 判斷是否存在 miniRenderData 最小數據渲染集,如果沒有那么 Mpx 完成 miniRenderData 最小渲染數據集的收集,如果有那么使用處理后的 renderData 和 miniRenderData 進行數據的 diff 工作(diffAndCloneA),并更新最新的 miniRenderData 的值;
- 調用 doRender 方法,進入到 setData 階段
盡可能的減少 setData 的調用頻次
每次調用 setData 方法都會完成一次從邏輯層 -> native bridge -> 視圖層的通訊,并完成頁面的更新。因此頻繁的調用 setData 方法勢必也會造成視圖的多次渲染,用戶的交互受阻。
所以對于 setData 方法另外一個優化角度就是盡可能的減少 setData 的調用頻次,將多個同步的 setData 操作合并到一次調用當中。接下來就來看下 mpx 在這方面是如何做優化的。
還是先來看一個簡單的 demo:
在示例 demo 當中,msg 和 obj 都作為模板依賴的數據,這個組件開始展示后的 200ms,更新 obj.a 的值,同時 obj 被 watch,當 obj 發生改變后,更新 msg 的值。這里的邏輯處理順序是:
obj.a 變化 -> 將 renderWatch 加入到執行隊列 -> 觸發 obj watch -> 將 obj watch 加入到執行隊列 -> 將執行隊列放到下一幀執行 -> 按照 watch id 從小到大依次執行 watch.run -> setData 方法調用一次(即 renderWatch 回調),統一更新 obj.a 及 msg -> 視圖重新渲染復制代碼接下來就來具體看下這個流程:由于 obj 作為模板渲染的依賴數據,自然會被這個組件的 renderWatch 作為依賴而被收集。當 obj 的值發生變化后,首先觸發 reaction 的回調,即 this.update() 方法,如果是個同步的 watch,那么立即調用 this.run() 方法,即 watcher 監聽的回調方法,否則就通過 queueWatcher(this) 方法將這個 watcher 加入到執行隊列:
而在 queueWatcher 方法中,lockTask 維護了一個異步鎖,即將 flushQueue 當成微任務統一放到下一幀去執行。所以在 flushQueue 開始執行之前,還會有同步的代碼將 watcher 加入到執行隊列當中,當 flushQueue 開始執行的時候,依照 watcher.id 升序依次執行,這樣去確保 renderWatcher 在執行前,其他所有的 watcher 回調都執行完了,即執行 renderWatcher 的回調的時候獲取到的 renderData 都是最新的,然后再去進行 setData 的操作,完成頁面的更新。
總結
以上是生活随笔為你收集整理的axure 小程序 lib_详细揭秘微信小程序框架技术——Mpx的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 三菱电机宣布投建 8 英寸 SiC 工厂
- 下一篇: 新浪有借改名了吗