Hot Reloading
React Native(下文简称RN)致力于提供最好的开发体验,亮点之一就是极大缩短了修改文件到页面刷新的时间。在文件修改之后,页面可以直接获取到这一变动并更新逻辑,这一技术被称为Hot Reloading。
RN这一技术的实现依赖以下三种特性:
- 使用JavaScript作为开发语言,规避了长时间编译的问题。
- 使用Packager的工具将es6/flow/jsx文件转为Virtual Dom可以理解的普遍JS文件。Packager以服务器的形式将中间状态保存在内存中,这一处理使得对快速更新变动提供了强效支持,并且使用多内核处理。
- 使用一个称为Live Reload的特性在项目保存后刷新。
通过以上特性,开发瓶颈由编译时间变为如何保持APP的state。
Hot Reloading实现
Hot Reloading的设计是在文件发生变化时,实时去刷新应用的状态。Hot Reloading是在Hot Module Replacement特性的基础上实现的,简称HMR(热组件替换)。这个特性由Webpack首次提出,现在应用在RN Packager。
HMR包含了JS组件改变后的最新代码,在HMR Runtime收到时,会将旧的代码替换为新的:
下面通过代码示例进行讲解:
// 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.js内依赖了time。
在应用打包时,RN会在组件系统内通过__d
方法注册每个组件。对于以上示例来说,在众多的__d
定义中,log
的定义如下:
__d('log', function() {
... // module's code
});
RN将每个组件的代码通过匿名函数的方式封装,类似于我们知道的工厂方法。组件系统的运行时会追踪每个组件的工厂方法。
组件在被获取后会进行缓存,缓存与否的处理方式是不一样的。
未缓存:在time的代码发生改变时,Packager会将time的最新代码发送给runtime。
存在缓存:先清除缓存 -> 替换time -> 在log使用time时重新建立缓存
HMR API
RN中通过引入hot
对象的方式扩展了组件系统。hot
对象提供了accept
方法用于接受组件被修改后的回调方法。
下面这个示例,是说明了HMR中accept的用法,并没有使用React Hot Loader,没有React Transform等其他RN Hot Reloading的特性。HMR并不会改变React组件的语法,HMR只是一个用于处理几个步骤的优秀框架:获取更新,将更新更新的模块注入到脚本,调用回调。
var React = require('react')
var ReactDOM = require('react-dom')
// Render the root component normally
var rootEl = document.getElementById('root')
ReactDOM.render(<App />, rootEl)
// Are we in development mode?
if (module.hot) {
// Whenever a new version of App.js is available
module.hot.accept('./App', function () {
// Require the new version and render it instead
var NextApp = require('./App')
ReactDOM.render(<NextApp />, rootEl)
})
}
WebPack设计HMR的内部细节
上面我们说过,React Native的Hot Reloading是在HMR的基础上实现的,而HMR是WebPack的重要特性,我们下面分析下,HRM的实现细节。
对于应用来说更新步骤如下:
- 应用使用HRM runtime去检测更新。
- runtime异步下载更新,通知应用。
- 告知runtime去应用更新
- 同步去应用更新
对于编译器的更新步骤如下:
manifest内包含新的编译哈希和已更新的chunks列表。
对于Runtime的更新步骤:
这里我们先说一下Runtime:对于manifest来讲,runtime主要是指在浏览器运行时,webpack 用来连接模块化的应用程序的所有代码。runtime 包含在模块交互时连接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。
对于组件系统运行时,runtime提供了两个方法:check
和apply
。
check
会发起HTTP请求来更新manifest,如果请求失败,则更新失败。请求成功后会对比新的chunks和已经加载的chunks列表。当所有更新的chunks下载完成并且可以应用时,runtime会切换到ready
状态。
apply
方法会将所有更新的组件标记为不可用。对于不可用组件,需要handler去处理,如果没有处理,会逐级向上寻找,知道找到handler或者程序入口,如果直至入口都没有处理,就会退出程序。
HMR Runtime
如果需要更新的组件,已经被缓存了,就不能单纯的只是完成替换,需要先接触缓存的绑定关系。清除依赖关系的方式是递归进行的。
对于已经缓存的组件,会查找依赖关系,并逐级解除。例如上图,log的上级有MovieScreen和MovieSearch,MovieScreen没有缓存log,所以递归结束。MovieSearch依赖log,而MovieRouter依赖MovieSearch,他们之间的缓存也需要解除。
为了遍历依赖树,会创建一个逆序依赖文件,如下:
{
modules: [
{
name: 'time',
code: /* time's new code */
}
],
inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}
React 组件
React组件使用Hot Reloading机制要更复杂些,因为不能单纯的替换代码,这样会导致DOM和组件的状态丢失。
因为模块被重新计算了,内部组件的ID和过去的不同,对于React而言,你想要重新渲染一个全心的组件,所以React会卸载过去组件。所以,如上所说,React会摧毁组件的DOM和本地state。
解决这种问题有几种方法:
将state存于外部
类似于Redux这种数据流管理框架,每个组件并不管理自己的状态,而是整个APP公用一个state,每个组件获取这个state内的数据,这样在更新组件时,就不需要担心组件更新的问题。
保存DOM和本地state
为了解决DOM和本地状态被销毁的问题,有两种不同的做法:
- 找到一种方法,将React实例从DOM和状态中分离出来,只更新这个实例,完成后将其“粘回”DOM和state。
- 使用代理组件类型。这样对于React来说,类型没有改变,不需要重新去卸载和装载,内部的实际实现,会根据热更新发生变化。
失败方案:React实例从DOM和状态中分离
第一种方法听上去更好,但是目前React并没有提供可以分离/聚合React实例和DOM、还有运行的生命周期钩子。哪怕我们可以使用React的私有API,这个方案也未必可行。
例如React组件可能会订阅一些生命周期方法(componentDidMount
等),即使我们可以在不摧毁DOM和状态的情况下静默替换旧的实例,但是因为过去的实例没有取消对生命周期的订阅,新的实例也无法订阅成功。
成功方案:Proxy Component代理组件
Proxy Component就是使用在React Hot Loader和React Transform中的方案。这种方案会改变代码的语法,但是目前在React中的应用还算成功。
Proxy就是一个类,内部封装了关于显示和状态的实现。
React Hot Loader将通过module.exports
内的React组件通过代理进行封装,输出封装后的代理类。
可以这么理解:当调用<App>render<NavBar>时,实际是在render<NavbarProxy>。
转换机制:在转换时为每个React组件创建代理,代理在组件真正的生命周期中持有它们的状态和其他代理方法。
除了创建代理组件,转换还定义了
accept
方法,用于对组件进行强制刷新。这样组件的热更新就不会丢失任何状态了。