Introducing Hot Reloading
React Native 的目标是为您提供尽可能最佳的开发者体验。其中很大一部分是您保存文件到能够看到更改之间所花费的时间。我们的目标是使这个反馈循环保持在 1 秒以内,即使您的应用不断增长。
我们通过三个主要功能接近了这个理想目标
- 使用 JavaScript 作为语言,因为它没有很长的编译周期时间。
- 实现了一个名为 Packager 的工具,该工具将 es6/flow/jsx 文件转换为 VM 可以理解的普通 JavaScript。它被设计为一个服务器,将中间状态保存在内存中,以实现快速的增量更改,并使用多核。
- 构建了一个名为 Live Reload 的功能,该功能在保存时重新加载应用程序。
此时,开发人员的瓶颈不再是重新加载应用程序所需的时间,而是丢失应用程序的状态。一个常见的场景是开发一个离启动屏幕有多个屏幕距离的功能。每次重新加载时,您都必须一次又一次地单击相同的路径才能返回到您的功能,这使得周期长达数秒。
Hot Reloading
热重载背后的想法是保持应用程序运行,并在运行时注入您编辑的文件的新版本。这样,您就不会丢失任何状态,如果您正在调整 UI,这将特别有用。
一个视频胜过千言万语。查看 Live Reload(当前)和 Hot Reload(新)之间的区别。
如果您仔细观察,您会注意到可以从红色框中恢复,并且您还可以开始导入以前不存在的模块,而无需进行完全重新加载。
警告: 因为 JavaScript 是一种非常有状态的语言,所以热重载无法完美实现。在实践中,我们发现当前的设置在大量常见用例中运行良好,并且在出现问题时始终可以使用完全重新加载。
热重载从 0.22 版本开始可用,您可以启用它
- 打开开发者菜单
- 点击“启用热重载”
简而言之的实现
既然我们已经了解了我们为什么需要它以及如何使用它,那么有趣的部分开始了:它实际上是如何工作的。
Hot Reloading 构建在 Hot Module Replacement 或 HMR 功能之上。它最初由 webpack 引入,我们在 React Native Packager 内部实现了它。HMR 使 Packager 监视文件更改并将 HMR 更新发送到应用程序中包含的精简 HMR 运行时。
简而言之,HMR 更新包含已更改的 JS 模块的新代码。当运行时收到它们时,它会将旧模块的代码替换为新代码
HMR 更新包含的内容不仅仅是我们想要更改的模块代码,因为仅仅替换它不足以让运行时获取更改。问题是模块系统可能已经缓存了我们想要更新的模块的 exports。例如,假设您的应用程序由以下两个模块组成
// 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;
模块 log
打印出提供的消息,包括模块 time
提供的当前日期。
当应用程序被捆绑时,React Native 使用 __d
函数在模块系统上注册每个模块。对于此应用程序,在许多 __d
定义中,将有一个用于 log
__d('log', function() {
... // module's code
});
此调用将每个模块的代码包装到一个匿名函数中,我们通常将其称为工厂函数。模块系统运行时跟踪每个模块的工厂函数、它是否已执行以及执行结果(exports)。当需要一个模块时,模块系统要么提供已缓存的 exports,要么首次执行模块的工厂函数并保存结果。
假设您启动您的应用程序并 require log
。此时,log
和 time
的工厂函数都尚未执行,因此没有缓存 exports。然后,用户修改 time
以返回 MM/DD
格式的日期
// time.js
function bar() {
const date = new Date();
return `${date.getMonth() + 1}/${date.getDate()}`;
}
module.exports = bar;
Packager 会将 time 的新代码发送到运行时(步骤 1),当 log
最终被 require 时,导出的函数将被执行,它将使用 time
的更改执行(步骤 2)
现在假设 log
的代码需要将 time
作为顶级 require
const time = require('./time'); // top level require
// log.js
function log(message) {
console.log(`[${time()}] ${message}`);
}
module.exports = log;
当 log
被 require 时,运行时将缓存其 exports 和 time
的 exports。(步骤 1)。然后,当 time
被修改时,HMR 进程不能仅仅在替换 time
的代码后就完成。如果这样做,当 log
被执行时,它将使用 time
的缓存副本(旧代码)。
为了让 log
获取 time
的更改,我们需要清除其缓存的 exports,因为它依赖的模块之一已被热交换(步骤 3)。最后,当 log
再次被 require 时,其工厂函数将被执行,require time
并获取其新代码。
HMR API
React Native 中的 HMR 通过引入 hot
对象扩展了模块系统。此 API 基于 webpack 的 API。hot
对象公开了一个名为 accept
的函数,该函数允许您定义一个回调,该回调将在模块需要热交换时执行。例如,如果我们按如下方式更改 time
的代码,每次我们保存 time 时,我们都会在控制台中看到“time changed”
// time.js
function time() {
... // new code
}
module.hot.accept(() => {
console.log('time changed');
});
module.exports = time;
请注意,只有在极少数情况下您才需要手动使用此 API。对于最常见的用例,热重载应该可以开箱即用。
HMR 运行时
正如我们之前所看到的,有时仅仅接受 HMR 更新是不够的,因为使用正在热交换的模块的模块可能已经被执行并且其导入已被缓存。例如,假设电影应用程序示例的依赖树有一个顶级的 MovieRouter
,它依赖于 MovieSearch
和 MovieScreen
视图,而这些视图又依赖于先前示例中的 log
和 time
模块
如果用户访问电影的搜索视图,但没有访问另一个视图,则除了 MovieScreen
之外的所有模块都将具有缓存的 exports。如果对模块 time
进行了更改,则运行时将必须清除 log
的 exports,以便它获取 time
的更改。该过程不会在那里结束:运行时将递归地重复此过程,直到所有父模块都被接受。因此,它会抓取依赖于 log
的模块并尝试接受它们。对于 MovieScreen
,它可以跳过,因为它尚未被 require。对于 MovieSearch
,它将必须清除其 exports 并递归处理其父模块。最后,它将对 MovieRouter
执行相同的操作,并在那里结束,因为没有模块依赖于它。
为了遍历依赖树,运行时从 Packager 接收 HMR 更新上的反向依赖树。对于此示例,运行时将收到如下所示的 JSON 对象
{
modules: [
{
name: 'time',
code: /* time's new code */
}
],
inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}
React 组件
React 组件更难与热重载一起工作。问题是我们不能简单地用新代码替换旧代码,因为我们会丢失组件的状态。对于 React Web 应用程序,Dan Abramov 实现了一个 Babel transform,它使用 webpack 的 HMR API 来解决此问题。简而言之,他的解决方案通过在 transform time 时为每个 React 组件创建一个代理来工作。代理保存组件的状态并将生命周期方法委托给实际组件,而实际组件是我们热重载的组件
除了创建代理组件之外,transform 还定义了 accept
函数,其中包含一段代码,用于强制 React 重新渲染组件。这样,我们就可以热重载渲染代码,而不会丢失应用程序的任何状态。
React Native 附带的默认 transformer 使用 babel-preset-react-native
,它 配置 为以与在使用 webpack 的 React Web 项目中使用 react-transform
相同的方式使用它。
Redux 存储
要在 Redux 存储上启用热重载,您只需使用 HMR API,类似于您在使用 webpack 的 Web 项目中所做的那样
// 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 时,用于接受该 reducer 的代码将被发送到客户端。然后客户端将意识到 reducer 不知道如何接受自身,因此它将查找引用它的所有模块并尝试接受它们。最终,流程将到达单个存储 configureStore
模块,该模块将接受 HMR 更新。
结论
如果您有兴趣帮助改进热重载,我鼓励您阅读 Dan Abramov 关于热重载未来的帖子 并做出贡献。例如,Johny Days 将 使其与多个连接的客户端一起工作。我们依靠大家来维护和改进此功能。
借助 React Native,我们有机会重新思考构建应用程序的方式,以使其成为出色的开发者体验。热重载只是拼图的一部分,我们还可以做哪些疯狂的 hack 来使其变得更好?