關注 Weex 開發進展的同窗,可能會知道 Weex 前段時間發佈了 v0.18.0 版本(release note),其中包含了一個叫 <recycle-list>
的組件,它是一個帶有回收複用功能的列表容器,聽說是有史以來最特別的組件,性能也有大幅提高,開發過程也涉及到不少底層的改造,陸陸續續花了半年才實現了第一個正式的版本。<recycle-list>
的文檔也在官網上線了,不過總體看下來好像和普通的 <list>
也差很少,反而多了一大堆莫名其妙的注意事項,一副很敏感又很脆弱的樣子,真的有那麼好用嗎?這篇文章裏就好好聊一聊它的特別之處。html
在現在 App 的開發中,有大部分的頁面都是以可滾動列表的形式展示的,尤爲是在貨架式琳琅滿目的活動頁面中,更是長列表的主場,並且愈來愈長,帶上「懶加載」和「自動加載更多」之後,其實就是一個能夠無限滾動的列表。因此說,列表的性能和體驗每每從很大程度上決定了頁面的性能和體驗,優化了列表的性能就會大幅提升頁面的性能。前端
Weex 目前提供的列表組件 <list>
其實已是功能很強大的一個組件了,在 Android 上使用的是 RecyclerView
組件,在 iOS 上使用的是 UITableView
,自己就具備了操做系統原生提供的回收功能,在節點離屏時能夠回收掉部分原生組件持有的內存。和 Web 中的開發技術相比,在 webview 中實現的列表,不管是渲染性能、滾動體驗仍是內存佔用方面,都難以和原生列表相媲美。即使如此,性能也永遠是值得優化的,使用 Weex 的開發者對列表性能的追求也是永無止境的。vue
就像你們以爲前端框架引入 Virtual DOM 以後就必定比原生 DOM 慢同樣,一些 Weex 的原生開發者也以爲 Weex 提供的列表畢竟多了一層封裝,不能精細地操控列表的渲染行爲,性能必定不如直接操做原生列表。這也是有必定道理的,若是再仔細分析一下這些需求,到底如何精細操控列表的渲染行爲能提高性能呢?有沒有辦法抽象出通用的邏輯呢?假如說不考慮兼容現有的 list 組件,也容許對框架和現有渲染流程作重構級別的改動,能不能開個腦洞放個大招來提高列表的性能呢?這也是要開發新列表容器的出發點。git
既然名字叫 recycle-list,它與普通 list 的最大差別就在於節點的回收複用能力。github
在大部分使用列表的場景中,有不少行節點的結構都是大體相同的,一個列表可能有 500 行那麼長,所有展開的話長度會超過 100 屏,可是極可能只用了 5 個不一樣的模板。若是在渲染這 500 行節點的時候,能不斷複用這 5 個模板結構的話,只渲染可視區內的組件的話,確定能大幅優化列表的渲染性能。web
因此在渲染 recycle-list 的時候,會記錄不一樣的模板結構,用數據驅動模板的渲染,首次渲染時只會先建立首屏以及有可能滾動到的安全區域內的節點;在滾動時,會將脫離安全區域內的節點回收,清空模板並灌注新數據追加到即將出現的區域內。這是 recycle-list 在渲染行爲上最大的不一樣。基於這種行爲,前端和客戶端之間節點的通訊數據量將會減小,列表的內存也能夠獲得大幅的優化,即便列表愈來愈長,內存的增量也不會不少。編程
首先分析一下目前在 Weex 裏常規組件的渲染流程是怎樣的。segmentfault
在 Weex 的架構中,能夠簡略的分紅三層:【DSL】->【JS Framework】->【原生渲染引擎】。其中 DSL (Domain Specific Language) 指的是 Weex 裏支持的上層前端框架,即 Vue 和 Rax。原生渲染引擎就是在 Weex 支持的平臺上(Android 或 iOS)繪製原生 UI 的引擎。JS Framework 是橋接並適配 DSL 和原生渲染引擎的一層。參考 《詳細介紹 Weex 的 JS Framework》。
常規組件的渲染過程能夠分爲以下這幾個步驟:數組
以 Vue.js 爲例,它在 Weex 裏的渲染過程能夠用下面這一張圖來歸納:安全
簡而言之,模板是能夠被複用的,傳入多條數據能夠展開成多個前端組件,這也是組件化的優點之一,組件進一步在前端框架中展開成 VNode 節點。JS Framework 裏提供了不少相似 DOM API 的接口,在內部構建出適用於 Weex 平臺的 Element
節點(和 DOM 很像,但並非「真實」的 DOM),這些節點會以渲染指令的形式發給客戶端。客戶端根據渲染指令建立相應的原生組件,最終調用系統提供的接口繪製原生 UI。具體過程請參考:Weex 頁面的渲染。
回顧上述過程能夠看出,組件的模板結構是可複用的,這也是組件化的優點之一,可是組件的展開發生在前端框架內部,在傳遞到客戶端的過程當中,節點的結構保留了,可是組件的信息都被過濾掉了。即便同一個組件使用兩份數據來渲染,生成了兩份結構一致只有小部份內容有差別的節點,客戶端也不會認爲他們之間有聯繫,依然彼此獨立的渲染。也就是說,在常規組件的渲染流程中,客戶端感知不到前端組件的概念,渲染粒度過小,難以複用。
借鑑函數式編程裏的惰性計算的思路,能夠將渲染過程延後,交給客戶端執行,這樣客戶端就能更好的施展複用邏輯。具體來說就是不把節點在前端渲染好了再把結果發給客戶端,而是把「渲染方法」和數據分別發給客戶端,避免模板在前端框架中展開,客戶端根據數據和用戶的操做行爲控制模板的渲染和複用。
如上圖所示,前端框架中的 Template
再也不須要數據,而是直接展開成一種純靜態的模板結構,結構中包含了模板渲染邏輯,格式仍然是 VNode
。而後通過 JS Framework 轉換成 Weex 支持的 Element
,其中也包含了模板的原生渲染指令。客戶端解析出可複用的模板結構,由數據驅動模板渲染,這個模板結構和前端組件中的定義是一致的。
這個過程除了要把模板發給客戶端,還得帶上模板的渲染邏輯,告訴客戶端如何根據數據來渲染模板。爲了描述這些渲染邏輯,就得設計一套面向原生渲染引擎的模板渲染指令,用來聲明節點的循環渲染、條件渲染、事件綁定等邏輯。下文有詳解。
上述改造過程若是能實現的話,從理論上上講,內存和渲染性能必然會有提高,並且列表越長性能優點越明顯。下面也從實際的數據中看一下性能的對比結果究竟是怎樣的。
目前 Weex 提供了 <scroller>
、 <list>
、 和 <recycle-list>
這三種可滾動容器,功能看起來差很少,可是能力和特徵都有差別。爲了方便比較性能,咱們對一樣的一個頁面,分別使用了不一樣的列表容器來實現,並記錄了在 iOS 和 Android 下頁面的加載時間、進入頁面時的內存、滑動到頁面底部時的內存、滑動時CPU的使用量等數據。
使用的測試用例以下:
<scroller>
。<list>
。<recycle-list>
(須要使用最新版 playground app 掃碼才能看到渲染效果)。在 iOS 設備(iPhone 6, iOS 11.0)中的測試結果以下所示:
在 Android 設備(Honor 6x, Android 7.0)中的測試結果以下所示:
從上面的數據能夠看出,<list>
相比 <scroller>
已經有了較大的性能提高,<recycle-list>
比 <list>
的性能表現更加優秀。尤爲在內存方面,<recycle-list>
在 iOS 下的內存佔用量始終保持在個位數,在 Android 下除此加載時的內存和滑動到底部時的內存也分別優化了 42.7% 和 23.6%。
recycle-list 不只特別,也是開發跨時最久的一個組件了,從最先明確提出 Proposal(2017/08/04)到發佈 v0.18.0(2018/02/09)歷時長達半年之久。由於它是一個重要但不緊急的功能,在研發期間不斷被打斷,自己的技術難度由很大,涉及的技術面比較多,整個研發過程也是陸陸續續、磕磕絆絆、邊探索邊驗證。
recycle-list 雖然說是一個組件,可是它開闢了一條新的渲染模式,不管是前端框架、JS Framework 仍是原生渲染引擎都有重構級別的改造;開發者也是多樣的,前端、iOS、Android 都全程參與了。因爲這個組件涉及大量對前端框架內部的改造,Vue.js 的原做者尤雨溪(微博 @尤小右) 也深度參與了開發和討論,尤爲在前期討論實現方案的時候提供了大量思路。這個組件不管是技術方案仍是開發協做方式都和以往不一樣,能夠說是至關特別了。
這個組件雖然開發歷時好久,可是在討論了大體思路之後,幾乎在前幾天內就作出了一個 MVP (Minimum Viable Product) 版原本驗證想法,而且當即對比了渲染性能。
爲了快速驗證想法,先隨意約定了一套模板指令,繞過前端框架和 JS Framework 的渲染流程,直接手寫 callNative
指令將模板結構和數據發給客戶端,客戶端也不考慮兼容性和反作用,先實現了渲染和複用的基本邏輯。這個步驟只是用來驗證設想的方案是否可行,若是行不通就不必繼續浪費時間。
雖然快速作出了 MVP 版本,看似已經成功一半,可是設計得太過粗糙,不少流程並未想清楚,原有列表的大部分功能都沒有實現思路,真正的進度可能連 10% 都不到。
驗證了可行性以後,下一步並無當即繼續寫代碼,而是靜下心來認真再討論一下詳細的技術方案。這個過程邀請了尤雨溪一塊兒參與,從編譯工具、上層語法糖到組件生命週期和狀態同步等功能,都作過細緻的分析和討論。
最初在討論的時候,以爲生命週期和有狀態的子組件這些功能都是沒法實現的,由於組件的私有狀態和生命週期是在前端框架裏的,然而組件渲染過程又徹底交給了客戶端來控制,語言都不同,甚至不在同一個線程裏,簡直沒法再聯繫起來。不過最終仍是設計出了一系列的通訊和狀態同步機制,將功能作得更完善,下文有詳解。
在明確實現細節的過程當中,因爲沒有兼容歷史版本的包袱,開發期間能夠冷靜思考真正合理而且好用的技術方案,不惜屢次推翻原有的設計,反覆重構代碼,最終才能實現「看起來和舊的 list 差很少嘛,無非是用了新的名字多了 for/switch/case
的語法而已」這種效果。
有了詳細的設計之後,前端、iOS、Android 開發者分別獨立開發,同時編譯工具的用例也在不斷的更新,三端都頻繁的迭代,漸進式的完善功能。這個項目的前期工做作的比較足,先有的使用文檔和實現方案,而後有的測試用例和各類 demo,最後纔是寫代碼實現功能,開發流程仍是比較工整的。
目前發佈的第一個版本中,基礎功能都完備了,可是存在較多注意事項,有些是組件固有的差別,還有些是正在討論技術方案但還沒來得及實現的功能,如支持動態綁定樣式類名、雙向綁定、filter、組件的自定義事件等。這些功能將在後續版本里逐步實現,雖然它們寫出來只有短短几個字,看起來也都是現有組件理所固然就支持的功能,可是在 recycle-list 裏可能對應了涉及多端的大範圍改造。
在前面的章節裏介紹了可複用列表的渲染過程,這只是開了個頭,想要實現這個效果,至少要涉及編譯工具、客戶端渲染引擎以及前端框架裏的改造。
把「渲染方法」發給客戶端,提及來簡單,這裏邊包含了循環、條件、使用自定義組件的邏輯,能把它們完備地發給客戶端嗎?絕大多數渲染邏輯均可以。 要實現這個功能,就得設計一套描述渲染邏輯的原生指令,保障自身的完備性,而後對接上層前端框架中的模板語法,這個對接過程能夠交給工具在編譯期實現。
以 Vue 爲例,它提供了單文件組件的語法,其中 v-bind
、 v-for
、 v-if
之類的特殊屬性(模板指令),以及 {{}}
中的數據綁定都是描述渲染邏輯的,這些特殊語法若是用在 recycle-list 裏,將會被編譯工具編譯成 Weex 支持的原生渲染指令。這層渲染指令是面向客戶端的渲染行爲設計的,是原生渲染器和 JS Framework 之間的約定,能夠對接到 Vue 和 Rax 等多個上層框架,語法基本上都是一一對應的。具體的語法規則,能夠參考 Implementation.md#模板語法。
Vue 裏的渲染邏輯是聲明式的寫在模板裏的,所以能夠很容易的編譯成 Weex 的原生渲染指令,整個轉換過程能夠融入到現有的編譯工具中處理,對上層開發者透明,基本上對開發過程無影響,也不影響原有功能。在 Rax/React 的渲染函數中,標籤語法可使用 JSX 編寫,可是模板的渲染規則(循環和條件)仍然由 JS 腳原本控制,是命令式的而不是聲明式的,很難編譯成靜態的描述,要想使用長列表的複用功能,須要對開發時的寫法作特殊約定,或者使用特殊的 渲染流程控制組件。
客戶端拿到了數據和模板之後,在內部創建起 Watcher 和 Updater 的更新機制,由數據驅動模板的渲染。在最初渲染時只渲染屏幕內呈現出來的節點。
當列表向下滾動時,回收掉上方不在屏幕內的模板,並不銷燬而是將其中的數據清空。當列表下方須要渲染新的數據時,會取出回收的空模板,注入數據渲染出真實節點,而後追加到列表下方。列表向上滾動時的原理是同樣的,爲了保障列表滾動的流暢,也會渲染屏幕上下方擴展區域內的節點。不管真實的數據有多少條,真實渲染的只有可滾動區域內的節點,這樣不只能夠加快首屏的渲染速度,內存的佔用量也不會隨着列表長度大幅增加。
因爲我只是個前端開發,對於客戶端裏的底層細節就不在這裏班門弄斧了,期待客戶端開發者再詳細介紹一下這一部分。
想讓客戶端只根據模板和數據就能渲染出來節點,看起來只有函數式組件才能夠作到,也就是要求組件必須是不含內部狀態的,然而實際應用中絕大多數組件都含有內部狀態的,只作到這一步是遠遠不夠的。
對於包含了狀態的組件,渲染過程就比較複雜了,由於組件內部狀態的處理邏輯(data
,watch
, computed
)都在前端中,然而模板和數據都已經發給客戶端處理了,因此須要通過多個回合的通訊來解決狀態同步問題(詳細處理過程能夠參考 Implementation.md#渲染過程)。
爲了實現可複用的原生組件,在前端框架中引入了 Virtual Component Template 和 Virtual Component 這兩個概念:
在渲染的過程當中,若是發現某個組件用在了 <recycle-list>
裏,就再也不走以前的處理邏輯,而是建立一個 Virtual Component Template,而且不初始化任何狀態(data
,watch
, computed
)、不綁定生命週期,可是會初始化自定義事件的功能。渲染組件時不執行 render
函數,而是執行定製的 @render
函數生成帶有原生渲染指令的模板結構,這個結構將一次性發給客戶端,後續不會再修改。
在建立 Virtual Component Template 時,會監聽客戶端原生組件的 create
生命週期鉤子,當客戶端派發了 create
的時候,纔會真正的開始建立只含狀態不含節點的 Virtual Component。虛擬組件模板只有一份,可是從同一份模板建立出的 Virtual Component 會有多個,與客戶端發送的 create
鉤子的次數有關,與數據有關。另外,因爲事件是綁定在節點上的,原生 UI 捕獲到的事件只會派發給 Virtual Component Template,而後再找到相應的 Virtual Component 並以其爲做用域執行事件處理函數。
Virtual Component 內部只管理數據,即便數據有變更也不會觸發渲染,而是調用特殊接口向客戶端更新組件的內部狀態,由客戶端根據新數據更新組件的 UI。在建立 Virtual Component 時,會監聽客戶端原生組件的 attach
、detach
、 update
、 syncState
生命週期,生命週期的派發有客戶端來控制,語義和前端框架略有差別。
Weex 是個開源項目,是一個社區項目,分享經驗、貢獻代碼、貢獻想法、修訂文檔都算是爲開源項目貢獻力量,我相信有許多開發者都使用過 Weex,也踩過一些坑,積累了實踐經驗,也但願你們能多多分享,一塊兒參與改善 Weex,讓它變得更強大用起來更順手。