跳至主要内容

介绍热重载

·阅读时间: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 版本开始提供热重载,您可以启用它

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

简而言之的实现

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

热重载建立在 热模块替换(或 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,它可以终止,因为它尚未被 require。对于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 不知道如何接受自身,因此它将查找所有引用它的模块并尝试接受它们。最终,流程将到达单个存储,即configureStore模块,该模块将接受 HMR 更新。

结论

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

使用 React Native,我们有机会重新思考构建应用程序的方式,以使其成为出色的开发体验。热重载只是拼图的一块,我们还能做哪些疯狂的黑客行为来使其变得更好?