天天看點

重新介紹 Weex 的 JS Framework

Weex 是一個既支援多個前端架構又能跨平台渲染的架構,JS Framework 介于前端架構和原生渲染引擎之間,處于承上啟下的位置,也是跨架構跨平台的關鍵。無論你使用的是 Vue 還是 Rax,無論是渲染在 Android 還是 iOS,JS Framework 的代碼都會運作到(如果是在浏覽器和 WebView 裡運作,則不依賴 JS Framework)。

重新介紹 Weex 的 JS Framework

像 Vue 和 Rax 這類前端架構雖然内部的渲染機制、Virtual DOM 的結構都是不同的,但是都是用來描述頁面結構以及開發範式的,對 Weex 而言隻屬于文法層,或者稱之為 DSL (Domain Specific Language)。無論前端架構裡資料管理群組件管理的政策是什麼樣的,它們最終都将調用 JS Framework 提供的接口來調用原生功能并且渲染真實 UI。底層渲染引擎中也不必關心上層架構中元件化的文法和更新政策是怎樣的,隻需要處理 JS Framework 中統一定義的節點結構和渲染指令。多了這麼一層抽象,有利于标準的統一,也使得跨架構和跨平台成為了可能。

圖雖然這麼畫,但是大部分人并不區分得這麼細,喜歡把 Vue 和 Rax 以及下邊這一層放一起稱為 JS Framework。

如果将 JS Framework 的功能進一步拆解,可以分為如下幾個部分:

适配前端架構

建構渲染指令樹

JS-Native 通信

JS Service

準備環境接口

前端架構在 Weex 和浏覽器中的執行過程不一樣,這個應該不難了解。如何讓一個前端架構運作在 Weex 平台上,是 JS Framework 的一個關鍵功能。

以 Vue.js 為例,在浏覽器上運作一個頁面大概分這麼幾個步驟:首先要準備好頁面容器,可以是浏覽器或者是 WebView,容器裡提供了标準的 Web API。然後給頁面容器傳入一個位址,通過這個位址最終擷取到一個 HTML 檔案,然後解析這個 HTML 檔案,加載并執行其中的腳本。想要正确的渲染,應該首先加載執行 Vue.js 架構的代碼,向浏覽器環境中添加 <code>Vue</code> 這個變量,然後建立好挂載點的 DOM 元素,最後執行頁面代碼,從入口元件開始,層層渲染好再挂載到配置的挂載點上去。

在 Weex 裡的執行過程也比較類似,不過 Weex 頁面對應的是一個 js 檔案,不是 HTML 檔案,而且不需要自行引入 Vue.js 架構的代碼,也不需要設定挂載點。過程大概是這樣的:首先初始化好 Weex 容器,這個過程中會初始化 JS Framework,Vue.js 的代碼也包含在了其中。然後給 Weex 容器傳入頁面位址,通過這個位址最終擷取到一個 js 檔案,用戶端會調用 createInstance 來建立頁面,也提供了重新整理頁面和銷毀頁面的接口。大緻的渲染行為和浏覽器一緻,但是和浏覽器的調用方式不一樣,前端架構中至少要适配用戶端打開頁面、銷毀頁面(push、pop)的行為才可以在 Weex 中運作。

重新介紹 Weex 的 JS Framework

在 JS Framework 裡提供了如上圖所示的接口來實作前端架構的對接。圖左側的四個接口與頁面功能有關,分别用于擷取頁面節點、監聽用戶端的任務、注冊元件、注冊子產品,目前這些功能都已經轉移到 JS Framework 内部,在前端架構裡都是可選的,有特殊處理邏輯時才需要實作。圖右側的四個接口與頁面的生命周期有關,分别會在頁面初始化、建立、重新整理、銷毀時調用,其中隻有 <code>createInstance</code> 是必須提供的,其他也都是可選的(在新的 Sandbox 方案中,<code>createInstance</code> 已經改成了 <code>createInstanceContext</code>)。詳細的初始化和渲染過程會在後續章節裡展開。

