詳細介紹 Weex 的 JS Framework

好久之前,我寫過兩篇文章(《 Weex 框架中 JS Framework 的結構》,《 詳解 Weex JS Framework 的編譯過程》)介紹過 JS Framework。可是文章寫於 2016 年 8 月份,這都是一年半之前的事了,說是「詳解」其實解釋得並不詳細,並且是基於舊版 .we 框架寫的,DSL 和底層框架各部分的功能解耦得的並非很清楚。這一年多以來 JS Framework 已經有了很大的變化,不只支持了 Vue 和 Rax,原生容器和底層接口也作了大量改造,這裏再從新介紹一遍。

在 Weex 框架中的位置

Weex 是一個既支持多個前端框架又能跨平臺渲染的框架,JS Framework 介於前端框架和原生渲染引擎之間,處於承上啓下的位置,也是跨框架跨平臺的關鍵。不管你使用的是 Vue 仍是 Rax,不管是渲染在 Android 仍是 iOS,JS Framework 的代碼都會運行到(若是是在瀏覽器和 WebView 裏運行,則不依賴 JS Framework)。html

js framework position

像 Vue 和 Rax 這類前端框架雖然內部的渲染機制、Virtual DOM 的結構都是不一樣的,可是都是用來描述頁面結構以及開發範式的,對 Weex 而言只屬於語法層,或者稱之爲 DSL (Domain Specific Language)。不管前端框架裏數據管理和組件管理的策略是什麼樣的,它們最終都將調用 JS Framework 提供的接口來調用原生功能而且渲染真實 UI。底層渲染引擎中也沒必要關心上層框架中組件化的語法和更新策略是怎樣的,只須要處理 JS Framework 中統必定義的節點結構和渲染指令。多了這麼一層抽象,有利於標準的統一,也使得跨框架和跨平臺成爲了可能。前端

圖雖然這麼畫,可是大部分人並不區分得這麼細,喜歡把 Vue 和 Rax 以及下邊這一層放一塊兒稱爲 JS Framework。

主要功能

若是將 JS Framework 的功能進一步拆解,能夠分爲以下幾個部分:express

  • 適配前端框架
  • 構建渲染指令樹
  • JS-Native 通訊
  • JS Service
  • 準備環境接口

適配前端框架

前端框架在 Weex 和瀏覽器中的執行過程不同,這個應該不難理解。如何讓一個前端框架運行在 Weex 平臺上,是 JS Framework 的一個關鍵功能。api

以 Vue.js 爲例,在瀏覽器上運行一個頁面大概分這麼幾個步驟:首先要準備好頁面容器,能夠是瀏覽器或者是 WebView,容器裏提供了標準的 Web API。而後給頁面容器傳入一個地址,經過這個地址最終獲取到一個 HTML 文件,而後解析這個 HTML 文件,加載並執行其中的腳本。想要正確的渲染,應該首先加載執行 Vue.js 框架的代碼,向瀏覽器環境中添加 Vue 這個變量,而後建立好掛載點的 DOM 元素,最後執行頁面代碼,從入口組件開始,層層渲染好再掛載到配置的掛載點上去。瀏覽器

在 Weex 裏的執行過程也比較相似,不過 Weex 頁面對應的是一個 js 文件,不是 HTML 文件,並且不須要自行引入 Vue.js 框架的代碼,也不須要設置掛載點。過程大概是這樣的:首先初始化好 Weex 容器,這個過程當中會初始化 JS Framework,Vue.js 的代碼也包含在了其中。而後給 Weex 容器傳入頁面地址,經過這個地址最終獲取到一個 js 文件,客戶端會調用 createInstance 來建立頁面,也提供了刷新頁面和銷燬頁面的接口。大體的渲染行爲和瀏覽器一致,可是和瀏覽器的調用方式不同,前端框架中至少要適配客戶端打開頁面、銷燬頁面(push、pop)的行爲才能夠在 Weex 中運行。前端框架

js framework apis

在 JS Framework 裏提供瞭如上圖所示的接口來實現前端框架的對接。圖左側的四個接口與頁面功能有關,分別用於獲取頁面節點、監聽客戶端的任務、註冊組件、註冊模塊,目前這些功能都已經轉移到 JS Framework 內部,在前端框架裏都是可選的,有特殊處理邏輯時才須要實現。圖右側的四個接口與頁面的生命週期有關,分別會在頁面初始化、建立、刷新、銷燬時調用,其中只有 createInstance 是必須提供的,其餘也都是可選的(在新的 Sandbox 方案中,createInstance 已經改爲了 createInstanceContext)。詳細的初始化和渲染過程會在後續章節裏展開。weex

