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

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

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

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

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

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

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

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

  • 基于 Javascript 進(jìn)行開發(fā),避免了長時間的代碼編譯過程
  • 實現(xiàn)了一個名為 Packager 的工具,用來將 es6/flow/jsx 文件轉(zhuǎn)換成虛擬機(jī)可以理解的普通 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ù)之前的多次點擊才能再次進(jìn)入這個三級頁面,而這將花費好幾秒的時間。

熱加載

熱加載的思想是運行時動態(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 中開始引入,搖動手機(jī)打開 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)用將每個模塊的代碼包裹進(jìn)一個匿名函數(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 進(jìn)程不能簡單的替換完 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ù),強(qiáng)制 React 重新渲染這個 Component。這樣,我們實現(xiàn)了熱加載渲染的代碼且不丟失應(yīng)用的狀態(tài)。

React Native 默認(rèn)使用的轉(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]這篇文章并作出自己的貢獻(xiàn)。例如,Johny Days 正在嘗試使熱加載支持多個 HMR 客戶端[10],我們有賴于你來維護(hù)和改進(jìn)這個特性。

React Native 讓我們有機(jī)會重新思考在構(gòu)建 APP 時如何提供更好的開發(fā)體驗,熱加載只是冰山一角,我們還有哪些其他的 hacks 可以更進(jìn)一步提高開發(fā)體驗?zāi)??有待你來發(fā)掘和貢獻(xiàn)!

歡迎關(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容