React Native 的目标是为您提供最佳的开发者体验。其中很重要的一部分是您保存文件到能够看到更改之间所需的时间。我们的目标是,即使您的应用不断增长,也要将此反馈循环缩短到 1 秒以内。
我们通过三个主要功能接近了这个理想目标
使用 JavaScript 作为语言,因为它没有漫长的编译周期。
实现了一个名为 Packager 的工具,它将 ES6/Flow/JSX 文件转换为 VM 可以理解的普通 JavaScript。它被设计为一个服务器,在内存中保存中间状态,以实现快速增量更改,并利用多核。
构建一个名为 Live Reload 的功能,在保存时重新加载应用。
此时,开发者的瓶颈不再是重新加载应用程序所需的时间,而是应用程序状态的丢失。一个常见的场景是,您正在处理一个远离启动屏幕的多个屏幕的功能。每次重新加载时,您都必须一遍又一遍地点击相同的路径才能返回到您的功能,这使得这个周期长达数秒。
热重载
热重载背后的想法是让应用程序保持运行,并在运行时注入您编辑过的文件的新版本。这样,您就不会丢失任何状态,这在您调整 UI 时特别有用。
一图胜千言。查看 Live Reload(当前)和 Hot Reload(新增)之间的区别。
VIDEO
如果你仔细观察,你会发现可以从红色错误框中恢复,并且还可以开始导入以前不存在的模块,而无需进行完全重新加载。
警告: 由于 JavaScript 是一种非常依赖状态的语言,热重载无法完美实现。在实践中,我们发现当前的设置对于大量常见用例都运行良好,并且在出现问题时始终可以进行完全重新加载。
热重载从 0.22 版本开始可用,您可以启用它
实现简述
既然我们已经了解了为什么需要它以及如何使用它,那么有趣的部分就开始了:它实际上是如何工作的。
热重载是基于 热模块替换 (Hot Module Replacement,简称 HMR)功能构建的。它最初由 webpack 引入,我们将其实现在 React Native Packager 内部。HMR 使 Packager 监视文件更改并将 HMR 更新发送到应用程序中包含的一个轻量级 HMR 运行时。
简而言之,HMR 更新包含了发生变化的 JS 模块的新代码。当运行时接收到它们时,它会将旧模块的代码替换为新代码。
HMR 更新包含的不仅仅是我们要更改的模块代码,因为仅仅替换它不足以让运行时识别更改。问题在于模块系统可能已经缓存了我们要更新模块的导出 。例如,假设您的应用程序由这两个模块组成
function log ( message ) { const time = require ( './time' ) ; console . log ( ` [ ${ time ( ) } ] ${ message } ` ) ; } module . exports = log ;
function time ( ) { return new Date ( ) . getTime ( ) ; } module . exports = time ;
模块 log
打印出提供的消息,其中包含模块 time
提供的当前日期。
当应用程序打包时,React Native 使用 __d
函数在模块系统上注册每个模块。对于这个应用程序,在许多 __d
定义中,会有一个用于 log
的定义。
__d ( 'log' , function ( ) { ... } ) ;
这个调用将每个模块的代码包装成一个匿名函数,我们通常称之为工厂函数。模块系统运行时会跟踪每个模块的工厂函数,包括它是否已经执行过,以及执行结果(导出)。当一个模块被请求时,模块系统要么提供已经缓存的导出,要么首次执行该模块的工厂函数并保存结果。
假设您启动应用程序并请求 log
。此时,log
和 time
的工厂函数都未执行,因此没有导出被缓存。然后,用户修改 time
以返回 MM/DD
格式的日期。
function bar ( ) { const date = new Date ( ) ; return ` ${ date . getMonth ( ) + 1 } / ${ date . getDate ( ) } ` ; } module . exports = bar ;
打包器会将 time
的新代码发送到运行时(步骤 1),当 log
最终被请求时,导出的函数将被执行,并包含 time
的更改(步骤 2)。
现在假设 log
的代码在顶层请求了 time
。
const time = require ( './time' ) ; 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”
function time ( ) { ... } module . hot . accept ( ( ) => { console . log ( 'time changed' ) ; } ) ; module . exports = time ;
请注意,只有在极少数情况下您才需要手动使用此 API。热重载应该适用于大多数常见用例,开箱即用。
HMR 运行时
正如我们之前所看到的,有时仅仅接受 HMR 更新是不够的,因为使用正在热替换的模块的模块可能已经执行并缓存了其导入。例如,假设电影应用程序示例的依赖树有一个顶层 MovieRouter
,它依赖于 MovieSearch
和 MovieScreen
视图,而这些视图又依赖于前面示例中的 log
和 time
模块。
如果用户访问了电影的搜索视图,但没有访问另一个视图,那么除了 MovieScreen
之外的所有模块都将具有缓存的导出。如果对模块 time
进行了更改,运行时将不得不清除 log
的导出,以便它接收 time
的更改。过程不会在那里结束:运行时将递归地重复此过程,直到所有父模块都被接受。因此,它将获取依赖于 log
的模块并尝试接受它们。对于 MovieScreen
,它可以跳过,因为它尚未被请求。对于 MovieSearch
,它将不得不清除其导出并递归处理其父模块。最后,它将对 MovieRouter
执行相同的操作并在那里结束,因为没有模块依赖于它。
为了遍历依赖树,运行时会在 HMR 更新中从 Packager 接收到逆向依赖树。对于此示例,运行时将收到一个如下所示的 JSON 对象
{ modules : [ { name : 'time' , 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
,该预设配置 为使用 react-transform
,方式与您在使用 webpack 的 React Web 项目中使用它的方式相同。
Redux Store
要在 Redux store 上启用热重载,您只需要像在使用了 webpack 的 Web 项目中那样使用 HMR API 即可。
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,我们有机会重新思考构建应用程序的方式,以创造出色的开发者体验。热重载只是其中一部分,我们还能做哪些“疯狂”的改进来使其变得更好呢?