不同的前端架構裡 Virtual DOM 的結構、patch 的方式都是不同的,這也反應了它們開發理念和優化政策的不同,但是最終,在浏覽器上它們都使用一緻的 DOM API 把 Virtual DOM 轉換成真實的 HTMLElement。在 Weex 裡的邏輯也是類似的,隻是在最後一步生成真實元素的過程中,不使用原生 DOM API,而是使用 JS Framework 裡定義的一套 Weex DOM API 将操作轉化成渲染指令發給用戶端。

重新介紹 Weex 的 JS Framework

JS Framework 提供的 Weex DOM API 和浏覽器提供的 DOM API 功能基本一緻,在 Vue 和 Rax 内部對這些接口都做了适配,針對 Weex 和浏覽器平台調用不同的接口就可以實作跨平台渲染。

此外 DOM 接口的設計相當複雜,背負了大量的曆史包袱,也不是所有特性都适合移動端。JS Framework 裡将這些接口做了大量簡化,借鑒了 W3C 的标準,隻保留了其中最常用到的一部分。目前的狀态是夠用、精簡高效、和 W3C 标準有很多差異,但是已經成為 Vue 和 Rax 渲染原生 UI 的事實标準,後續還會重新設計這些接口,使其變得更标準一些。JS Framework 裡 DOM 結構的關系如下圖所示:

重新介紹 Weex 的 JS Framework

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

這顆樹的層次結構和原生 UI 的層次結構是一緻的,目前端的節點有更新時,這棵樹也會跟着更新,然後把更新結果以渲染指令的形式發送給用戶端。這棵樹并不計算布局,也沒有什麼副作用,操作也都是很高效的,基本都是 O(1) 級别,偶爾有些 O(n) 的操作會周遊同層兄弟節點或者上溯找到根節點,不會周遊整棵樹。

在開發頁面過程中,除了節點的渲染以外,還有原生子產品的調用、事件綁定、回調等功能,這些功能都依賴于 js 和 native 之間的通信來實作。

重新介紹 Weex 的 JS Framework

首先,頁面的 js 代碼是運作在 js 線程上的,然而原生元件的繪制、事件的捕獲都發生在 UI 線程。在這兩個線程之間的通信用的是 <code>callNative</code> 和 <code>callJS</code> 這兩個底層接口(現在已經擴充到了很多個),它們預設都是異步的,在 JS Framework 和原生渲染器内部都基于這兩個方法做了各種封裝。

<code>callNative</code> 是由用戶端向 JS 執行環境中注入的接口,提供給 JS Framework 調用,界面的節點(上文提到的渲染指令樹)、子產品調用的方法和參數都是通過這個接口發送給用戶端的。為了減少調用接口時的開銷,其實作在已經開了更多更直接的通信接口,其中有些接口還支援同步調用(支援傳回值),它們在原理上都和 <code>callNative</code> 是一樣的。

<code>callJS</code> 是由 JS Framework 實作的,并且也注入到了執行環境中,提供給用戶端調用。事件的派發、子產品的回調函數都是通過這個接口通知到 JS Framework,然後再将其傳遞給上層前端架構。

Weex 是一個多頁面的架構,每個頁面的 js bundle 都在一個獨立的環境裡運作,不同的 Weex 頁面對應到浏覽器上就相當于不同的“标簽頁”,普通的 js 庫沒辦法實作在多個頁面之間實作狀态共享,也很難實作跨頁通信。

由于 Weex 運作環境和浏覽器環境有很大差異,在 JS Framework 裡還對一些環境變量做了封裝,主要是為了解決解決原生環境裡的相容問題,底層使用渲染引擎提供的接口。主要的改動點是:

console: 原生提供了 <code>nativeLog</code> 接口,将其封裝成前端熟悉的 <code>console.xxx</code> 并可以控制日志的輸出級别。

timer: 原生環境裡 timer 接口不全,名稱和參數不一緻。目前來看有了原生 C/C++ 實作的 timer 後,這一層可以移除。

freeze: 當機目前環境裡全局變量的原型鍊(如 Array.prototype)。

另外還有一些 ployfill:<code>Promise</code> 、<code>Arary.from</code> 、<code>Object.assign</code> 、<code>Object.setPrototypeOf</code> 等。

