webpack - hmr熱更新

文章首發於我的blog,歡迎關注~javascript

webpack hmr

webpack-dev-server

在使用 webpack-dev-server 的過程當中,若是指定了 hot 配置的話(使用 inline mode 的前提下), wds 會在內部更新 webpack 的相關配置,即將 HotModuleReplacementPlugin 加入到 webpack 的 plugins 當中。vue

HotModuleReplacementPlugin

在 HotModuleReplacementPlugin 執行的過程當中主要是完成了如下幾個工做:java

  1. 在建立 normalModule 的階段添加 parser 的 hook,即在以後的 module 編譯解析階段 parser 處理不一樣的語法時能夠交由在這個階段添加的 hook 回調來進行相關的處理。
normalModuleFactory.hooks.parser
  .for("javascript/auto")
  .tap("HotModuleReplacementPlugin", addParserPlugins);

normalModuleFactory.hooks.parser
  .for("javascript/dynamic")
  .tap("HotModuleReplacementPlugin", addParserPlugins);

其中在 addParserPlugins 方法當中添加了具體有關 parser hook 的回調,有幾個比較關鍵的 hook 單獨拿出來講下:webpack

parser.hooks.call
  .for("module.hot.accept")
  .tap("HotModuleReplacementPlugin")

這個 hook 主要是在 parser 編譯代碼過程當中遇到module.hot.accept的調用的時候會觸發,主要的工做就是處理當前模塊部署依賴模塊的依賴分析,在編譯階段處理好依賴的路徑替換等內容。git

parser.hooks.call
  .for("module.hot.decline")
  .tap("HotModuleReplacementPlugin")

這個 hook 一樣是在 parser 編譯代碼過程當中遇到module.hot.decline的調用的時候觸發,所作的工做和上面的 hook 相似。github

  1. 在 mainTemplate 上添加不一樣 hook 的處理回調來完成對於 webpack 在生成 bootstrap runtime 的代碼階段去注入和 hmr 相關的運行時代碼,有幾個比較關鍵的 hook 單獨拿出來講下:
const mainTemplate = compilation.mainTemplate

mainTemplate.hooks.moduleRequire.tap(
  "HotModuleReplacementPlugin",
  (_, chunk, hash, varModuleId) => {
    return `hotCreateRequire(${varModuleId})`;
})

這個 hook 主要完成的工做是在生成 webpack bootstrap runtime 代碼當中對加載 module 的 require function進行替換,變爲hotCreateRequire(${varModuleId})的形式,這樣作的目的其實就是對於 module 的加載作了一層代理,在加載 module 的過程中創建起相關的依賴關係(須要注意的是這裏的依賴關係並不是是 webpack 在編譯打包構建過程當中的那個依賴關係,而是在 hmr 模式下代碼執行階段,一個 module 加載其餘 module 時在 hotCreateRequire 內部會創建起相關的加載依賴關係,方便以後的修改代碼以後進行的熱更新操做),具體這塊的分析能夠參見下面的章節。web

mainTemplate.hooks.bootstrap.tap(
  "HotModuleReplacementPlugin",
  (source, chunk, hash) => {
    // 在生成 runtime 最終的代碼前先經過 hooks.hotBootstrap 鉤子生成相關的 hmr 代碼而後再完成代碼的拼接
    source = mainTemplate.hooks.hotBootstrap.call(source, chunk, hash);
    return Template.asString([
      source,
      "",
      hotInitCode
        .replace(/\$require\$/g, mainTemplate.requireFn)
        .replace(/\$hash\$/g, JSON.stringify(hash))
        .replace(/\$requestTimeout\$/g, requestTimeout)
        .replace(
          /\/\*foreachInstalledChunks\*\//g, // 經過一系列的佔位字符串,在生成代碼的階段完成代碼的替換工做
          needChunkLoadingCode(chunk)
            ? "for(var chunkId in installedChunks)"
            : `var chunkId = ${JSON.stringify(chunk.id)};`
        )
    ]);
  }
)

在這個 hooks.bootstrap 當中所作的工做是在 mainTemplate 渲染 bootstrap runtime 的代碼的過程當中,對於hotInitCode代碼進行字符串的匹配和替換工做。hotInitCode這部分的代碼其實就是下面章節所要講的HotModuleReplacement.runtime向 bootstrap runtime 代碼裏面注入的 hmr 運行時代碼。ajax

