去哪兒網迷你React的研發心得

去哪兒網迷你React是年初立項的新做品,在這前,去哪兒網已經深耕多年,擁有QRN(react-native的公司制定版),HY(基於React的hybird方案), yo(基於React的移動UI庫),QRN-web(基於React的三端合一移植方案),此外,像機票等部門也大規模將React用於前臺頁面,後臺頁面就更不在話下。javascript

如此普遍地應用React,咱們熟曉其優缺點。優勢是代碼的可維護性大大提升,性能卓然!但缺點也明顯,因爲體積太大,React.js+React-DOM.js超過3萬行,體量過3MB,已經加上immutable.js , redux, redux-react, react-router等全家桶,工程師一行代碼沒有寫,已經好幾MB了。這在過去絕對不可想象,要知道,體積意味着流量,流量表明着金錢,越大越燒錢,越大下載速度越慢,用戶體驗越差,用戶就會流失。如今問題擺在咱們面前,咱們就得解決。雖然webpack官網有各類瘦身方案,但瘦死的大象也比馬大。這是一個如此廣泛存在的問題,所以外國人確定也遇到過,思考過,提出什麼新點子——全部這一切,都指向一個名詞,「迷你React」。前端

做爲一個生態圈成熟的標誌,一個庫出名了,就各類偏門補丁,閃光效果往上加,庫不免會膨脹,不爽的人就會推出迷你化方案。上一個世代是jQuery與zepto。React的迷你方案也有很多,preact, inferno, react-lite, dio, rax……vue

clipboard.png

但問題不是簡單到直接從github找一個迷你React庫替換上就能搞定。之因此稱爲迷你,確定有所欠缺,若是是內部實現的改良也罷,若是閹割了功能可不是鬧着玩。偏偏他們就好這口,所以現有方案不能知足咱們。咱們須要一個能直接替換,或至少95%的業務代碼不用動。這意味着這迷你框架,須要與React的接口徹底一致,而且全面支持它的全家桶。固然細化一下來的需來就更多了,早期說只要支持移動,所以取名爲react-mobile,後來連iE8也要支持,所以這活不能期望別人,咱們本身動手擼了。java

clipboard.png

咱們先來一趟競品分析。爲了加快產出速度,能借鑑的儘量借鑑。React推出之後,針對其性能研究衍生出很多庫,針對其體積也誕生出大量仿品。它比jQuery更加繽紛多采。市面上居然擁有100多個虛擬DOM庫。虛擬DOM庫,就是React出來後的一種新式庫,以虛擬DOM與diff算法爲核心,屏蔽DOM操做,操做數據即操做視圖。這聽起來有點像MVVM,MVVM也是屏蔽DOM操做,操做數據即操做視圖,但它是以VM爲核心。react

clipboard.png

React及其餘虛擬DOM庫已經將虛擬DOM的生成交由JSX與babel處理了,所以不一樣點是,虛擬DOM的結構與diff算法。虛擬DOM萬宗不離其變是三大屬性,type, props, children,固然也能夠改一下別名,babel能夠作相應配置。此外,虛擬DOM能夠加入更多冗餘標識,以幫diff算法的改良。webpack

React最初推出時也不火,那時的招牌與如今性能不什麼兩樣,也是高性能。可是JSX離經背道,與業界宣揚了多年的先後端分離,數據結構樣式分離等教條差太遠了,一直默默在角落裏畫圈。直到RN出來,解決原生編寫界面的痛苦才一炮而紅。你們才留意它的性能,它的性能背後的diff算法。最先研究React的diff算法是virtual-dom這個庫,是基於經典的DFS算法。後來相應的算法就多起來。最後纔是從接口進行模擬,就是所謂的React-like框架。所以虛擬DOM庫是分爲兩大派系:算法派與擬態派。git

clipboard.png

下面將從這兩大派系進行扼要的描述。github

將前端迴歸到算法上的探索是前端框架史上的一個巨大進步。以前是MVVM將數據從繁複的DOM操做分離出來。正由於有了純數據,而且數據結構可控,那麼算法纔有發揮的餘地。web