這一層裡的東西可以說都是用來“填坑”的,也是與環境有關 Bug 的高發地帶,如果你隻看代碼的話會覺得莫名奇妙,但是它很可能解決了某些版本某個環境中的某個神奇的問題,也有可能觸發了一個更神奇的問題。随着對 JS 引擎本身的優化和定制越來越多,這一層代碼可以越來越少,最終會全部移除掉。

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

JS Framework 以及 Vue 和 Rax 的代碼都是内置在了 Weex SDK 裡的,随着 Weex SDK 一起初始化。SDK 的初始化一般在 App 啟動時就已經完成了,隻會執行一次。初始化過程中與 JS Framework 有關的是如下這三個操作:

初始化 JS 引擎,準備好 JS 執行環境,向其中注冊一些變量和接口,如 <code>WXEnvironment</code>、<code>callNative</code>。

執行 JS Framework 的代碼。

注冊原生元件和原生子產品。

針對第二步,執行 JS Framework 的代碼的過程又可以分成如下幾個步驟:

注冊上層 DSL 架構,如 Vue 和 Rax。這個過程隻是告訴 JS Framework 有哪些 DSL 可用,适配它們提供的接口,如 <code>init</code>、<code>createInstance</code>,但是不會執行前端架構裡的邏輯。

初始化環境變量,并且會将原生對象的原型鍊當機,此時也會注冊内置的 JS Service,如 <code>BroadcastChannel</code>。

如果 DSL 架構裡實作了 <code>init</code> 接口,會在此時調用。

向全局環境中注入可供用戶端調用的接口,如 <code>callJS</code>、<code>createInstance</code>、<code>registerComponents</code>,調用這些接口會同時觸發 DSL 中相應的接口。

再回顧看這兩個過程,可以發現原生的元件和子產品是注冊進來的,DSL 也是注冊進來的,Weex 做的比較靈活,元件子產品是可插拔的,DSL 架構也是可插拔的,有很強的擴充能力。

在初始化好 Weex SDK 之後,就可以開始渲染頁面了。通常 Weex 的一個頁面對應了一個 js bundle 檔案,頁面的渲染過程也是加載并執行 js bundle 的過程,大概的步驟如下圖所示:

重新介紹 Weex 的 JS Framework

首先是調用原生渲染引擎裡提供的接口來加載執行 js bundle,在 Android 上是 <code>renderByUrl</code>,在 iOS 上是 <code>renderWithURL</code>。在得到了 js bundle 的代碼之後,會繼續執行 SDK 裡的原生 <code>createInstance</code> 方法,給目前頁面生成一個唯一 id,并且把代碼和一些配置項傳遞給 JS Framework 提供的 <code>createInstance</code> 方法。

在 JS Framework 接收到頁面代碼之後,會判斷其中使用的 DSL 的類型(Vue 或者 Rax),然後找到相應的架構,執行 <code>createInstanceContext</code> 建立頁面所需要的環境變量。

重新介紹 Weex 的 JS Framework

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

Weex 裡頁面的渲染過程和浏覽器的渲染過程類似,整體可以分為【建立前端元件】-&gt; 【建構 Virtual DOM】-&gt;【生成“真實” DOM】-&gt;【發送渲染指令】-&gt;【繪制原生 UI】這五個步驟。前兩個步驟發生在前端架構中,第三和第四個步驟在 JS Framework 中處理,最後一步是由原生渲染引擎實作的。下圖描繪了頁面渲染的大緻流程:

重新介紹 Weex 的 JS Framework

以 Vue.js 為例,頁面都是以元件化的形式開發的,整個頁面可以劃分成多個層層嵌套和平鋪的元件。Vue 架構在執行渲染前,會先根據開發時編寫的模闆建立相應的元件執行個體,可以稱為 Vue Component,它包含了元件的内部資料、生命周期以及 <code>render</code> 函數等。

如果給同一個模闆傳入多條資料,就會生成多個元件執行個體,這可以算是元件的複用。如上圖所示,假如有一個元件模闆和兩條資料,渲染時會建立兩個 Vue Component 的執行個體,每個元件執行個體的内部狀态是不一樣的。

