资讯专栏INFORMATION COLUMN

ReactNative-HMR原理探索

GT / 1815人阅读

摘要:原理探索前言在开始本文前,先简单说下我们在开发项目中,本地的服务究竟扮演的是什么样的角色。这无疑是阉割了一大部分功能综上,如果仅仅用于切图,可能不会有那么多的问题

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.js

const 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这个总共做了以下几件事:

起一个socket服务,这样在监听到文件变动的时候能够将处理完的code通过socket层扔给App端

订阅packager server提供fileChange方法

拿到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.js

const 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}`,
              "
},",
              `${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.js

global.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如果仅仅用于切图,可能不会有那么多的问题

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94378.html

相关文章

  • React原理探索- @providesModule 模块系统

    摘要:原理探索模块系统是什么抛出组件化的概念后,对于开发者而言,为了提高代码的可读性与结构性,通过文件目录结构去阐述组件嵌套关系无疑是一个很好的办法,但是目录级别的加深,同时让的文件路径让人头疼。 React原理探索- @providesModule 模块系统 @providesModule是什么 react抛出组件化的概念后,对于开发者而言,为了提高代码的可读性与结构性,通过文件目录结构去...

    My_Oh_My 评论0 收藏0
  • #yyds干货盘点#探索RocketMQ的DefaultMQPullConsumer的原理及源码分析

    摘要:与相比最大的区别是,消费哪些队列的消息,从哪个位移开始消费,以及何时提交消费位移都是由程序自己的控制的。下面来介绍一下的内部原理。最后将对象集合返回给调用者。向发送请求获取参数对应的信息和配置信息,即对象。 前提介绍在RocketMQ中一般有两种获取消息的方式,一个是拉(pull,消费者主动去broker拉取)...

    不知名网友 评论0 收藏0
  • JavaScript尾递归优化探索

    摘要:原文地址尾调优化在知道尾递归之前,我们要直到什么是尾调用优化,因为尾调用优化是尾递归的基础。尾递归优化,就是利用尾调用优化的原理,对递归进行优化。所以当我们使用尾递归进行优化的时候,依旧发生了栈溢出的错误。 原文地址:https://github.com/HolyZheng/... 尾调优化 在知道尾递归之前,我们要直到什么是尾调用优化,因为尾调用优化是尾递归的基础。尾调用就是:在函...

    sean 评论0 收藏0
  • 利用RSA对前后端加密的探索

    摘要:项目地址前后端交互时为了保证信息安全可使用方式加密信息,在数据量大的时候可采用结合方式。由于加密和解密使用同样规则简称密钥,这被称为对称加密算法。从那时直到现在,算法一直是最广为使用的非对称加密算法。 RSA-JS-PHP 项目地址rsa-js-php 前后端交互时为了保证信息安全可使用RSA方式加密信息,在数据量大的时候可采用DES+RSA结合方式。DEMO演示地址 一点历史 1...

    banana_pi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<