最開始出現的是 virtual-dom這個庫,是你們好奇React爲何這麼快而搞鼓出來的。它的實現是很是學院風格,經過深度優先搜索與in-order tree來實現高效的diff。它與React後來公開出來的算法是很不同。算法

而後是cito.js的橫空出世,它對從此全部虛擬DOM的算法都有重大影響。它採用兩端同時進行比較的算法,將diff速度拉高到幾個層次。

緊隨其後的是kivi.js,在cito.js的基出提出兩項優化方案,使用key實現移動追蹤及基於key的編輯長度矩離算法應用(算法複雜度 爲O(n^2))。

但這樣的diff算法太過複雜了,因而後來者snabbdom將kivi.js進行簡化,去掉編輯長度矩離算法,調整兩端比較算法。速度略有損失,但可讀性大大提升。再以後,就是著名的vue2.0 把sanbbdom整個庫整合掉了。

固然算法派的老大是inferno,它使用多種優化方案將性能提至最高,所以其做者便邀請進react core team,負責react的性能優化了。這個我後面會詳細。

clipboard.png

再看擬態派。React的接口並很少,可是其組件的實現是至關有難度。它的生命週期是如何運做,須要對源碼有深入的理解,所以它們出來得比較晚。咱們的學習對象也就是它們幾個。

clipboard.png

先說虛擬DOM。虛擬DOM就是一個普通的JS對象,一般擁有三個屬性,type, props, children。但無狀態組件出來後,children改放到props中。此外,有些元素還有ref屬性,能夠是字符串與函數。在數組裏,爲了提升diff速度,又多出了key屬性。bable會將JSX這些屬性轉換爲一個VNode對象。這是虛擬DOM的最小單元。全部虛擬DOM會組成一棵樹,叫虛擬DOM樹。

爲了防止每次都是整個樹進行diff,須要造成子樹的概念,因而出現組件了。組件有render方法,會返回一個虛擬DOM,這個虛擬DOM及其子孫,就造成一棵子樹。但render方法不是虛擬DOM的東西,因而咱們規定當虛擬DOM爲一個函數時,若是這個函數繼承於React.Component,這個方法的實例必須有render方法。因而咱們就像虛擬DOM與組件統合起來了。或者衍生這兩個稱呼,原子虛擬DOM與組件虛擬DOM。原子虛擬DOM對應元素節點,而組件則是用來產出原子虛擬DOM。此外,原子虛擬DOM還能包含一些東西,字符串與數字與null。字符串與數字對應文本節點,null對應註釋節點。

clipboard.png

一個組件虛擬DOM實例化爲組件後,會返回原子虛擬DOM或另外一個組件虛擬DOM。這就造成函數式編程上的高階函數的機制,所以進行出無狀態函數組件,就是虛擬DOM的type屬性就是一個函數,不繼承其餘東西了。

組件虛擬DOM的實例化過程是很是複雜,若是能簡化這過程,簡化其結構,這性能就上去了。

clipboard.png

此外組件的實例自己就巨耗性能,所以官方推薦頁面的結構以下,經過最少許的有狀態組件(smart component)控制無狀態組件(dumb component)的變化,全部狀態經過redux在路由進行分發。

clipboard.png

通過一番比較後,咱們着手開發本身的迷你React。這個過程也比較坎坷,這仍是有前人蔘照物的狀況下,可想而知,當初facebook開發出React這樣一個獨行特立的框架時,是多麼艱辛。咱們在這半年總共搞了三個東西,第一還孵化失敗。

clipboard.png

最初是基於react-lite,考慮時當時是咱們母公司的人搞的,方便交流。但後來發現它的事件系統太雞肋,難以擴展,最後放棄了。

