博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ReactNative-HMR原理探索
阅读量:5861 次
发布时间:2019-06-19

本文共 9262 字,大约阅读时间需要 30 分钟。

ReactNative-HMR原理探索

前言

在开始本文前,先简单说下我们在开发RN项目中,本地的node服务究竟扮演的是什么样的角色。在我们的RN APP中有配置本地开发的地方,只要我们输入我们本地的IP和端口号8081就可以开始调试本地代码,其实质是APP发起了一个请求bundle文件的HTTP请求,而我们的node server在接到request后,开始对本地项目文件进行babel,pack,最后返回一个bundle.js。而本地的node服务扮演的角色还不止如此,比如启动基础服务dev tool,HMR等

什么是HMR

HMR(Hot Module Replacement)模块热替换,可以类比成Webpack的Hot Reload。可以让你在代码变动后不用reload app,代码直接生效,且当前路由栈不会发生改变

名词说明

  • 逆向依赖:如上图 对于D模块来说,A,B文件就是D的逆向依赖
  • 浅层依赖:如上图 对于index.js来说,A,B模块就是index.js的浅层依赖(直属依赖),C,D,E跟index没有直接依赖关系

实现原理

先贴上个人整理的的一个HMR热更新的过程

我们来逐步按流程对应相应的源码分析

启动Packerage&HMR server

run packager server

# react-native/local-cli/server/runServer.jsconst serverInstance = http.createServer(app).listen(   args.port,   args.host,   () => {     attachHMRServer({       httpServer: serverInstance,       path: '/hot',       packagerServer,     });     wsProxy = webSocketProxy.attachToServer(serverInstance, '/debugger-proxy');     ms = messageSocket.attachToServer(serverInstance, '/message');     webSocketProxy.attachToServer(serverInstance, '/devtools');     readyCallback();   } );

本地启动在8081启动HTTP服务的同时,也初始化了本地HMR的服务,这里在初始化的时候注入了packagerServer,为的是能订阅packagerServer提供的watchman回调,同时也为了能拿到packagerServer提供的getDependencies方法,这样能在HMR内部拿到文件的依赖关系(相互require的关系)

#react-native/local-cli/server/util/attachHMRServer.js// 略微简化下代码function attachHMRServer({httpServer, path, packagerServer}) {        ...        const WebSocketServer = require('ws').Server;     const wss = new WebSocketServer({       server: httpServer,       path: path,     });     wss.on('connection', ws => {     ...   getDependencies(params.platform, params.bundleEntry)     .then((arg) => {       client = {         ...       };   packagerServer.setHMRFileChangeListener((filename, stat) => {                ...                 client.ws.send(JSON.stringify({type: 'update-start'}));         stat.then(() => {           return packagerServer.getShallowDependencies({             entryFile: filename,             platform: client.platform,             dev: true,             hot: true,           })             .then(deps => {               if (!client) {                 return [];               }               const oldDependencies = client.shallowDependencies[filename];               // 分析当前文件的require关系是否与之前一致,如果require关系有变动,需要重新对文件的dependence进行分析               if (arrayEquals(deps, oldDependencies)) {                 return packagerServer.getDependencies({                   platform: client.platform,                   dev: true,                   hot: true,                   entryFile: filename,                   recursive: true,                 }).then(response => {                   const module = packagerServer.getModuleForPath(filename);                   return response.copy({dependencies: [module]});                 });               }               return getDependencies(client.platform, client.bundleEntry)                 .then(({                   dependenciesCache: depsCache,                   dependenciesModulesCache: depsModulesCache,                   shallowDependencies: shallowDeps,                   inverseDependenciesCache: inverseDepsCache,                   resolutionResponse,                 }) => {                   if (!client) {                     return {};                   }               return packagerServer.buildBundleForHMR({                 entryFile: client.bundleEntry,                 platform: client.platform,                 resolutionResponse,               }, packagerHost, httpServerAddress.port);             })             .then(bundle => {               if (!client || !bundle || bundle.isEmpty()) {                 return;               }               return JSON.stringify({                 type: 'update',                 body: {                   modules: bundle.getModulesIdsAndCode(),                   inverseDependencies: client.inverseDependenciesCache,                   sourceURLs: bundle.getSourceURLs(),                   sourceMappingURLs: bundle.getSourceMappingURLs(),                 },               });             })            .then(update => {               client.ws.send(update);             });           }         ).then(() => {           client.ws.send(JSON.stringify({type: 'update-done'}));         });       });       client.ws.on('close', () => disconnect());     })}

