天天看點

Weex 架構中 JS Framework 的結構

weex 具有移動端跨平台的特性,js framework 是其中比較關鍵的一層。首先來看一下 js framework 在 weex 中的位置:

Weex 架構中 JS Framework 的結構

從圖中可以看出 weex 整體的工作流程。首先開發者可以聲明式的定義元件,形成 <code>.we</code> 檔案,通過 weex-toolkit 提供的工具将 <code>.we</code> 檔案轉為 js bundle。js framework 接收并執行 js bundle 的代碼,并且執行資料綁定、模闆編譯等操作,然後輸出 json 格式的 virtual dom 傳遞給移動端,同時也提供了 <code>callnative</code> 和 <code>calljs</code> 接口,友善 js framework 和 native 的通信。同樣的一份 json 資料,在不同平台的渲染引擎中能夠渲染成不同版本的 ui,這也是 weex 可以實作動态化的原因。

簡而言之,js framework 的輸入是 js bundle,輸出是 json 格式的 virtual dom,同時也提供了與 native 通信的方法。

文中代碼的版本是 v0.15 。

weex 執行個體包含了如下方法:

Weex 架構中 JS Framework 的結構
注:在 web 環境下,挂在 window 上的變量名是小寫 <code>weex</code>,而且經過了封裝,并非 js framework 直接暴露的接口。

在擷取到 js bundle 後,會調用 <code>createinstance</code> 建立頁面執行個體。它首先會 <code>new app()</code> 建立新的 app 執行個體對象,并且把對象放入 <code>instancemap</code> 中。app 執行個體中有如下幾個常用屬性:

<code>id</code> 與 native 端通信時的唯一辨別。

<code>vm</code> view model,元件模型,包含了資料綁定相關功能。

<code>doc</code> virtual dom 中的根節點。

由于 js bundle 是工具打包生成的 js 代碼,app 執行個體建立完成後,會通過 <code>new function</code> 的方式來執行。在代碼中用到的 <code>define</code> 、<code>require</code> 、<code>bootstrap</code> 、<code>document</code> 、<code>register</code> 、<code>render</code> 等方法都是在 js framework 的 <code>init</code> 中定義的,以參數的方式傳遞到 js bundle 中。<code>new function</code> 中代碼将會在全局環境中執行,并不能擷取到 js framework 執行環境中的資料(除了以參數傳遞過去的那些)。js bundle 本身也用了立即執行函數做封裝,并不會污染全局環境。

注: 使用 <code>new function</code> 可能會導緻一些性能問題,目前正在嘗試其他執行方式,新版本建立 app 執行個體的過程可能會有所不同。

在加載 js bundle 過程中,會首先執行 define &amp; require 的功能,使用者自定義的子產品,放在了 <code>app.customcomponentmap</code> 中,然後對調用 <code>bootstrap</code> 方法啟動根元件。<code>bootstrap</code> 方法首先會校驗一下參數和環境,如果不符合條件可能會觸發頁面降級(也可以手動設定使頁面降級,這一特性可以在 native 出現問題時,使頁面降級為 html5 運作)。

<code>bootstrap</code> 最後會建立應用的 <code>vm</code> 執行個體,整個過程可以分成三個步驟:

initevents 初始化事件和生命周期。

initstate 實作資料綁定功能。

編譯模闆并且繪制 native ui。

initevents 會依次綁定三類事件:<code>options</code> 參數中定義的事件、<code>externalevents</code> 外部事件、内置的生命周期事件,前兩項通常都為 <code>null</code>,生命周期包含了<code>init</code> 、<code>created</code> 、 <code>ready</code> 三個鈎子。生命周期函數可以在元件中定義,具體觸發時機如下:

事件綁定完畢後會立即觸發 <code>hook:init</code> 事件,并且将 <code>_inited</code> 屬性設定為 true。

資料綁定的核心思想是基于 es5 的 <code>object.defineproperty</code> 方法,在 vm 執行個體上建立了一系列的 getter / setter,支援數組和深層對象,在設定屬性值的時候,會派發更新事件。這部分功能的實作借鑒了 vue 的思路以及部分代碼。資料綁定的過程主要涉及了三個對象:

Weex 架構中 JS Framework 的結構

在執行資料綁定之前,會将參數中傳遞的資料 merge 到 <code>_data</code> 屬性中來,然後執行 <code>initstate</code>,分為三個步驟:

initdata,設定 proxy,監聽 <code>_data</code> 中的屬性;然後添加 reactivegetter &amp; reactivesetter 實作資料監聽。 (這個過程比較繁瑣,涉及很多技巧,以後新開文章講解)

initcomputed,初始化計算屬性,隻有 getter,在 <code>_data</code> 中沒有對應的值。

initmethods 将 <code>_method</code> 中的方法挂在執行個體上。

建立的 <code>observer</code> 的執行個體會挂載到 <code>_data.__ob__</code> 屬性中。資料綁定結束後會觸發 <code>hook:created</code> 事件,并且将 <code>_created</code> 屬性設定為 true。

模闆編譯函數 <code>build</code> 會調用 <code>compile</code> 函數,<code>compile</code> 會遞歸編譯整個模闆,這個過程會展開自定義的元件,編譯指令,也會執行一些資料綁定,最終生成 virtual dom。其中,真正建立節點的是 <code>createbody</code> 和 <code>createelement</code> 兩個方法,<code>createbody</code> 隻會在建立根節點時調用。

此外還有一個比較常用的方法:<code>createblock</code>,它會建立一個特殊格式的 block,在真實 <code>element</code> 的開始和結束位置會添加兩個 <code>comment</code> 節點,在編譯過程中可以和 element 同等對待。之是以這麼設計,是為了友善編譯 <code>if</code> 、<code>repeat</code> 等指令,當其綁定的資料項發生變化時,可以快速定位到需要改變的 dom 節點,僅在 start 和 end 兩個 comment 元素之間執行操作。

在編譯過程中,會根據節點的類型不同,将編譯邏輯分派到不同的函數中,主要包含以下幾種:

<code>compilerepeat</code>: 編譯 <code>repeat</code> 指令,同時會執行資料綁定,在資料變動時會觸發 dom 節點的更新。

<code>compileshown</code>: 編譯 <code>if</code> 指令,也會執行資料綁定。

<code>compilefragment</code>: 編譯多個節點,建立 fragment 片段。

<code>compilechildren</code>: 編譯子元件,用于實作遞歸。

<code>compiletype</code>: 編譯動态類型的元件。

<code>compilecustomcomponent</code>: 編譯展開使用者自定義的元件,這個過程會遞歸建立子 <code>vm</code>,并且綁定父子關系,也會觸發子元件的生命周期函數。

<code>compilenativecomponent</code>: 編譯内置原生元件。這個方法會調用 <code>createbody</code> 或 <code>createelement</code> 與原生子產品通信并建立 native ui。

在 js framework 中實作的 virtual dom,包含了四類對象:<code>document</code> 、<code>node</code> 、<code>element</code> 、<code>comment</code>,接口的定義也基本上都和 w3c 标準保持一緻,不過要更為精簡一些。

Weex 架構中 JS Framework 的結構

不過,這裡建立的是 virtual dom,如何在不同的平台上建立 native ui ?

在 <code>document</code> 對象中包含一個 <code>listener</code> 屬性,它可以向 native 端發送消息,每當建立元素或者是有更新操作時,listener 就會拼裝出制定格式的 action,并且最終調用 <code>callnative</code> 把 action 傳遞給原生子產品,原生子產品中也定義了相應的方法來執行 action 。

例如當某個元素執行了 <code>element.appendchild()</code> 時,就會調用 <code>listener.addelement()</code>,然後就會拼成一個如下格式的 action 通過 callnative 傳遞給原生子產品。

模闆編譯的過程需要遞歸生成整個 virtual dom tree,期間還會與原生子產品密集通信,會消耗很多記憶體和計算資源,這個過程通常也是性能瓶頸。

在模闆編譯完成後,會觸發 <code>hook:ready</code> 事件。

這篇文章簡單講述了 js framework 的功能以及實作方法,是我自己對 js framework 的了解,如果發現了不嚴謹地方或者有其他觀點,歡迎一起探讨。

繼續閱讀