mainTemplate.hooks.moduleObj.tap(
  "HotModuleReplacementPlugin",
  (source, chunk, hash, varModuleId) => {
    return Template.asString([
      `${source},`,
      `hot: hotCreateModule(${varModuleId}),`, // 這部分的內容即這個 hook 對相關內容的拓展
      "parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),",
      "children: []"
    ]);
  }
)

在這個 hooks.moduleObj 當中所作的工做是對__webpack_require__這個函數體內部的 installedModules 緩存模塊變量進行拓展。幾個很是關鍵的點就是:json

  1. 新增了 module 上的 hot: hotCreateModule(${varModuleId}) 配置。這個 module.hot api 即對應這個 module 有關熱更新的 api,能夠看到這個部署 hot api 的工做是由 hotCreateModule 這個方法來完成的(這個方法是由 hmr runtime 代碼提供的,下面的章節會講)。最終和這個 module 全部有關熱更新相關的接口都經過module.hot.*去訪問。
  2. 新增 parents 屬性配置:初始化有關這個 module 在 hmr 下,它的 parents(這個 module 被其餘 module 依賴);
  3. 新增 children 屬性配置:初始化有關這個 module 在 hmr 下,它的 children(這個 module 所依賴的 module)

HotModuleReplacement.runtime

Webpack 內部提供了 HotModuleReplacement.runtime 即熱更新運行時部分的代碼。這部分的代碼並非經過經過添加 webpack.entry 入口文件的方式來注入這部分的代碼,而是經過 mainTemplate 在渲染 boostrap runtime 代碼的階段完成代碼的注入工做的(對應上面的 mainTemplate.hooks.boostrap 所作的工做)。bootstrap

在這部分熱更新運行時的代碼當中所作的工做主要包含了如下幾個點:

  1. 提供運行時的hotCreateRequire方法,用以對__webpack_require__模塊引入方法進行代理,當一個模塊依賴其餘模塊,並將其引入的時候,會創建起宿主模塊和依賴模塊之間的相互依賴關係,這個依賴關係也是做爲以後某個模塊發生更新後,尋找與其有依賴關係的模塊的憑證。
function hotCreateRequire(moduleId) {
  var me = installedModules[moduleId];
  if (!me) return $require$;
  var fn = function(request) { // 這個是 hmr 模式下,對原來的 __webpack_require__ 引入模塊的函數作的一層代理
    // 經過 depModule.parents 和 module.children 來雙向創建起 module 之間的依賴關係
    if (me.hot.active) {
      if (installedModules[request]) {
        if (installedModules[request].parents.indexOf(moduleId) === -1) {
          installedModules[request].parents.push(moduleId); // 創建 module 之間的依賴關係,在被引入的 module 的 module.parents 當中添加當前這個 moduleId 
        }
      } else {
        hotCurrentParents = [moduleId];
        hotCurrentChildModule = request;
      }
      if (me.children.indexOf(request) === -1) {
        me.children.push(request); // 在當前 module 的 module.children 屬性當中添加被引入的 moduleId
      }
    } else {
      console.warn(
        "[HMR] unexpected require(" +
          request +
          ") from disposed module " +
          moduleId
      );
      hotCurrentParents = [];
    }
    return $require$(request); // 引入模塊
  };

  ...

  return fn
}
  1. 提供運行時的hotCreateModule方法,用以給每一個 module 都部署熱更新相關的 api:
function hotCreateModule(moduleId) {
  var hot = {
    // private stuff
    _acceptedDependencies: {},
    _declinedDependencies: {},
    _selfAccepted: false, 
    _selfDeclined: false,
    _disposeHandlers: [],
    _main: hotCurrentChildModule !== moduleId,

    // Module API
    active: true,
    accept: function(dep, callback) {
      if (dep === undefined) hot._selfAccepted = true; // 表示這個 module 能夠進行 hmr
      else if (typeof dep === "function") hot._selfAccepted = dep;
      else if (typeof dep === "object") // 和其餘 module 創建起熱更新之間的關係
        for (var i = 0; i < dep.length; i++)
          hot._acceptedDependencies[dep[i]] = callback || function() {}; 
      else hot._acceptedDependencies[dep] = callback || function() {};
    },
    decline: function(dep) {
      if (dep === undefined) hot._selfDeclined = true; // 當前 module 不須要進行熱更新
      else if (typeof dep === "object") // 當其依賴的 module 發生更新後,並不會觸發這個 module 的熱更新
        for (var i = 0; i < dep.length; i++)
          hot._declinedDependencies[dep[i]] = true;
      else hot._declinedDependencies[dep] = true;
    },
    dispose: function(callback) {
      hot._disposeHandlers.push(callback);
    },
    addDisposeHandler: function(callback) {
      hot._disposeHandlers.push(callback);
    },
    removeDisposeHandler: function(callback) {
      var idx = hot._disposeHandlers.indexOf(callback);
      if (idx >= 0) hot._disposeHandlers.splice(idx, 1);
    },

    // Management API
    check: hotCheck,
    apply: hotApply,
    status: function(l) {
      if (!l) return hotStatus;
      hotStatusHandlers.push(l);
    },
    addStatusHandler: function(l) {
      hotStatusHandlers.push(l);
    },
    removeStatusHandler: function(l) {
      var idx = hotStatusHandlers.indexOf(l);
      if (idx >= 0) hotStatusHandlers.splice(idx, 1);
    },

    //inherit from previous dispose call
    data: hotCurrentModuleData[moduleId]
  };
  hotCurrentChildModule = undefined;
  return hot;
}

在 hotCreateModule 方法當中完成 module.hot.* 和熱更新相關接口的定義。這些 api 也是暴露給用戶部署熱更新代碼的接口。

其中hot.accepthot.decline方法主要是用戶來定義發生熱更新的模塊及其依賴是否須要熱更新的相關策略。例如hot.accept方法用來決定當前模塊所依賴的哪些模塊發生更新的話,自身也須要完成一些更新相關的動做。而hot.decline方法用來決定當前模塊依賴的模塊發生更新後,來決定自身是否須要進行更新。

hot.checkhot.apply兩個方法實際上是 webpack 內部使用的2個方法,其中hot.check方法:首先調用hotDownloadManifest方法,經過發送一個 Get 請求去 server 獲取本次發生變動的相關內容。// TODO: 相關內容的具體格式和字段?

{
  c: { // 發生更新的 chunk 集合
    app: true
  },
  h: 'xxxxx' // 服務端本次生成的編譯hash值,用來做爲下次瀏覽器獲取發生變動的 hash 值(至關於服務端下發的一個 token,瀏覽器拿着這個 token 去後端獲取對應的內容)
}
function hotCheck(apply) {
  if (hotStatus !== "idle") {
    throw new Error("check() is only allowed in idle status");
  }
  hotApplyOnUpdate = apply;
  hotSetStatus("check"); // 更新 熱更新 流程的內部狀態
  return hotDownloadManifest(hotRequestTimeout).then(function(update) {
    if (!update) {
      hotSetStatus("idle");
      return null;
    }
    hotRequestedFilesMap = {};
    hotWaitingFilesMap = {};
    hotAvailableFilesMap = update.c; // 發生更新的 chunk 集合
    hotUpdateNewHash = update.h; // server 下發的本次生成的編譯 hash 值,做爲下次瀏覽器獲取發生變動的 hash 值

    hotSetStatus("prepare");
    var promise = new Promise(function(resolve, reject) {
      hotDeferred = {
        resolve: resolve,
        reject: reject
      };
    });
    hotUpdate = {};
    /*foreachInstalledChunks*/  // 這段註釋在渲染 bootstrap runtime 部分的代碼的時候會經過字符串匹配給替換掉,最終替換後的代碼執行就是對已經下載的 chunk 進行循環 hotEnsureUpdateChunk(chunkId)
    // eslint-disable-next-line no-lone-blocks
    {
      /*globals chunkId */
      hotEnsureUpdateChunk(chunkId); // hotEnsureUpdateChunk(lib/web/JsonpMainTemplate.runtime.js) 方法內部其實就是經過建立 script 標籤,而後傳入到文檔當中完成發生更新的 chunk 的下載
    }
    if (
      hotStatus === "prepare" &&
      hotChunksLoading === 0 &&
      hotWaitingFiles === 0
    ) {
      hotUpdateDownloaded();
    }
    return promise;
  });
}