RN最舒服的地方就是命名规范,基本看到函数名就能知道他的职能,我们来看上面这段代码,attachHMRServer这个总共做了以下几件事:

  1. 起一个socket服务,这样在监听到文件变动的时候能够将处理完的code通过socket层扔给App端
  2. 订阅packager server提供fileChange方法
  3. 拿到packager server提供的getDependence方法,对变动文件进行简单的依赖分析。如果说发现变动文件A之前require了B,C文件,但是这次只require了B文件,oldDependencies!==currentDep(这里HMRServer为了优化性能,对浅层依赖关系,逆向依赖关系,依赖缓存时间都做了cache),那么HMR server会让Packager Server重新梳理一遍项目文件的依赖关系(因为可能存在增删文件的可能),同时对它局部维护的一些cache Map做更新

HMRClient

注册

我们已经看到了socket的发送方,那么必定存在一个接收方,也就是这里要讲的HMRClient,首先先来看这边注册函数

#react-native/Libraries/BatchedBridge/BatchedBridge.jsconst MessageQueue = require('MessageQueue');const BatchedBridge = new MessageQueue(  () => global.__fbBatchedBridgeConfig,  serializeNativeParams);const Systrace = require('Systrace');const JSTimersExecution = require('JSTimersExecution');BatchedBridge.registerCallableModule('Systrace', Systrace);BatchedBridge.registerCallableModule('JSTimersExecution', JSTimersExecution);BatchedBridge.registerCallableModule('HeapCapture', require('HeapCapture'));if (__DEV__) {  BatchedBridge.registerCallableModule('HMRClient', require('HMRClient'));}

这边就是HMRClient注册阶段,贴这段代码其实是因为发现RN里的JS->Native,Native->JS通信是通过MQ(MessageQueue)实现的,而追溯到最里层发现竟然是一套setTimeout,setImmediate的异步队列...扯远了,有空的话,可以专门分享一下。

HMRClient

#react-native/Libraries/Utilities/HMRClient.js        activeWS.onmessage = ({ data }) => {                        ...          modules.forEach(({ id, code }, i) => {                                ...                        const injectFunction = typeof global.nativeInjectHMRUpdate === 'function'              ? global.nativeInjectHMRUpdate              : eval;            code = [              '__accept(',              `${id},`,              'function(global,require,module,exports){',              `${code}`,              '\n},',              `${JSON.stringify(inverseDependencies)}`,              ');',            ].join('');            injectFunction(code, sourceURLs[i]);          });      }    };

HMRClient做的事就很简单了,接到socket传入的String,直接eval运行,这边的code形如下图

我们可以看到这边是一个__accept函数在接受这个变更后的HMR bundle

真正的热更新过程

#react-native/packager/react-packager/src/Resolver/polyfills/require.js  const accept = function(id, factory, inverseDependencies) {      //在当前模块映射表里查找,如果找的到将其Code进行替换,并执行,若没有,重新进行声明    const mod = modules[id];    if (!mod) {        //重新申明      define(id, factory);      return true; // new modules don't need to be accepted    }    const {hot} = mod;    if (!hot) {      console.warn(        'Cannot accept module because Hot Module Replacement ' +        'API was not installed.'      );      return false;    }    // replace and initialize factory    if (factory) {      mod.factory = factory;    }    mod.hasError = false;    mod.isInitialized = false;    //真正进行热替换的地方    require(id);    //当前模块热更新后需要执行的回调,一般用来解决循环引用    if (hot.acceptCallback) {      hot.acceptCallback();      return true;    } else {      // need to have inverseDependencies to bubble up accept      if (!inverseDependencies) {        throw new Error('Undefined `inverseDependencies`');      }        //将当前moduleId的逆向依赖传入,热更新他的逆向依赖,递归执行      return acceptAll(inverseDependencies[id], inverseDependencies);    }  };  global.__accept = accept;