構建渲染指令樹

不一樣的前端框架裏 Virtual DOM 的結構、patch 的方式都是不一樣的,這也反應了它們開發理念和優化策略的不一樣,可是最終,在瀏覽器上它們都使用一致的 DOM API 把 Virtual DOM 轉換成真實的 HTMLElement。在 Weex 裏的邏輯也是相似的,只是在最後一步生成真實元素的過程當中,不使用原生 DOM API,而是使用 JS Framework 裏定義的一套 Weex DOM API 將操做轉化成渲染指令發給客戶端。框架

patch virtual dom

JS Framework 提供的 Weex DOM API 和瀏覽器提供的 DOM API 功能基本一致,在 Vue 和 Rax 內部對這些接口都作了適配,針對 Weex 和瀏覽器平臺調用不一樣的接口就能夠實現跨平臺渲染。dom

此外 DOM 接口的設計至關複雜,揹負了大量的歷史包袱,也不是全部特性都適合移動端。JS Framework 裏將這些接口作了大量簡化,借鑑了 W3C 的標準,只保留了其中最經常使用到的一部分。目前的狀態是夠用、精簡高效、和 W3C 標準有不少差別,可是已經成爲 Vue 和 Rax 渲染原生 UI 的事實標準,後續還會從新設計這些接口,使其變得更標準一些。JS Framework 裏 DOM 結構的關係以下圖所示:異步

Weex DOM

前端框架調用這些接口會在 JS Framework 中構建一顆樹,這顆樹中的節點不包含複雜的狀態和綁定信息,可以序列化轉換成 JSON 格式的渲染指令發送給客戶端。這棵樹曾經有過不少名字:Virtual DOM Tree、Native DOM Tree,我覺的其實它應該算是一顆 「Render Directive Tree」,也就是渲染指令樹。叫什麼無所謂了,反正它就是 JS Framework 內部的一顆與 DOM 很像的樹。

這顆樹的層次結構和原生 UI 的層次結構是一致的,當前端的節點有更新時,這棵樹也會跟着更新,而後把更新結果以渲染指令的形式發送給客戶端。這棵樹並不計算佈局,也沒有什麼反作用,操做也都是很高效的,基本都是 O(1) 級別,偶爾有些 O(n) 的操做會遍歷同層兄弟節點或者上溯找到根節點,不會遍歷整棵樹。

JS-Native 通訊

在開發頁面過程當中,除了節點的渲染之外,還有原生模塊的調用、事件綁定、回調等功能,這些功能都依賴於 js 和 native 之間的通訊來實現。

js-native communication

首先,頁面的 js 代碼是運行在 js 線程上的,然而原生組件的繪製、事件的捕獲都發生在 UI 線程。在這兩個線程之間的通訊用的是 callNativecallJS 這兩個底層接口(如今已經擴展到了不少個),它們默認都是異步的,在 JS Framework 和原生渲染器內部都基於這兩個方法作了各類封裝。

callNative 是由客戶端向 JS 執行環境中注入的接口,提供給 JS Framework 調用,界面的節點(上文提到的渲染指令樹)、模塊調用的方法和參數都是經過這個接口發送給客戶端的。爲了減小調用接口時的開銷,其實如今已經開了更多更直接的通訊接口,其中有些接口還支持同步調用(支持返回值),它們在原理上都和 callNative 是同樣的。

callJS 是由 JS Framework 實現的,而且也注入到了執行環境中,提供給客戶端調用。事件的派發、模塊的回調函數都是經過這個接口通知到 JS Framework,而後再將其傳遞給上層前端框架。

JS Service

Weex 是一個多頁面的框架,每一個頁面的 js bundle 都在一個獨立的環境裏運行,不一樣的 Weex 頁面對應到瀏覽器上就至關於不一樣的「標籤頁」,普通的 js 庫沒辦法實如今多個頁面之間實現狀態共享,也很難實現跨頁通訊。

在 JS Framework 中實現了 JS Service 的功能,主要就是用來解決跨頁面複用和狀態共享的問題的,例如 BroadcastChannel 就是基於 JS Service 實現的,它能夠在多個 Weex 頁面之間通訊。

準備環境接口