// TODO: 補一個 hot.check 執行的流程圖
總結下hot.check方法執行的流程其實就是:

  1. 經過 hotDownloadMainfest 方法發送一個 Get 方式的 ajax 請求用以獲取發生更新的 chunk 集合以及本次編譯生成的 hash;
  2. 遍歷已經安裝完成的全部 chunk,找出須要發生更新的 chunk 名,調用 hotEnsureUpdateChunk 方法經過 jsonp 的方式完成發生更新的 chunk 下載。

接下來看下被下載的更新的 chunk 具體內容:

webpackHotUpdate('app', {
  'compiled/module1/path': (function() {
    eval('...script...')
  }),
  'compiled/module2/path': (function() {
    eval('...script...')
  })
})

能夠看到的是返回的 chunk 內容是能夠當即執行的函數:

function hotAddUpdateChunk(chunkId, moreModules) {
  if (!hotAvailableFilesMap[chunkId] || !hotRequestedFilesMap[chunkId])
    return;
  hotRequestedFilesMap[chunkId] = false;
  for (var moduleId in moreModules) {
    if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      hotUpdate[moduleId] = moreModules[moduleId];
    }
  }
  if (--hotWaitingFiles === 0 && hotChunksLoading === 0) {
    hotUpdateDownloaded();
  }
}

對應所作的工做就是將須要更新的模塊緩存至hotUpdate上,同時判斷須要更新的 chunk 是否已經下載完了,若是所有下載完成那麼執行hotUpdateDownloaded方法,其內部實際就是調用hotApply進行接下來進行細粒度的模塊更新和替換的工做。

首先先講下hotApply內部的執行流程:

  1. 遍歷hotUpdate須要更新的模塊,找出和須要更新的模塊有依賴關係的模塊;
