React Native的热更新

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收到时,会将旧的代码替换为新的:


HMR structure

下面通过代码示例进行讲解:

// 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。

no cacke

存在缓存:先清除缓存 -> 替换time -> 在log使用time时重新建立缓存

has been required

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的实现细节。

对于应用来说更新步骤如下:

  1. 应用使用HRM runtime去检测更新。
  2. runtime异步下载更新,通知应用。
  3. 告知runtime去应用更新
  4. 同步去应用更新

对于编译器的更新步骤如下:

  1. 更新manifest
  2. 更新chunks,chunks是WebPack用于代码分离的工具之一。

manifest内包含新的编译哈希和已更新的chunks列表。

对于Runtime的更新步骤:
这里我们先说一下Runtime:对于manifest来讲,runtime主要是指在浏览器运行时,webpack 用来连接模块化的应用程序的所有代码。runtime 包含在模块交互时连接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。
对于组件系统运行时,runtime提供了两个方法:checkapply
check会发起HTTP请求来更新manifest,如果请求失败,则更新失败。请求成功后会对比新的chunks和已经加载的chunks列表。当所有更新的chunks下载完成并且可以应用时,runtime会切换到ready状态。

apply方法会将所有更新的组件标记为不可用。对于不可用组件,需要handler去处理,如果没有处理,会逐级向上寻找,知道找到handler或者程序入口,如果直至入口都没有处理,就会退出程序。

HMR Runtime

如果需要更新的组件,已经被缓存了,就不能单纯的只是完成替换,需要先接触缓存的绑定关系。清除依赖关系的方式是递归进行的。


clear recursively

对于已经缓存的组件,会查找依赖关系,并逐级解除。例如上图,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和本地状态被销毁的问题,有两种不同的做法:

  1. 找到一种方法,将React实例从DOM和状态中分离出来,只更新这个实例,完成后将其“粘回”DOM和state。
  2. 使用代理组件类型。这样对于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组件创建代理,代理在组件真正的生命周期中持有它们的状态和其他代理方法。

React Component hot reload by proxy

除了创建代理组件,转换还定义了accept方法,用于对组件进行强制刷新。这样组件的热更新就不会丢失任何状态了。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342