因爲 Weex 運行環境和瀏覽器環境有很大差別,在 JS Framework 裏還對一些環境變量作了封裝,主要是爲了解決解決原生環境裏的兼容問題,底層使用渲染引擎提供的接口。主要的改動點是:

  • console: 原生提供了 nativeLog 接口,將其封裝成前端熟悉的 console.xxx 並能夠控制日誌的輸出級別。
  • timer: 原生環境裏 timer 接口不全,名稱和參數不一致。目前來看有了原生 C/C++ 實現的 timer 後,這一層能夠移除。
  • freeze: 凍結當前環境裏全局變量的原型鏈(如 Array.prototype)。

另外還有一些 ployfill:PromiseArary.fromObject.assignObject.setPrototypeOf 等。

這一層裏的東西能夠說都是用來「填坑」的,也是與環境有關 Bug 的高發地帶,若是你只看代碼的話會以爲莫名奇妙,可是它極可能解決了某些版本某個環境中的某個神奇的問題,也有可能觸發了一個更神奇的問題。隨着對 JS 引擎自己的優化和定製愈來愈多,這一層代碼能夠愈來愈少,最終會所有移除掉。

執行過程

上面是用空間角度介紹了 JS Framework 裏包含了哪些部分,接下來從時間角度介紹一下某些功能在 JS Framework 裏的處理流程。

框架初始化

JS Framework 以及 Vue 和 Rax 的代碼都是內置在了 Weex SDK 裏的,隨着 Weex SDK 一塊兒初始化。SDK 的初始化通常在 App 啓動時就已經完成了,只會執行一次。初始化過程當中與 JS Framework 有關的是以下這三個操做:

  1. 初始化 JS 引擎,準備好 JS 執行環境,向其中註冊一些變量和接口,如 WXEnvironmentcallNative
  2. 執行 JS Framework 的代碼
  3. 註冊原生組件和原生模塊

針對第二步,執行 JS Framework 的代碼的過程又能夠分紅以下幾個步驟:

  1. 註冊上層 DSL 框架,如 Vue 和 Rax。這個過程只是告訴 JS Framework 有哪些 DSL 可用,適配它們提供的接口,如 initcreateInstance,可是不會執行前端框架裏的邏輯。
  2. 初始化環境變量,而且會將原生對象的原型鏈凍結,此時也會註冊內置的 JS Service,如 BroadcastChannel
  3. 若是 DSL 框架裏實現了 init 接口,會在此時調用。
  4. 向全局環境中注入可供客戶端調用的接口,如 callJScreateInstanceregisterComponents,調用這些接口會同時觸發 DSL 中相應的接口。

再回顧看這兩個過程,能夠發現原生的組件和模塊是註冊進來的,DSL 也是註冊進來的,Weex 作的比較靈活,組件模塊是可插拔的,DSL 框架也是可插拔的,有很強的擴展能力。

JS Bundle 的執行過程

在初始化好 Weex SDK 以後,就能夠開始渲染頁面了。一般 Weex 的一個頁面對應了一個 js bundle 文件,頁面的渲染過程也是加載並執行 js bundle 的過程,大概的步驟以下圖所示:

execute js bundle

首先是調用原生渲染引擎裏提供的接口來加載執行 js bundle,在 Android 上是 renderByUrl,在 iOS 上是 renderWithURL。在獲得了 js bundle 的代碼以後,會繼續執行 SDK 裏的原生 createInstance 方法,給當前頁面生成一個惟一 id,而且把代碼和一些配置項傳遞給 JS Framework 提供的 createInstance 方法。

在 JS Framework 接收到頁面代碼以後,會判斷其中使用的 DSL 的類型(Vue 或者 Rax),而後找到相應的框架,執行 createInstanceContext 建立頁面所須要的環境變量。

create instance

在舊的方案中,JS Framework 會調用 runInContex 函數在特定的環境中執行 js 代碼,內部基於 new Function 實現。在新的 Sandbox 方案中,js bundle 的代碼再也不發給 JS Framework,也再也不使用 new Function,而是由客戶端直接執行 js 代碼。

頁面的渲染

Weex 裏頁面的渲染過程和瀏覽器的渲染過程相似,總體能夠分爲【建立前端組件】-> 【構建 Virtual DOM】->【生成「真實」 DOM】->【發送渲染指令】->【繪製原生 UI】這五個步驟。前兩個步驟發生在前端框架中,第三和第四個步驟在 JS Framework 中處理,最後一步是由原生渲染引擎實現的。下圖描繪了頁面渲染的大體流程:

render process

建立前端組件

以 Vue.js 爲例,頁面都是以組件化的形式開發的,整個頁面能夠劃分紅多個層層嵌套和平鋪的組件。Vue 框架在執行渲染前,會先根據開發時編寫的模板建立相應的組件實例,能夠稱爲 Vue Component,它包含了組件的內部數據、生命週期以及 render 函數等。

若是給同一個模板傳入多條數據,就會生成多個組件實例,這能夠算是組件的複用。如上圖所示,假若有一個組件模板和兩條數據,渲染時會建立兩個 Vue Component 的實例,每一個組件實例的內部狀態是不同的。

構建 Virtual DOM

Vue Component 的渲染過程,能夠簡單理解爲組件實例執行 render 函數生成 VNode 節點樹的過程,也就是構建 Virtual DOM 的生成過程。自定義的組件在這個過程當中被展開成了平臺支持的節點,例如圖中的 VNode 節點都是和平臺提供的原生節點一一對應的,它的類型必須在 Weex 支持的原生組件範圍內。

生成「真實」 DOM

以上過程在 Weex 和瀏覽器裏都是徹底同樣的,從生成真實 DOM 這一步開始,Weex 使用了不一樣的渲染方式。前面提到過 JS Framework 中提供了和 DOM 接口相似的 Weex DOM API,在 Vue 裏會使用這些接口將 VNode 渲染生成適用於 Weex 平臺的 Element 對象,和 DOM 很像,但並非「真實」的 DOM。

發送渲染指令

在 JS Framework 內部和客戶端渲染引擎約定了一系列的指令接口,對應了一個原子的 DOM 操做,如 addElement removeElement updateAttrs updateStyle 等。JS Framework 使用這些接口將本身內部構建的 Element 節點樹以渲染指令的形式發給客戶端。

繪製原生 UI

客戶端接收 JS Framework 發送的渲染指令,建立相應的原生組件,最終調用系統提供的接口繪製原生 UI。具體細節這裏就不展開了。

事件的響應過程

不管是在瀏覽器仍是 Weex 裏,事件都是由原生 UI 捕獲的,然而事件處理函數都是寫在前端裏的,因此會有一個傳遞的過程。

fire event

如上圖所示,若是在 Vue.js 裏某個標籤上綁定了事件,會在內部執行 addEventListener 給節點綁定事件,這個接口在 Weex 平臺下調用的是 JS Framework 提供的 addEvent 方法向元素上添加事件,傳遞了事件類型和處理函數。JS Framework 不會當即向客戶端發送添加事件的指令,而是把事件類型和處理函數記錄下來,節點構建好之後再一塊兒發給客戶端,發送的節點中只包含了事件類型,不含事件處理函數。客戶端在渲染節點時,若是發現節點上包含事件,就監聽原生 UI 上的指定事件。

當原生 UI 監聽到用戶觸發的事件之後,會派發 fireEvent 命令把節點的 ref、事件類型以及事件對象發給 JS Framework。JS Framework 根據 ref 和事件類型找到相應的事件處理函數,而且以事件對象 event 爲參數執行事件處理函數。目前 Weex 裏的事件模型相對比較簡單,並不區分捕獲階段和冒泡階段,而是隻派發給觸發了事件的節點,並不向上冒泡,相似 DOM 模型裏 level 0 級別的事件。

上述過程裏,事件只會綁定一次,可是極可能會觸發屢次,例如 touchmove 事件,在手指移動過程當中,每秒可能會派發幾十次,每次事件都對應了一次 fireEvent -> invokeHandler 的處理過程,很容易損傷性能,瀏覽器也是如此。針對這種狀況,可使用用 expression binding 來將事件處理函數轉成表達式,在綁定事件時一塊兒發給客戶端,這樣客戶端在監聽到原生事件之後能夠直接解析並執行綁定的表達式,而不須要把事件再派發給前端。

寫在最後

Weex 是一個跨端的技術,涉及的技術面比較多,只從前端或者客戶端的某個角度去理解都是不全面的,本文只是之前端開發者的角度介紹了 Weex 其中一部分的功能。若是你對 Weex 的 JS Framework 有什麼新的想法和建議,歡迎賜教;對 Weex 有使用心得或者踩坑經歷,也歡迎分享。

相關文章
相關標籤/搜索