function hotApply(options) {
  function getAffectedStuff(updateModuleId) {
    var outdatedModules = [updateModuleId]
    var outdatedDependencies = {}

    var queue = outdatedModules.slice().map(function (id) {
      return {
        chain: [id],
        id: id
      }
    })
    while (queue.length > 0) {
      var queueItem = queue.pop()
      var moduleId = queueItem.id
      var chain = queueItem.chain
      module = installedModules[moduleId] // installedModules 爲在 bootstrap runtime 裏面定義的已經被加載過的 module 集合,這裏其實就是爲了取到這個 module 本身定義部署的有關熱更新的相關策略
      if (!module || module.hot._selfAccepted) continue // 若是這個 module 不存在或者只接受自更新,那麼直接略過接下來的代碼處理
      if (module.hot._selfDeclined) {
        return {
          type: 'self-declined',
          chain: chain,
          moduleId: moduleId
        }
      }
      if (module.hot._main) {
        return {
          type: 'unaccepted',
          chain: chain,
          moduleId: moduleId
        }
      }
      for (var i = 0; i < module.parents.length; i++) { // 遍歷全部依賴這個模塊的 module
        var parentId = module.parents[i]
        var parent = installedModules[parentId]
        if (!parent) continue
        if (parent.hot._declinedDependencies[moduleId]) { // 若是這個 parentModule 的 module.hot._declinedDependencies 裏面設置了不受更新影響的 moduleId
          return {
            type: 'declined',
            chain: chain.concat([parentId]),
            moduleId: moduleId,
            parentId: parentId
          }
        }
        if (outdatedModules.indexOf(parentId) !== -1) continue
        if (parent.hot._acceptedDependencies[moduleId]) { // 若是這個 parentModule 的 module.hot._acceptedDependencies 裏面設置了其受更新影響的 moduleId
          if (!outdatedDependencies[parentId])
            outdatedDependencies[parentId] = []
          addAllToSet(outdatedDependencies[parentId], [moduleId])
          continue
        }
        // 若是這個 parentModule 沒有部署任何相關熱更新的**模塊間依賴的更新策略**(不算_selfAccepted 和 _selfDeclined 狀態),那麼須要將這個 parentModule 加入到 outdatedModules 隊列裏面,同時更新 queue 來進行下一輪的遍歷找出全部須要進行更新的 module
        delete outdatedDependencies[parentId]
        outdatedModules.push(parentId)
        queue.push({
          chain: chain.concat([parentId]),
          id: parentId
        })
      }
    }

    return {
      type: 'accepted',
      moduleId: updateModuleId,
      outdatedModules: outdatedModules, // 本次更新當中全部過時的 modules
      outdatedDependencies: outdatedDependencies // 全部過時的依賴 modules
    }
  }

  for (var id in hotUpdate) {
    if (Object.prototype.hasOwnProperty.call(hotUpdate, id)) {
      moduleId = toModuleId(id)
      /** @type {TODO} */
      var result
      if (hotUpdate[id]) {
        result = getAffectedStuff(moduleId)
      } else {
        result = {
          type: 'disposed',
          moduleId: id
        }
      }
      /** @type {Error|false} */
      var abortError = false
      var doApply = false
      var doDispose = false
      var chainInfo = ''
      if (result.chain) {
        chainInfo = '\nUpdate propagation: ' + result.chain.join(' -> ')
      }
      switch (result.type) {
        case 'self-declined':
          if (options.onDeclined) options.onDeclined(result)
          if (!options.ignoreDeclined)
            abortError = new Error(
              'Aborted because of self decline: ' +
                result.moduleId +
                chainInfo
            )
          break
        case 'declined':
          if (options.onDeclined) options.onDeclined(result)
          if (!options.ignoreDeclined)
            abortError = new Error(
              'Aborted because of declined dependency: ' +
                result.moduleId +
                ' in ' +
                result.parentId +
                chainInfo
            )
          break
        case 'unaccepted':
          if (options.onUnaccepted) options.onUnaccepted(result)
          if (!options.ignoreUnaccepted)
            abortError = new Error(
              'Aborted because ' + moduleId + ' is not accepted' + chainInfo
            )
          break
        case 'accepted':
          if (options.onAccepted) options.onAccepted(result)
          doApply = true
          break
        case 'disposed':
          if (options.onDisposed) options.onDisposed(result)
          doDispose = true
          break
        default:
          throw new Error('Unexception type ' + result.type)
      }
      if (abortError) {
        hotSetStatus('abort')
        return Promise.reject(abortError)
      }
      if (doApply) {
        appliedUpdate[moduleId] = hotUpdate[moduleId] // 須要更新的模塊
        addAllToSet(outdatedModules, result.outdatedModules) // 使用單獨一個 outdatedModules 數組變量存放全部過時須要更新的 moduleId,其中 result.outdatedModules 是經過 getAffectedStuff 方法找到的當前遍歷的 module 所依賴的過時的須要更新的模塊
        for (moduleId in result.outdatedDependencies) { // 使用單獨的 outdatedDependencies 集合去存放相關依賴更新模塊
          if (
            Object.prototype.hasOwnProperty.call(
              result.outdatedDependencies,
              moduleId
            )
          ) {
            if (!outdatedDependencies[moduleId])
              outdatedDependencies[moduleId] = []
            addAllToSet(
              outdatedDependencies[moduleId],
              result.outdatedDependencies[moduleId]
            )
          }
        }
      }
      if (doDispose) {
        addAllToSet(outdatedModules, [result.moduleId])
        appliedUpdate[moduleId] = warnUnexpectedRequire
      }
    }

    // Store self accepted outdated modules to require them later by the module system
    // 在全部 outdatedModules 裏面找到部署了 module.hot._selfAccepted 屬性的模塊。(部署了這個屬性的模塊會經過 webpack 的模塊系統從新加載一次這個模塊的新的內容來完成熱更新)
    var outdatedSelfAcceptedModules = []
    for (i = 0; i < outdatedModules.length; i++) {
      moduleId = outdatedModules[i]
      if (
        installedModules[moduleId] &&
        installedModules[moduleId].hot._selfAccepted
      )
        outdatedSelfAcceptedModules.push({
          module: moduleId,
          errorHandler: installedModules[moduleId].hot._selfAccepted
        })
    }

    // dispose phase TODO: 各個熱更新階段 hooks?

    var idx
    var queue = outdatedModules.slice()
    while (queue.length > 0) {
      moduleId = queue.pop()
      module = installedModules[moduleId]
      if (!module) continue

      var data = {}

      // Call dispose handlers
      var disposeHandlers = module.hot._disposeHandlers
      for (j = 0; j < disposeHandlers.length; j++) {
        cb = disposeHandlers[j]
        cb(data)
      }
      hotCurrentModuleData[moduleId] = data

      // disable module (this disables requires from this module)
      module.hot.active = false

      // 從 installedModules 集合當中剔除掉過時的 module,即其餘 module 引入這個被剔除掉的 module 的時候,實際上是會從新執行這個 module,這也是爲何要從 installedModules 上剔除這個須要被更新的模塊的緣由
      // remove module from cache
      delete installedModules[moduleId]

      // when disposing there is no need to call dispose handler
      delete outdatedDependencies[moduleId]

      // 將這個 module 所依賴的模塊(module.children)當中剔除掉 module.children.parentModule,即解除模塊之間的依賴關係
      // remove "parents" references from all children
      for (j = 0; j < module.children.length; j++) {
        var child = installedModules[module.children[j]]
        if (!child) continue
        idx = child.parents.indexOf(moduleId)
        if (idx >= 0) {
          child.parents.splice(idx, 1)
        }
      }
    }

    // 這裏一樣是經過遍歷 outdatedDependencies 裏面須要更新的模塊,須要注意的是 outdateDependencies 裏面的 key 爲被依賴的 module,這個 key 所對應的 value 數組裏面存放的是發生了更新的 module。因此這是須要解除被依賴的 module 和這些發生更新了的 module 之間的引用依賴關係。
    // remove outdated dependency from module children
    var dependency
    var moduleOutdatedDependencies
    for (moduleId in outdatedDependencies) {
      if (
        Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)
      ) {
        module = installedModules[moduleId]
        if (module) {
          moduleOutdatedDependencies = outdatedDependencies[moduleId]
          for (j = 0; j < moduleOutdatedDependencies.length; j++) {
            dependency = moduleOutdatedDependencies[j]
            idx = module.children.indexOf(dependency)
            if (idx >= 0) module.children.splice(idx, 1)
          }
        }
      }
    }

    // Not in "apply" phase
    hotSetStatus('apply')

    // 更新當前的熱更新 hash 值(即經過 get 請求獲取 server 下發的 hash 值)
    hotCurrentHash = hotUpdateNewHash

    // 遍歷 appliedUpdate 發生更新的 module
    // insert new code
    for (moduleId in appliedUpdate) {
      if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
        modules[moduleId] = appliedUpdate[moduleId] // HIGHLIGHT: 這裏的 modules 變量爲 bootstrap 代碼裏面接收到的全部的 modules 的集合,即在這裏完成新老 module 的替換
      }
    }

    // 執行那些在 module.hot.accept 上部署了依賴模塊發生更新後的回調函數
    // call accept handlers
    var error = null
    for (moduleId in outdatedDependencies) {
      if (
        Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)
      ) {
        module = installedModules[moduleId]
        if (module) {
          moduleOutdatedDependencies = outdatedDependencies[moduleId]
          var callbacks = []
          for (i = 0; i < moduleOutdatedDependencies.length; i++) {
            dependency = moduleOutdatedDependencies[i]
            cb = module.hot._acceptedDependencies[dependency]
            if (cb) {
              if (callbacks.indexOf(cb) !== -1) continue
              callbacks.push(cb)
            }
          }
          for (i = 0; i < callbacks.length; i++) {
            cb = callbacks[i]
            try {
              cb(moduleOutdatedDependencies)
            } catch (err) {
              ...
            }
          }
        }
      }
    }

    // 從新加載那些部署了 module.hot._selfAccepted 爲 true 的 module,即這個 module 會被從新加載並執行一次,這樣也就在 installedModules 上緩存了這個新的 module
    // Load self accepted modules
    for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
      var item = outdatedSelfAcceptedModules[i]
      moduleId = item.module
      hotCurrentParents = [moduleId]
      try {
        $require$(moduleId) // $require$ 會在被最終渲染到 bootstrap runtime 當中被替換爲 webpack require 加載模塊的方法
      } catch (err) {
        if (typeof item.errorHandler === 'function') {
          try {
            item.errorHandler(err)
          } catch (err2) {
            ...
          }
        } else {
          ...
        }
      }

    hotSetStatus('idle')
      return new Promise(function (resolve) {
        resolve(outdatedModules)
      })
    }
  }
}