这边的代码就不进行删减了,accept函数接受三个参数,moduleId,factory,inverseDependencies。

  • moduleId:需要热更新的ID,对于每个模块,都会被赋予一个模块ID,RN 30之前的版本使用的是filePath作为key,而后使用的是一个递增的整型
  • factory:babel后实际的需要热替换的code
  • inverseDependencies:当前所有的逆向依赖Map

简单来说accept做的事情就是判断变动当前模块是新加的需要define,还是说直接更新内存里已存在的module,同时沿着他的逆向依赖树,全部load一遍,一直到最顶级的AppResigterElement,这样热替换的过程就完成了,形如下图

那么问题就来了,react的View展现对state是强依赖的,重新load一遍,state不会丢失么,实际上在load的过程中,RN把老的ref传入了,所以继承了之前的state

讲到这还略过了最重要的一点,为什么说我这边热替换了内存中module,并执行了一遍,我的App就能拿到这个更新后的代码,我们依旧拿代码来说

#react-native/packager/react-packager/src/Resolver/polyfills/require.jsglobal.require = require;global.__d = define;const modules = Object.create(null);function define(moduleId, factory) {  if (moduleId in modules) {    // prevent repeated calls to `global.nativeRequire` to overwrite modules    // that are already loaded    return;  }  modules[moduleId] = {    factory,    hasError: false,    isInitialized: false,    exports: undefined,  };  if (__DEV__) {    // HMR    modules[moduleId].hot = createHotReloadingObject();    // DEBUGGABLE MODULES NAMES    // avoid unnecessary parameter in prod    const verboseName = modules[moduleId].verboseName = arguments[2];    verboseNamesToModuleIds[verboseName] = moduleId;  }}function require(moduleId) {  const module = __DEV__    ? modules[moduleId] || modules[verboseNamesToModuleIds[moduleId]]    : modules[moduleId];  return module && module.isInitialized    ? module.exports    : guardedLoadModule(moduleId, module);}

RN复写了require,这样所有模块其实拿到的是这里

HMR存在的问题

  • 由于其原理是逆向load其依赖树,如果说项目的技术方法破坏了其树状依赖结构,那么HMR也没法生效。例如通过global挂载包装了AppResigter这样的方法。
  • 由于Ctrl+s会立即触发watchMan的回调,导致可能代码改了一半就走进了HMR的逻辑,在transfrom Code或者require的时候就直接红屏了
  • 由于其HMR原理是逆向执行依赖树,如果项目中存在文件循环引用,也会导致栈溢出,可以通过文件增加module.hot.accept这样的方法解决,但是如果项目公用方法存在这样的问题,就只能强行把HMR的逆向加载这块代码注释了。这无疑是阉割了HMR一大部分功能
  • 综上,HMR如果仅仅用于切图,可能不会有那么多的问题

转载地址:http://gzgjx.baihongyu.com/

你可能感兴趣的文章
《统一沟通-微软-技巧》-19-Lync 2010如何使用智能手机中联系人
查看>>
SCCM2012SP1---分发部署软件
查看>>
活动目录长时间未复制链接丢失解决办法
查看>>
19.Azure备份Azure上的虚拟机(上)
查看>>
自定义javascript调试输出函数
查看>>
云计算让教学随需应变
查看>>
linux下用tar进行数据备份
查看>>
单臂路由实现vlan之间的通信
查看>>
AMD:“全民四核”大作战
查看>>
微信账号,欢迎一起探讨信息、知识、学习和管理!
查看>>
Puppet基础篇7-编写第一个完整测试模块puppet
查看>>
用Saltstack的returners实现批量监控和数据存储
查看>>
Powershell管理系列(十七)在PowerShell中添加Exchange管理单元
查看>>
mySQL教程 第17章 MySQL群集
查看>>
浅谈程序猿的职业规划,看你如何决定自己的未来吧。
查看>>
Exchange Server 2013系列六:客户端访问服务器角色高可用性概述
查看>>
SUV车为何突然成了香饽饽?
查看>>
Android窗口管理服务WindowManagerService对输入法窗口(Input Method Window)的管理分析...
查看>>
Nut防丢启示录:智能硬件的未来
查看>>
关于ES6 import命令的一个补充
查看>>