React Native 熱加載(Hot Reload)原理簡介

@author ASCE1885的 Github 簡書 微博 CSDN 知乎

未標(biāo)題-1.png-1123.9kB
未標(biāo)題-1.png-1123.9kB

廣而告之時間:我的新書《Android 高級進階》(https://item.jd.com/10821975932.html在京東開始預(yù)售了,歡迎訂購疏叨!

TB2MnqlXH1J.eBjSszcXXbFzVXa_!!1020536390.png-39kB
TB2MnqlXH1J.eBjSszcXXbFzVXa_!!1020536390.png-39kB

最近發(fā)現(xiàn) React Native 官方博客上面這篇介紹 Hot Reload 原理的文章贱除,仔細閱讀了一下,順便翻譯為中文,以饗讀者该编。本文不少內(nèi)容加入了譯者的理解决瞳,并沒有嚴格字對字翻譯,英文水平不錯的同學(xué)可以直接閱讀原文[1]

React Native 的設(shè)計目標(biāo)是為開發(fā)者提供最好的開發(fā)體驗区拳,其中很重要的一點就是盡量縮短文件修改后到看到修改所產(chǎn)生的變化之間所需的時間。我們的目標(biāo)是將這個循環(huán)所需的時間降到 1 秒以下意乓,即使你應(yīng)用的功能和體積在不斷的膨脹樱调。

通過下面三個主要特性我們離目標(biāo)越來越近:

  • 基于 Javascript 進行開發(fā),避免了長時間的代碼編譯過程
  • 實現(xiàn)了一個名為 Packager 的工具届良,用來將 es6/flow/jsx 文件轉(zhuǎn)換成虛擬機可以理解的普通 JavaScript 語言笆凌。Packager 被設(shè)計為一個服務(wù)器,從而能夠在內(nèi)存中保存當(dāng)下的狀態(tài)士葫,實現(xiàn)快速的增量更新菩颖,同時可以使用系統(tǒng)的多核 CPU 提高性能。
  • 新增了一個名為實時加載(Live Reload)的特性为障,實現(xiàn)保存代碼修改后自動重新加載 APP

到這一步晦闰,對開發(fā)者而言瓶頸已然不是重新加載 APP 所需花費的時間,而是如何保持 APP 的狀態(tài)鳍怨。一個典型的場景是如果當(dāng)前頁面是一個三級頁面呻右,那么每次重新加載后,我們都要通過重復(fù)之前的多次點擊才能再次進入這個三級頁面鞋喇,而這將花費好幾秒的時間声滥。

熱加載

熱加載的思想是運行時動態(tài)注入修改后的文件內(nèi)容,同時不中斷 APP 的正常運行侦香。這樣落塑,我們就不會丟失 APP 的任何狀態(tài)信息,尤其是 UI 頁面棧相關(guān)的罐韩。

關(guān)于實時加載(Live Reload)和熱加載(Hot Reload)的區(qū)別憾赁,可以參見YouTube上面這個視頻[2],其中關(guān)鍵的區(qū)別在于實時加載應(yīng)用更新時需要刷新當(dāng)前頁面散吵,可以看到明顯的全局刷新效果龙考,而熱加載基本上看不出刷新的效果蟆肆,類似于局部刷新。

熱加載在 React Native 0.22 中開始引入晦款,搖動手機打開 RN 的開發(fā)者菜單炎功,點擊 Enable Hot Reloading 即可開啟。

核心實現(xiàn)原理

熱加載的基礎(chǔ)是模塊熱替換(HMR缓溅,Hot Module Replacement[3])蛇损,HMR 最開始是由 Webpack 引入的,我們在 React Native Packager 中也實現(xiàn)了這個功能坛怪。HMR 使得 Packager 可以監(jiān)控文件的改動并發(fā)送 HMR 更新消息(HMR update)給包含在 APP 中的一層很薄的 HMR 運行時(HMR runtime)淤齐。

簡而言之,HMR 更新消息包含 JS 模塊中發(fā)生變化的代碼酝陈,當(dāng) HMR 運行時接收到這個消息,就使用新的代碼替換舊的代碼毁涉,流程如下圖所示:

HMR 更新消息中除了包含發(fā)生改動的代碼之外沉帮,還需要包含其他一些信息,因為如果只有發(fā)生改動的代碼贫堰,HMR 運行時不足以實現(xiàn)代碼替換穆壕。原因在于模塊系統(tǒng)可能已經(jīng)緩存了我們想要替換的模塊的 exports,比如你的應(yīng)用有如下兩個模塊其屏,其中 log 模塊的功能是打印 time 模塊提供的日期信息喇勋,代碼如下所示:

// log.js
function log(message) {
  const time = require('./time');
  console.log(`[${time()}] ${message}`);
}

module.exports = log;
// time.js
function time() {
  return new Date().getTime();
}

module.exports = time;

當(dāng)應(yīng)用打包(bundled)后,React Native 會使用 __d 函數(shù)將所有的模塊注冊到模塊系統(tǒng)中偎行。在我們這個例子 APP 中川背,可以看到下面所示 log 模塊的 __d 定義:

__d('log', function() {
  ... // module's code
});

這個函數(shù)調(diào)用將每個模塊的代碼包裹進一個匿名函數(shù)中,我們通常稱之為工廠函數(shù)蛤袒。模塊系統(tǒng)運行時會跟蹤每個模塊的工廠函數(shù)熄云,看它是否已經(jīng)被執(zhí)行,以及執(zhí)行的結(jié)果(exports)妙真。當(dāng)一個模塊被 required 之后缴允,模塊系統(tǒng)會判斷當(dāng)前模塊的工廠函數(shù)是否已經(jīng)執(zhí)行過,如果是則返回緩存的 exports珍德,否則調(diào)用工廠函數(shù)并保存結(jié)果到緩存中练般。

因此,當(dāng)你啟動應(yīng)用并 require log 模塊時锈候,這時由于 logtime 這兩個模塊的工廠函數(shù)都還沒有執(zhí)行過薄料,因此不存在 exports 緩存。接著用戶修改 time 模塊添加返回 MM/DD 形式的日期泵琳,代碼如下:

// time.js
function bar() {
  var date = new Date();
  return `${date.getMonth() + 1}/${date.getDate()}`;
}

module.exports = bar;
  • 步驟一:Packager 會將 time 模塊的新代碼發(fā)送給 HMR 運行時
  • 步驟二:當(dāng) log 模塊最終被 required 且 exported 函數(shù)被執(zhí)行到時都办,它會隨著 time 模塊的變化而變化

整個過程如下圖所示:

讓我們假設(shè) log 模塊以最頂層的方式 require time 模塊:

const time = require('./time'); // top level require

// log.js
function log(message) {
  console.log(`[${time()}] ${message}`);
}

module.exports = log;
  • 步驟一:當(dāng) logrequired 時嫡锌,HMR 運行時會緩存它和 time 的 exports
  • 步驟二:接著當(dāng) time 被修改后,HMR 進程不能簡單的替換完 time 的代碼后就結(jié)束運行琳钉,否則當(dāng) log 被執(zhí)行時势木,它會使用到 time 的緩存,也就是舊代碼
  • 步驟三:為了實現(xiàn) log 可以得到 time 的最新修改歌懒,我們需要清空緩存的 exports啦桌,因為 log 所依賴的模塊有至少一個發(fā)生了改變
  • 步驟四:最后,當(dāng) log 被再次 required及皂,它的工廠函數(shù)會被執(zhí)行并 require time 模塊從而得到最新的代碼甫男。

整個過程如下圖所示:

HMR API

React Native 中的 HMR 通過引入 hot 對象實現(xiàn)對模塊系統(tǒng)的繼承,這個 API 基于 Webpack 的基礎(chǔ)上验烧。hot 對象對外暴露了一個名為 accept 的函數(shù)板驳,它使得開發(fā)者可以定義一個回調(diào)函數(shù),當(dāng)模塊需要熱交換(hot swapped)時會執(zhí)行到碍拆。例如若治,我們?nèi)缦滤拘薷?time 的代碼,每次我們保存 time 模塊時感混,可以在控制臺看到 time changed 這句日志:

// time.js
function time() {
  ... // new code
}

module.hot.accept(() => {
  console.log('time changed');
});

module.exports = time;

需要注意的是端幼,只有在很少數(shù)情況下你才需要手動調(diào)用這個 API,熱加載在大多數(shù)情況下已經(jīng)幫我們實現(xiàn)了弧满。

HMR Runtime

如前所見婆跑,有時僅僅 accept HMR 更新是不夠的,因為模塊 A 如果依賴一個經(jīng)過熱交換的模塊 B庭呜,且此時模塊 A 可能已經(jīng)執(zhí)行過且緩存了所有的 imports滑进。例如,假設(shè)一個 movies 應(yīng)用的依賴樹有一個最頂層的 MovieRouter 模塊募谎,它依賴于 MovieSearchMovieScreen 兩個頁面郊供,而這兩個頁面又依賴于前面介紹過的 logtime 模塊:

當(dāng)用戶訪問了 MovieSearch 頁面而還沒有訪問 MovieScreen 頁面,此時除了 MovieScreen 模塊之外近哟,其他模塊的 exports 都被緩存了驮审。這時 time 模塊代碼發(fā)生了改動,HMR 運行時將會清空 log 模塊的 exports 緩存吉执,并加載 time 的改動疯淫。接著運行時會向上遞歸直到所有的父模塊被 accepted。也就是運行時會獲取所有依賴于 log 的模塊并嘗試 accept 它們戳玫。當(dāng)嘗試 accept MovieScreen 模塊時會失敗熙掺,因為這個模塊還沒有被 required;當(dāng)嘗試 accept MovieSearch 模塊時咕宿,運行時將會清空它緩存的 exports 并繼續(xù)遞歸執(zhí)行它的父模塊币绩,最后執(zhí)行到最頂層的 MovieRouter 模塊時結(jié)束蜡秽。