以上過程在 Weex 和浏覽器裡都是完全一樣的,從生成真實 DOM 這一步開始,Weex 使用了不同的渲染方式。前面提到過 JS Framework 中提供了和 DOM 接口類似的 Weex DOM API,在 Vue 裡會使用這些接口将 <code>VNode</code> 渲染生成适用于 Weex 平台的 <code>Element</code> 對象,和 DOM 很像,但并不是“真實”的 DOM。

在 JS Framework 内部和用戶端渲染引擎約定了一系列的指令接口,對應了一個原子的 DOM 操作,如 <code>addElement</code> <code>removeElement</code> <code>updateAttrs</code> <code>updateStyle</code> 等。JS Framework 使用這些接口将自己内部建構的 Element 節點樹以渲染指令的形式發給用戶端。

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

無論是在浏覽器還是 Weex 裡,事件都是由原生 UI 捕獲的,然而事件處理函數都是寫在前端裡的,是以會有一個傳遞的過程。

重新介紹 Weex 的 JS Framework

如上圖所示,如果在 Vue.js 裡某個标簽上綁定了事件,會在内部執行 <code>addEventListener</code> 給節點綁定事件,這個接口在 Weex 平台下調用的是 JS Framework 提供的 <code>addEvent</code> 方法向元素上添加事件,傳遞了事件類型和處理函數。JS Framework 不會立即向用戶端發送添加事件的指令,而是把事件類型和處理函數記錄下來,節點建構好以後再一起發給用戶端,發送的節點中隻包含了事件類型,不含事件處理函數。用戶端在渲染節點時,如果發現節點上包含事件,就監聽原生 UI 上的指定事件。

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

上述過程裡,事件隻會綁定一次,但是很可能會觸發多次,例如 <code>touchmove</code> 事件,在手指移動過程中,每秒可能會派發幾十次,每次事件都對應了一次 <code>fireEvent</code> -&gt; <code>invokeHandler</code> 的處理過程,很容易損傷性能,浏覽器也是如此。針對這種情況,可以使用用 expression binding 來将事件處理函數轉成表達式,在綁定事件時一起發給用戶端,這樣用戶端在監聽到原生事件以後可以直接解析并執行綁定的表達式,而不需要把事件再派發給前端。

其實在 Weex 裡,能跨多個渲染引擎通用的不止 JS Framework,還有 Weex Core,它們要解決的問題差不多,然而 JS Framework 是用 javascript 寫的,Weex Core 是用 C/C++ 寫的,實作的功能更底層一些,其實你可以将 JS Framework 了解為 js 版本的 Weex Core。不過 Weex Core 目前的功能還比較少,和 JS Framework 沒有重疊,隻包含了對 JS 引擎的優化和新的 CSS 布局引擎。

文章最開始的第一張圖,其實應該畫成這樣:

重新介紹 Weex 的 JS Framework

随着技術的演進,JS Framework 的大部分功能将逐漸轉移到 Weex Core 中,文章裡介紹的用 js 實作的功能,最終将會用 C/C++ 實作,性能會有大幅提升,結構也會簡化。 這個過程不是簡單的複制複制代碼、調調接口就能完成的事,語言都變了,大部分接口和特性都要重新設計,還得做到向下相容,而且也不能是埋頭做個三五個月然後再出結果,每個步驟都要保證功能可用。不能停車,邊開邊更新發動機。

具體來講,首先要做的就是在 Weex Core 中實作一份 DOM 接口,将會設計得更加标準、更加符合規範,有了原生的 <code>document</code> 、<code>Element</code> 這些對象以後,前端架構就可以直接調用原生接口不必經過 JS Framework,渲染性能會有提升,JS Framework 裡的那顆不知道怎麼稱呼的樹也就可以拿掉了,也不需要将節點發送用戶端了,這樣通信的邏輯也可以大幅簡化。等把原生子產品的調用、回調、事件響應這些問題解決了之後,JS-Native 之間的通信也可以拿掉了。

重新介紹 Weex 的 JS Framework

就像上面這幅圖畫得那樣,JS Framework 會變成非常薄的一層,僅負責适配前端架構和修複一些相容性的問題,最終在未來某個版本裡,可能根本就不存在了。

如果你對 Weex 的 JS Framework 有什麼新的想法和建議,歡迎來找我聊。@門柳

繼續閱讀