因此當一個模塊發生變化後,依賴這個模塊的 parentModule 有以下幾種熱更新執行的策略:

module.hot.accept()

當依賴的模塊發生更新後,這個模塊須要經過從新加載去完成本模塊的全量更新。

module.hot.accept(['xxx'], callback)

當依賴的模塊且爲 xxx 模塊發生更新後,這個模塊會執行 callback 來完成相關的更新的動做。而不須要經過從新加載的方式去完成更新。

module.hot.decline()

這個模塊無論其依賴的模塊是否發生了變化。這個模塊都不會發生更新。

module.hot.decline(['xxx'])

當依賴的模塊爲xxx發生更新的狀況下,這個模塊不會發生更新。當依賴的其餘模塊(除了xxx模塊外)發生更新的話,那麼最終仍是會將本模塊從緩存中刪除。

這些熱更新的 api 也是須要用戶本身在代碼當中進行部署的。就拿平時咱們使用的 vue 來講,在本地開發階段, vue sfc 通過 vue-loader 的編譯處理後,會自動幫咱們在組件代碼當中當中注入和熱更新相關的代碼。

// vue-loader/lib/codegen/hotReload.js
const hotReloadAPIPath = JSON.stringify(require.resolve('vue-hot-reload-api'))

const genTemplateHotReloadCode = (id, request) => {
  return `
    module.hot.accept(${request}, function () {
      api.rerender('${id}', {
        render: render,
        staticRenderFns: staticRenderFns
      })
    })
  `.trim()
}

