跳到主要内容

介绍热重载

·9 分钟阅读
Martín Bigio
Instagram 软件工程师

React Native 的目标是为您提供最佳的开发者体验。其中很重要的一部分是您保存文件到能够看到变化所需的时间。我们的目标是即使您的应用不断增长,也能将这个反馈循环控制在 1 秒以内。

我们通过以下三个主要功能接近了这个理想

  • 使用 JavaScript 作为语言,因为它没有较长的编译周期时间。
  • 实现一个名为 Packager 的工具,它将 es6/flow/jsx 文件转换为 VM 可以理解的普通 JavaScript。它被设计成一个服务器,在内存中保存中间状态,以实现快速增量更改,并使用多核。
  • 构建一个名为 Live Reload 的功能,它在保存时重新加载应用程序。

此时,开发人员的瓶颈不再是重新加载应用所需的时间,而是失去应用的状态。一个常见的情况是,您正在处理一个距离启动屏幕多层界面的功能。每次重新加载,您都必须一遍又一遍地点击相同的路径才能回到您的功能,这使得这个周期持续数秒。

热重载

热重载的理念是保持应用运行,并在运行时注入您编辑的文件的新版本。这样,您不会丢失任何状态,这在您调整 UI 时特别有用。

一图胜千言。查看 Live Reload(当前)和 Hot Reload(新)之间的区别。

如果您仔细观察,会发现可以从红框错误中恢复,并且还可以开始导入以前不存在的模块,而无需进行完全重新加载。

警告:由于 JavaScript 是一种非常依赖状态的语言,热重载无法完美实现。在实践中,我们发现当前设置对于大量常见用例都运行良好,并且在出现问题时,始终可以进行完全重新加载。

热重载从 0.22 版本开始可用,您可以启用它

  • 打开开发者菜单
  • 点击“启用热重载”

实现概述

既然我们已经了解了为什么需要它以及如何使用它,那么有趣的部分就开始了:它实际上是如何工作的。

热重载是建立在 热模块替换(Hot Module Replacement,简称 HMR)功能之上的。它最初由 webpack 引入,我们将其实现到了 React Native Packager 中。HMR 使 Packager 能够监视文件更改,并将 HMR 更新发送到应用程序中包含的一个轻量级 HMR 运行时。

简而言之,HMR 更新包含已更改的 JS 模块的新代码。当运行时接收到它们时,它会将旧模块的代码替换为新代码。

HMR 更新包含的不仅仅是我们要更改的模块代码,因为仅替换代码不足以让运行时识别更改。问题在于模块系统可能已经缓存了我们要更新模块的导出。例如,假设您的应用由以下两个模块组成

// 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
});

这种调用将每个模块的代码包装成一个匿名函数,我们通常称之为工厂函数。模块系统运行时会跟踪每个模块的工厂函数、它是否已被执行以及执行结果(导出)。当需要一个模块时,模块系统要么提供已缓存的导出,要么首次执行模块的工厂函数并保存结果。

因此,假设您启动应用并引用 log。此时,logtime 的工厂函数都尚未执行,因此没有导出被缓存。然后,用户修改 timeMM/DD 格式返回日期。

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

module.exports = bar;

Packager 会将 time 的新代码发送到运行时(步骤 1),当 log 最终被引用时,导出的函数会执行并带有 time 的更改(步骤 2)。

现在假设 log 的代码将 time 作为顶级引用。

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

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

module.exports = log;

log 被引用时,运行时会缓存其导出和 time 的导出。(步骤 1)。然后,当 time 被修改时,HMR 过程不能简单地在替换 time 的代码后结束。如果这样做,当 log 执行时,它将使用 time 的缓存副本(旧代码)。

为了让 log 拾取 time 的更改,我们需要清除其缓存的导出,因为它依赖的一个模块被热替换了(步骤 3)。最后,当 log 再次被引用时,其工厂函数将执行,引用 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,它依赖于 MovieSearchMovieScreen 视图,而这些视图又依赖于前面示例中的 logtime 模块。

如果用户访问电影搜索视图而不是另一个视图,除了 MovieScreen 之外的所有模块都将拥有缓存的导出。如果对模块 time 进行了更改,运行时将不得不清除 log 的导出,以便它能够接收 time 的更改。这个过程不会就此结束:运行时将递归地重复这个过程,直到所有父模块都被接受。因此,它将获取依赖于 log 的模块并尝试接受它们。对于 MovieScreen,它可以跳过,因为它尚未被引用。对于 MovieSearch,它将不得不清除其导出并递归处理其父模块。最后,它将对 MovieRouter 执行相同的操作,并在此处结束,因为没有模块依赖于它。

为了遍历依赖树,运行时在 HMR 更新中从 Packager 接收反向依赖树。对于这个例子,运行时将收到一个这样的 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 转换,它使用 webpack 的 HMR API 来解决这个问题。简而言之,他的解决方案是在转换时为每个 React 组件创建一个代理。这些代理持有组件的状态,并将生命周期方法委托给实际组件,这些实际组件是我们热重载的对象。

除了创建代理组件之外,该转换还使用一段代码定义了 accept 函数,以强制 React 重新渲染组件。通过这种方式,我们可以在不丢失任何应用程序状态的情况下热重载渲染代码。

React Native 附带的默认 转换器 使用 babel-preset-react-native,它被配置为以与使用 webpack 的 React Web 项目相同的方式使用 react-transform

Redux 存储

要在 Redux 存储上启用热重载,您只需像在使用 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 时,接受该 reducer 的代码将发送到客户端。然后客户端会意识到该 reducer 不知道如何接受自身,因此它会查找所有引用它的模块并尝试接受它们。最终,流程将到达单个 store,即 configureStore 模块,它将接受 HMR 更新。

总结

如果您有兴趣帮助改进热重载,我鼓励您阅读 Dan Abramov 关于热重载未来的文章并做出贡献。例如,Johny Days 将使其与多个连接的客户端协同工作。我们依赖大家来维护和改进此功能。

通过 React Native,我们有机会重新思考构建应用程序的方式,以提供出色的开发者体验。热重载只是其中的一部分,我们还可以做哪些疯狂的技巧来使其更好?