一.HMR
HMR(Hot Module Replacement)は実行時の JavaScript モジュールに対してホットアップデートを実行できます(再読み込み不要で、モジュールの置換、追加、削除が可能)
(webpack HMR から引用)
HMR 特性は webpack などの構築ツールによって提供され、アプリケーション層フレームワーク(React、Vue など)が对接するための一連の ランタイム API を公開します:
Basically it's just a way for modules to say "When a new version of some module I import is available, run a callback in my app so I can do something with it".
その基本原理は、実行時に(構築ツールが起動した)Dev Server に対してポーリングを実行し、script タグを通じて更新されたモジュールを実行環境に注入し、関連するコールバック関数を実行することです:
HMR is just a fancy way to poll the development server, inject <script> tags with the updated modules, and run a callback in your existing code.
例えば:
import printMe from './print.js';
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}
HMR を有効にすると、./print.js モジュールに更新がある場合、コールバック関数がトリガーされ、モジュールが置換完了したことを示します。その後該モジュールにアクセスすると、すべて新しいモジュールインスタンスが取得されます
実行時のモジュール置換能力(HMR)に基づき、アプリケーション層フレームワーク(React、Vue、さらには Express)と結合して、Live Reloading、Hot Reloading などのより効率的な開発モードをさらに実現できます
二.Live Reloading
いわゆる Live Reloading とは、モジュールファイルが変化した時に、アプリケーション全体を再読み込みすることです:
Live reloading reloads or refreshes the entire app when a file changes. For example, if you were four links deep into your navigation and saved a change, live reloading would restart the app and load the app back to the initial route.
React を例に:
const App = require('./App')
const React = require('react')
const ReactDOM = require('react-dom')
// Render the root component normally
const rootEl = document.getElementById('root')
ReactDOM.render(<App />, rootEl)
// Are we in development mode?
if (module.hot) {
// Whenever a new version of App.js is available
module.hot.accept('./App', function () {
// Require the new version and render it instead
const NextApp = require('./App')
ReactDOM.render(<NextApp />, rootEl)
})
}
HMR を通じてルートコンポーネントを置き換え、再レンダリングするだけ です。HMR モジュール更新には バブリングメカニズム があるため、accept で処理されない更新イベントは依存チェーンを逆向きに伝達します。したがってコンポーネントツリーのトップ層でツリー中のすべてのコンポーネントの変化を監視でき、この時にコンポーネントツリー全体を再作成し、プロセス中に取得するのはすべて更新完了したコンポーネントであり、レンダリングすれば新しいビューが得られます
この方案はアプリケーション層フレームワークへの依存が少なく(re-render 部分のみ)、実装が簡単で安定信頼できますが、此前的な実行状態はすべて失われ、SPA などの実行時状態が多く複雑なシーンに対して極めて不親切 です。リフレッシュ後に再度操作し直さなければ以前のビューステートに戻れず、開発効率の向上は非常に限られています
では、実行時の状態データを保持し、変化したビューのみをリフレッシュする方法はあるでしょうか?
あります。Hot Reloading です
三.Hot Reloading
下層も同様に HMR に基づきますが、Hot Reloading はアプリケーションの実行状態を保持でき、変化した部分に対してのみ部分的なリフレッシュを実行します:
Hot reloading only refreshes the files that were changed without losing the state of the app. For example, if you were four links deep into your navigation and saved a change to some styling, the state would not change, but the new styles would appear on the page without having to navigate back to the page you are on because you would still be on the same page.
ビューの部分的なリフレッシュは、全体リフレッシュ後に再度以前の状態に戻るための煩雑な操作を免除し、真に開発効率を向上させます
しかし、部分的なリフレッシュはコンポーネント(さらにはコンポーネントの一部)のホット置換を要求し、これは実装上に不小的な挑戦が存在します(正確性の保障、影響範囲の縮小、エラーのフィードバックなどを含む。詳細は My Wishlist for Hot Reloading を参照)
どうやってコンポーネントを動的に置換するか?
HMR 置換後の新しいモジュールは、実行時から見ると完全に異なる 2 つのコンポーネントであり、以下に相当します:
function getMyComponent() {
// script タグを通じて、同じコンポーネントコードを再ロード
class MyComponent {}
return MyComponent;
}
getMyComponent() === getMyComponent() // false
明らかに React 自身の Diff メカニズム を通じて無傷置換を完了することはできません。したがって、JavaScript 言語から可能性を探すしかありません
クラシックな React コンポーネントは [ES6 Class](/articles/class-es6 笔记 10/) を通じて定義されます:
class Foo extends Component {
state = {
clicked: false
}
handleClick = () => {
console.log('Click happened');
this.setState({ clicked: true });
}
render() {
return <button onClick={this.handleClick}>{!this.state.clicked ? 'Click Me' : 'Clicked'}</button>;
}
}
実行時にコンポーネントクラスに基づいて一連のコンポーネントインスタンスを作成し、它们は render ライフサイクルなどのプロトタイプメソッドを持ち、handleClick などのインスタンスメソッド、および state などのインスタンス属性を持ちます
プロトタイプメソッド、プロトタイプ属性は置換が難しくありませんが、インスタンスメソッドとインスタンス属性を置き換えるのはそれほど簡単ではありません。它们はコンポーネントツリー中にしっかりと包まれているためです
そのため、ある人は非常に賢い方法を思いつきました
四.React Hot Loader
React エコシステムにおいて、現在(2020/5/31)最も広く応用されている Hot Reloading 方案は依然として RHL(React Hot Loader) です:
Tweak React components in real time.
コンポーネントメソッドの動的置換を実現するために、RHL は React コンポーネントの上に 1 層のプロキシを追加 しました:
Proxies React components without unmounting or losing their state.
P.S.詳細は react-proxy を参照
重要原理

1 層のプロキシを通じてコンポーネント状態を剥離し、プロキシコンポーネント中に配置して維持します(其余のライフサイクルメソッドなどはすべてソースコンポーネントにプロキシ)。したがってソースコンポーネントを置き換えた後でもコンポーネント状態を保持できます:
The proxies hold the component's state and delegate the lifecycle methods to the actual components, which are the ones we hot reload.
ソースコンポーネントはプロキシコンポーネントに包まれ、コンポーネントツリーに掛かっているのはすべてプロキシコンポーネントです。ホット更新前後でコンポーネントタイプは変化せず(背後のソースコンポーネントはすでにこっそりと新しいコンポーネントインスタンスに置き換えられています)、したがって追加のライフサイクル(componentDidMount など)をトリガーしません:
Proxy component types so that the types that React sees stay the same, but the actual implementations change to refer to the new underlying component type on every hot update.
具体的な実装詳細は、以下を参照:
-
プロキシコンポーネント:react-hot-loader/src/proxy/createClassProxy.js
-
コンポーネント更新戦略:Not all methods could|should be updated
-
オンライン Demo:http://gaearon.github.io/react-hot-loader/
Redux Store
特別に、Redux アプリケーション而言、Reducer の変化もホット有効にすることが必要です(大多数の状態は Redux によって管理されているため):
// 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;
};
replaceReducer を通じて Reducer を置き換え、同時に store 状態を保持します
P.S.Redux アプリケーション Hot Reloading に関する詳細情報は、RFC: remove React Transform from examples を参照
参考資料
-
[Hot Reloading in React](https://medium.com/ @dan_abramov/hot-reloading-in-react-1140438583bf-.jmivpvmz4)
-
What is the difference between Hot Reloading and Live Reloading in React Native?
コメントはまだありません