為了遍歷上面的依賴樹,運行時在 HMR 更新時從 Packager 獲取反轉(zhuǎn)后的依賴樹信息缆镣,在上面這個例子中芽突,獲取到的反轉(zhuǎn)依賴樹如下,是一個 JSON 對象:

{
  modules: [
    {
      name: 'time',
      code: /* time's new code */
    }
  ],
  inverseDependencies: {
    MovieRouter: [],
    MovieScreen: ['MovieRouter'],
    MovieSearch: ['MovieRouter'],
    log: ['MovieScreen', 'MovieSearch'],
    time: ['log'],
  }
}

React Components

想要實現(xiàn) React Components 的熱加載并不是一件容易的事情董瞻,因為我們不能簡單的使用新的 Component 替換舊的寞蚌,這樣會丟失它的狀態(tài)。對于 React 的 Web 應(yīng)用钠糊,Dan Abramov[4] 實現(xiàn)了一個名為 React Hot Loader[5] 的 babel 轉(zhuǎn)換器挟秤,它使用 Webpack 的 HMR API 來解決這個問題。簡而言之抄伍,他的解決方案是在轉(zhuǎn)換階段為每個 React Component 創(chuàng)建一個代理艘刚,這些代理保存了 Component 的狀態(tài),并將生命周期函數(shù)委托給實際的 Components截珍,也就是我們執(zhí)行熱加載的 Components攀甚。

除了創(chuàng)建代理 Component,React Hot Loader 轉(zhuǎn)換器還通過一段代碼定義了 accept 函數(shù)笛臣,強制 React 重新渲染這個 Component云稚。這樣隧饼,我們實現(xiàn)了熱加載渲染的代碼且不丟失應(yīng)用的狀態(tài)沈堡。

React Native 默認使用的轉(zhuǎn)換器[6]使用 babel-preset-react-native,它跟前面介紹的 React Web 應(yīng)用一樣的方式使用[7] react-transform燕雁。

Redux Stores

想要在 Redux[8] stores 中開啟熱加載诞丽,只需像前面介紹的基于 Webpack 的 Web 應(yīng)用中那樣使用 HMR API 即可,如下所示:

// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../reducers';

