ReactNative-HMR道理探究

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,代碼直接見效,且當前路由棧不會發作轉變

名詞說明

《ReactNative-HMR道理探究》

  • 逆向依靠:如上圖 關於D模塊來講,A,B文件就是D的逆向依靠
  • 淺層依靠:如上圖 關於index.js來講,A,B模塊就是index.js的淺層依靠(直屬依靠),C,D,E跟index沒有直接依靠關聯

完成道理

先貼上個人整頓的的一個HMR熱更新的歷程
《ReactNative-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這個統共做了以下幾件事:

  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.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}`,
              '\n},',
              `${JSON.stringify(inverseDependencies)}`,
              ');',
            ].join('');

            injectFunction(code, sourceURLs[i]);
          });
      }
    };

HMRClient做的事就很簡樸了,接到socket傳入的String,直接eval運轉,這邊的code形如下圖
《ReactNative-HMR道理探究》
我們可以看到這邊是一個__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,如許熱替代的歷程就完成了,形如下圖

《ReactNative-HMR道理探究》

那末題目就來了,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假如僅僅用於切圖,能夠不會有那末多的題目
    原文作者:natureless
    原文地址: https://segmentfault.com/a/1190000014458395
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