
HMR 更新消息中除了包含发生改动的代码之外,还需要包含其他一些信息,因为如果只有发生改动的代码,HMR 运行时不足以实现代码替换。原因在于模块系统可能已经缓存了我们想要替换的模块的 exports
,比如你的应用有如下两个模块,其中 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;
当应用打包(bundled)后,React Native 会使用 __d
函数将所有的模块注册到模块系统中。在我们这个例子 APP 中,可以看到下面所示 log
模块的 __d
定义:
__d('log', function() {
... // module's code
});
这个函数调用将每个模块的代码包裹进一个匿名函数中,我们通常称之为工厂函数。模块系统运行时会跟踪每个模块的工厂函数,看它是否已经被执行,以及执行的结果(exports)。当一个模块被 required
之后,模块系统会判断当前模块的工厂函数是否已经执行过,如果是则返回缓存的 exports,否则调用工厂函数并保存结果到缓存中。
因此,当你启动应用并 require log
模块时,这时由于 log
和 time
这两个模块的工厂函数都还没有执行过,因此不存在 exports 缓存。接着用户修改 time
模块添加返回 MM/DD
形式的日期,代码如下:
// time.js
function bar() {
var date = new Date();
return `${date.getMonth() + 1}/${date.getDate()}`;
}
module.exports = bar;
- 步骤一:Packager 会将
time
模块的新代码发送给 HMR 运行时 - 步骤二:当
log
模块最终被required
且 exported 函数被执行到时,它会随着time
模块的变化而变化
整个过程如下图所示:

让我们假设 log
模块以最顶层的方式 require time
模块:
const time = require('./time'); // top level require
// log.js
function log(message) {
console.log(`[${time()}] ${message}`);
}
module.exports = log;
- 步骤一:当
log
被required
时,HMR 运行时会缓存它和time
的 exports - 步骤二:接着当
time
被修改后,HMR 进程不能简单的替换完time
的代码后就结束运行,否则当log
被执行时,它会使用到time
的缓存,也就是旧代码 - 步骤三:为了实现
log
可以得到time
的最新修改,我们需要清空缓存的 exports,因为log
所依赖的模块有至少一个发生了改变 - 步骤四:最后,当
log
被再次required
,它的工厂函数会被执行并 requiretime
模块从而得到最新的代码。
整个过程如下图所示:

HMR API
React Native 中的 HMR 通过引入 hot
对象实现对模块系统的继承,这个 API 基于 Webpack 的基础上。hot
对象对外暴露了一个名为 accept
的函数,它使得开发者可以定义一个回调函数,当模块需要热交换(hot swapped)时会执行到。例如,我们如下所示修改 time
的代码,每次我们保存 time
模块时,可以在控制台看到 time changed
这句日志:
// time.js
function time() {
... // new code
}
module.hot.accept(() => {
console.log('time changed');
});
module.exports = time;
需要注意的是,只有在很少数情况下你才需要手动调用这个 API,热加载在大多数情况下已经帮我们实现了。
HMR Runtime
如前所见,有时仅仅 accept HMR 更新是不够的,因为模块 A 如果依赖一个经过热交换的模块 B,且此时模块 A 可能已经执行过且缓存了所有的 imports。例如,假设一个 movies 应用的依赖树有一个最顶层的 MovieRouter
模块,它依赖于 MovieSearch
和 MovieScreen
两个页面,而这两个页面又依赖于前面介绍过的 log
和 time
模块:

当用户访问了 MovieSearch
页面而还没有访问 MovieScreen
页面,此时除了 MovieScreen
模块之外,其他模块的 exports
都被缓存了。这时 time
模块代码发生了改动,HMR 运行时将会清空 log
模块的 exports
缓存,并加载 time
的改动。接着运行时会向上递归直到所有的父模块被 accepted
。也就是运行时会获取所有依赖于 log
的模块并尝试 accept 它们。当尝试 accept MovieScreen
模块时会失败,因为这个模块还没有被 required
;当尝试 accept MovieSearch
模块时,运行时将会清空它缓存的 exports
并继续递归执行它的父模块,最后执行到最顶层的 MovieRouter
模块时结束。
为了遍历上面的依赖树,运行时在 HMR 更新时从 Packager 获取反转后的依赖树信息,在上面这个例子中,获取到的反转依赖树如下,是一个 JSON 对象:
{
modules: [
{
name: 'time',
code: /* time's new code */
}
],
inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}
React Components
想要实现 React Components 的热加载并不是一件容易的事情,因为我们不能简单的使用新的 Component 替换旧的,这样会丢失它的状态。对于 React 的 Web 应用,Dan Abramov[4] 实现了一个名为 React Hot Loader[5] 的 babel 转换器,它使用 Webpack 的 HMR API 来解决这个问题。简而言之,他的解决方案是在转换阶段为每个 React Component 创建一个代理,这些代理保存了 Component 的状态,并将生命周期函数委托给实际的 Components,也就是我们执行热加载的 Components。

除了创建代理 Component,React Hot Loader 转换器还通过一段代码定义了 accept
函数,强制 React 重新渲染这个 Component。这样,我们实现了热加载渲染的代码且不丢失应用的状态。
React Native 默认使用的转换器[6]使用 babel-preset-react-native
,它跟前面介绍的 React Web 应用一样的方式使用[7] react-transform
。
Redux Stores
想要在 Redux[8] stores 中开启热加载,只需像前面介绍的基于 Webpack 的 Web 应用中那样使用 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;
};
当我们改变了一个 reducer,客户端会接收到 accept 这个 reducer 的代码,这时,客户端将会发现 reducer 不知道如何 accept 自身。因此它将会查询依赖它的所有模块并尝试 accept 他们。最终,数据会流向单一的 store:configureStore
,由它来 accept HMR 的更新。
总结
如果你对于改善热加载感兴趣的话,我建议你阅读 Dan Abramov 关于热加载的未来[9]这篇文章并作出自己的贡献。例如,Johny Days 正在尝试使热加载支持多个 HMR 客户端[10],我们有赖于你来维护和改进这个特性。
React Native 让我们有机会重新思考在构建 APP 时如何提供更好的开发体验,热加载只是冰山一角,我们还有哪些其他的 hacks 可以更进一步提高开发体验呢?有待你来发掘和贡献!
欢迎关注我的微信公众号,专注与原创或者分享 Android,iOS,ReactNative,Web 前端移动开发领域高质量文章,主要包括业界最新动态,前沿技术趋势,开源函数库与工具等。

-
https://webpack.github.io/docs/hot-module-replacement-with-webpack.html ↩
-
https://github.com/facebook/react-native/blob/master/packager/transformer.js#L92-L95 ↩
-
https://github.com/facebook/react-native/blob/master/babel-preset/configs/hmr.js#L24-L31 ↩
-
https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf#.jmivpvmz4 ↩
网友评论