ReactNative-HMR原理探索

ReactNative-HMR原理探索

前言

在開始本文前,先簡單說下咱們在開發RN項目中,本地的node服務究竟扮演的是什麼樣的角色。在咱們的RN APP中有配置本地開發的地方,只要咱們輸入咱們本地的IP和端口號8081就能夠開始調試本地代碼,其實質是APP發起了一個請求bundle文件的HTTP請求,而咱們的node server在接到request後,開始對本地項目文件進行babel,pack,最後返回一個bundle.js。而本地的node服務扮演的角色還不止如此,好比啓動基礎服務dev tool,HMR等node

什麼是HMR

HMR(Hot Module Replacement)模塊熱替換,能夠類比成Webpack的Hot Reload。可讓你在代碼變更後不用reload app,代碼直接生效,且當前路由棧不會發生改變react

名詞說明

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

實現原理

先貼上我的整理的的一個HMR熱更新的過程

咱們來逐步按流程對應相應的源碼分析web

啓動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的關係)segmentfault

#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這個總共作了如下幾件事:react-native

  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的異步隊列...扯遠了,有空的話,能夠專門分享一下。babel

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 bundleapp

真正的熱更新過程

#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,這樣熱替換的過程就完成了,形以下圖socket

那麼問題就來了,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若是僅僅用於切圖,可能不會有那麼多的問題
相關文章
相關標籤/搜索