exports.genHotReloadCode = (id, functional, templateRequest) => {
  return `
/* hot reload */
if (module.hot) {
  var api = require(${hotReloadAPIPath})
  api.install(require('vue'))
  if (api.compatible) { // 判斷使用的 vue 的版本是否支持熱更新
    module.hot.accept()
    if (!api.isRecorded('${id}')) {
      api.createRecord('${id}', component.options)
    } else {
      api.${functional ? 'rerender' : 'reload'}('${id}', component.options)
    }
    ${templateRequest ? genTemplateHotReloadCode(id, templateRequest) : ''}
  }
}
  `.trim()
}

vue-loader經過 genHotReloadCode 方法在處理 vue sfc 代碼的時候完成熱更新 api 的部署功能。這裏大體講下 vue component 進行熱更新的流程:

  1. 當這個 vue component 被初次加載的時候,首先執行 module.hot.accept() 方法完成熱更新接口的部署(上文也提到了這個接口執行的策略是會從新加載這個 vue component 來完成熱更新);
  2. 若是這個 vue component 是被初次加載的話,那麼會經過 api.createRecord 方法在全局緩存這個組件的 options 配置,若是這個 vue component 不是被初次加載的話(即全局已經緩存了這個組件的 options 配置),那麼就直接調用 api.reload(或rerender) 方法來進行組件的從新渲染($forceUpdate);
  3. 若是這個 vue component 提供了 template 模板的話,也會部署模板的熱更新代碼(即這個 component 的模板發生了變化,那麼會觸發 api.rerender 方法);
  4. 當這個 vue component 的依賴發生了變化,且這些依賴都部署了熱更新的代碼(若是沒有部署熱更新的代碼的話,可能會直接刷新頁面 TODO:解釋下爲啥會刷新頁面),那麼這個 vue component 會被從新加載一次。對應的會從新進行前面的1,2,3流程。
  5. 在咱們開發 vue 的應用當中,除了修改組件當中的<template><script>中的內容外會進行熱更新外,在咱們修改<style>樣式內容的時候也有熱更新的效果。這也是 vue component 在編譯階段在 vue style block 的代碼當中部署了熱更新代碼的緣由。具體更新策略可參見vue-style-loader

相關資料:

相關文章
相關標籤/搜索