export default function configureStore(initialState) {
  const store = createStore(
    reducer,
    initialState,
    applyMiddleware(thunk),
  );

  if (module.hot) {
    module.hot.accept(() => {
      const nextRootReducer = require('../reducers/index').default;
      store.replaceReducer(nextRootReducer);
    });
  }

  return store;
};

當(dāng)我們改變了一個 reducer拐格,客戶端會接收到 accept 這個 reducer 的代碼僧免,這時,客戶端將會發(fā)現(xiàn) reducer 不知道如何 accept 自身捏浊。因此它將會查詢依賴它的所有模塊并嘗試 accept 他們懂衩。最終,數(shù)據(jù)會流向單一的 store:configureStore金踪,由它來 accept HMR 的更新浊洞。

總結(jié)

如果你對于改善熱加載感興趣的話,我建議你閱讀 Dan Abramov 關(guān)于熱加載的未來[9]這篇文章并作出自己的貢獻胡岔。例如法希,Johny Days 正在嘗試使熱加載支持多個 HMR 客戶端[10],我們有賴于你來維護和改進這個特性靶瘸。

React Native 讓我們有機會重新思考在構(gòu)建 APP 時如何提供更好的開發(fā)體驗苫亦,熱加載只是冰山一角毛肋,我們還有哪些其他的 hacks 可以更進一步提高開發(fā)體驗?zāi)兀坑写銇戆l(fā)掘和貢獻屋剑!

歡迎關(guān)注我的微信公眾號润匙,專注與原創(chuàng)或者分享 Android,iOS饼丘,ReactNative趁桃,Web 前端移動開發(fā)領(lǐng)域高質(zhì)量文章,主要包括業(yè)界最新動態(tài)肄鸽,前沿技術(shù)趨勢卫病,開源函數(shù)庫與工具等。


  1. http://facebook.github.io/react-native/blog/ ?

  2. https://youtu.be/2uQzVi-KFuc ?

  3. https://webpack.github.io/docs/hot-module-replacement-with-webpack.html ?

  4. https://twitter.com/dan_abramov ?

  5. http://gaearon.github.io/react-hot-loader/ ?

  6. https://github.com/facebook/react-native/blob/master/packager/transformer.js#L92-L95 ?

  7. https://github.com/facebook/react-native/blob/master/babel-preset/configs/hmr.js#L24-L31 ?

  8. http://redux.js.org/ ?

  9. https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf#.jmivpvmz4 ?

  10. https://github.com/facebook/react-native/pull/6179 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末典徘,一起剝皮案震驚了整個濱河市蟀苛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逮诲,老刑警劉巖帜平,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異梅鹦,居然都是意外死亡裆甩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門齐唆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗤栓,“玉大人,你說我怎么就攤上這事箍邮≤运В” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵锭弊,是天一觀的道長堪澎。 經(jīng)常有香客問我,道長味滞,這世上最難降的妖魔是什么樱蛤? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮剑鞍,結(jié)果婚禮上昨凡,老公的妹妹穿的比我還像新娘。我一直安慰自己攒暇,他們只是感情好土匀,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著形用,像睡著了一般就轧。 火紅的嫁衣襯著肌膚如雪证杭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天妒御,我揣著相機與錄音解愤,去河邊找鬼。 笑死乎莉,一個胖子當(dāng)著我的面吹牛送讲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播惋啃,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼哼鬓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了边灭?” 一聲冷哼從身側(cè)響起异希,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绒瘦,沒想到半個月后称簿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡惰帽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年憨降,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片该酗。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡授药,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出垂涯,到底是詐尸還是另有隱情烁焙,我是刑警寧澤航邢,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布耕赘,位于F島的核電站,受9級特大地震影響膳殷,放射性物質(zhì)發(fā)生泄漏操骡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一赚窃、第九天 我趴在偏房一處隱蔽的房頂上張望册招。 院中可真熱鬧,春花似錦勒极、人聲如沸是掰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽键痛。三九已至炫彩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間絮短,已是汗流浹背江兢。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丁频,地道東北人杉允。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像席里,于是被迫代替她去往敵國和親叔磷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容