第二代是基於preact,代號qreact, 國內也有許多公司基於它作本身的迷你化方案,由於官方提供了一個preact-compat的模塊。可是preact-compat是使用Object.defineProperty來實現一些屬性名映射與同步的,所以不支持IE8,而且使用了Object.defineProperty會嚴重拖慢速度。preact自己也有很多BUG,最著名的有三個,生命週期的unmount鉤子不能保證在mount以前執行,元素節點的重複利用沒有清理樣式會致使出錯,同一組孩子下可能存在同名的key致使排序失敗。這些咱們都爲官方提issue,而且在咱們的版本中進行修復了。第二代也公司內部幾個項目中試水落地,反映不錯。

第三代是咱們團隊獨力開發,a內部代號anu,正式名稱仍然是 QReact,不過演進到了 QReact 1.0 了 (如今的版本使用了 1.0 大版本,由於以前這個版本沒被佔用,因此沒跳到 2.0)。在對preact縫縫補補的過程,掌握很多核心知識,新的框架是使用全新的算法,全新的結構。因爲不使用高級API,理論上能支持到IE6,但咱們公司只需支持到IE8。

Qreact1.0使用requestAnimationFrame來穩定它的運行幀數,保證在60幀每秒的流暢速度。因爲bable會對type進行打補丁,內部統一用typeNumber代替type進行類型斷定。使用列隊保證生命週期鉤子按順序執行。使用__rerender標識一個組件在一次大的更新只會被render一次。凡此種種,通過大量測試,它們的接口與React別無二致,甚至React廢棄的接口createClass, PropTypes,咱們都有相應的polyfill。

下面是Qreact1.0的測試數據。
從性能上,直追preact。

clipboard.png

從體積上,是官方react的1/4至1/5之間。

clipboard.png

這裏透露一下React性能爆表的祕密,除了diff算法外,setState的合併操做也是一個關鍵。當組件沒有插入到DOM樹前,用戶在componentWillMount方法屢次執行setState,這些state是不會觸發更新,而是存放到一個數組中,而後在render方法裏進行合併與應用。當一個組件進行更新,多是用戶在componentDidMount或者事件回調執行setState,這時更新是即時的,同步的,但這之次再setState,它就不會更新了,它的state會進入列隊。此外,若是用戶在componentWillReceiveProps屢次執行setState,也會產生延遲。React這種行爲是保證頁面的更新次數最少,同時用戶不會察覺它沒有更新。它只是將state進行了合併。Qreact1.0是徹底遵循了這些規則,從而實現了高性能。

而在體積上,則經過刪除一些對線上沒用的代碼實現迷你化效果。

clipboard.png

但僅是這樣不足以大吹特吹了。爲了超越React的性能,從inferno與preact借簽了很多手段。

clipboard.png

最值得一提的是hydrate機制,經過合併相鄰字符串,從而減小文本節點的生成,從而減小diff次數。

clipboard.png

最後還有一個壓軸大戲,不作測試不知道,原來高級API是如何耗性能。經過去掉Object.freeze, Object.defineProperty這些es5方法, 框架就有10幀的提高!

clipboard.png

說了這麼多,來些實際可運行的例子吧。目前,Qreact跑通內部幾套測試,已經在金融與大搜的項目使用。它的第二版也在機票一個擁有820個模塊的大項目中試水,在IE8下良好運行。

clipboard.png

若是你們想在項目中使用qreact,能夠在webpack或ykit的config中以下配置:

resolve: {
   alias: {
      'react': 'anujs',
      'react-dom': 'anujs',
        // 若要兼容 IE 請使用如下配置
        // 'react': 'anujs/dist/ReactIE',
        // 'react-dom': 'anujs/dist/ReactIE',
    
        // 若是引用了 prop-types 或 create-react-class
        // 須要添加以下別名
        'prop-types': 'anujs/lib/ReactPropTypes',
        'create-react-class': 'anujs/lib/createClass'
        //若是你在移動端用到了onTouchTap事件
        'react-tap-event-plugin': 'anujs/lib/injectTapEventPlugin',  
   }
},

若是你們對qreact在感興趣,歡迎與我聯繫,也歡迎你們爲個人框架加星。

https://github.com/RubyLouvre/anu/
https://github.com/YMFE/qreact

相關文章
相關標籤/搜索