整個 watch
的過程經過事件的機制,完成幾個抽象對象的邏輯串聯,當觸發 Watching.prototype.watch
的調用回調函數時,流程便進入到了另一端,開始進行從新編譯,相較於第一次編譯,在 webpack
中在二次編譯階段利用了不少緩存機制,來加速代碼編譯。javascript
文章描述會涉及到整個 webpack
的編譯流程,有一些細節能夠在這篇文章中 Webpack 源碼(二)—— 如何閱讀源碼 詳細的流程描述圖中查看。這裏會針對 webpack
中涉及緩存的部分和狀況進行梳理。html
compilation
初始化在上一篇文章提到過:java
Watching.prototype.watch
經過compiler.watchFileSystem
的watch
方法實現,能夠大體看出在變化觸發編譯後,會執行傳遞的回調函數,最終會調用Watching.prototype.invalidate
進行編譯觸發react
當 Watching.prototype.invalidate
調用後,會再次調用 Watching.prototype._go
方法從新進行編譯流程,而不管在 Watching.prototype._go
方法仍是 Compiler.prototype.run
方法,編譯核心邏輯在 Compiler.prototype.compile
完成。而編譯中第一個緩存設置則就在 Compiler.prototype.compile
中初始化 compilation
中觸發。webpack
webpack/lib/Compiler.js Compiler.prototype.compile = function(callback) { var params = this.newCompilationParams(); this.applyPlugins("compile", params); var compilation = this.newCompilation(params); // ... 省略具體編譯流程 }
關聯前面的 watch
流程,能夠發現,每次編譯開始,也就是每次由 invalidate
-> _go
-> compile
這條邏輯鏈觸發編譯的過程當中,都會生成一個 compilation
對象,而實際上 compilation
對象是每單獨一次編譯的「流程中心」、「數據中心」,從編譯開始、文件輸出到最後的日誌輸出,都關聯在 compilation
上。git
而在 Compiler.prototype.newCompilation
中,則完成了大部分的 webpack
中緩存機制使用的大部分數據github
webpack/lib/Compiler.js Compiler.prototype.createCompilation = function() { return new Compilation(this); }; Compiler.prototype.newCompilation = function(params) { var compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; compilation.contextTimestamps = this.contextTimestamps; // 省略其餘屬性賦值、事件觸發 this.applyPlugins("compilation", compilation, params); return compilation; };
在調用 new Compilation(this)
生成實例以後,開始進行屬性賦值,在 Compiler.prototype.newCompilation
中,主要涉及緩存數據的初始化有兩部分web
webpack/lib/Compiler.js Watching.prototype.watch = function(files, dirs, missing) { this.watcher = this.compiler.watchFileSystem.watch(files, dirs, missing, this.startTime, this.watchOptions, function(err, filesModified, contextModified, missingModified, fileTimestamps, contextTimestamps) { this.watcher = null; if(err) return this.handler(err); this.compiler.fileTimestamps = fileTimestamps; this.compiler.contextTimestamps = contextTimestamps; this.invalidate(); }.bind(this), function() { this.compiler.applyPlugins("invalid"); }.bind(this)); };
這部分是緊接着完成編譯以後,將 Watching.prototype.watch
回調函數中 this.compiler.fileTimestamps = fileTimestamps;
、this.compiler.contextTimestamps = contextTimestamps;
文件(夾)監聽底層的 fileTimestamps
、contextTimestamps
數據賦值到新生成的 compilation
上。數組
這兩個值,在編譯時觸發編譯模塊實例判斷是否須要從新編譯的 needRebuild
方法中起到做用。緩存
第三個部分的入口是觸發webpack
編譯流程中的 compilation
事件,事件觸發主要引發 CachePlugin
插件邏輯的加載。
在 watch
過程當中,會發現一個規律是,編譯時間在編譯第一次以後,後面的編譯會增長不少,緣由是 watch
模式正在流程中,會默認開啓 cache
配置。在 webpack
中 cache
選項則是對應 CachePlugin
的加載:
webpack/lib/WebpackOptionsApply.js if(options.cache === undefined ? options.watch : options.cache) { var CachePlugin = require("./CachePlugin"); compiler.apply(new CachePlugin(typeof options.cache === "object" ? options.cache : null)); }
那麼在 CachePlugin
中對於 watch
流程中,最重要的一段邏輯則是將 CachePlugin
的 cache
屬性與當前編譯 compilation
對象進行關聯
webpack/lib/CachePlugin.js compiler.plugin("compilation", function(compilation) { compilation.cache = this.cache; }.bind(this));
這樣操做以後,編譯過程 compilation
中的緩存設置,因爲是引用的關係則會使 CachePlugin
的 cache
屬性也保持同步。
同時,在完成一次編譯後觸發變動開始下一次編譯的時候,上一次編譯完成後更新完成的 cache
結果經過 compilation
事件的觸發,就能無縫的銜接到下一次的 compilation
對象上,經過 CachePlugin
完成緩存在每次編譯流程中的同步。
在後續環節中,對於文件更新判斷,每每基於 contextTimestamps
、fileTimestamps
,而對於緩存的存儲,則大可能是放在由 cachePlugin
初始化在 compilation
對象中的 cache
屬性上。
webpack
編譯流程中,時刻都在處理着文件路徑問題,其中不管是編譯某一個文件,仍是調用某一個 loader
,都須要從配置的各類狀況(多是相對路徑、絕對路徑以及簡寫等狀況)的路徑中找到實際文件對應的絕對路徑。而這裏牽涉到一些耗時的操做,例如會對不一樣的文件夾類型、文件類型,以及一些 resolve
的配置進行處理。
這裏經過在 compiler.resolvers
中的三個 Resolver
實例加載 UnsafeCachePlugin
來針對路徑查找進行結果緩存,在相同狀況(request)下,經過緩存直接返回。
webpack/lib/WebpackOptionsApply.js compiler.resolvers.normal.apply( new UnsafeCachePlugin(options.resolve.unsafeCache), // 省略其餘插件加載 ); compiler.resolvers.context.apply( new UnsafeCachePlugin(options.resolve.unsafeCache), // 省略其餘插件加載 ); compiler.resolvers.loader.apply( new UnsafeCachePlugin(options.resolve.unsafeCache), // 省略其餘插件加載 );
分別針對處理編譯文件路徑查找的 normal
、處理文件夾路徑查找的 context
以及 loader
文件路徑查找的 loader
都加載了 UnsafeCachePlugin
插件。
enhanced-resolve/lib/UnsafeCachePlugin.js UnsafeCachePlugin.prototype.apply = function(resolver) { var oldResolve = resolver.resolve; var regExps = this.regExps; var cache = this.cache; resolver.resolve = function resolve(context, request, callback) { var id = context + "->" + request; if(cache[id]) { // From cache return callback(null, cache[id]); } oldResolve.call(resolver, context, request, function(err, result) { if(err) return callback(err); var doCache = regExps.some(function(regExp) { return regExp.test(result.path); }); if(!doCache) return callback(null, result); callback(null, cache[id] = result); }); }; };
UnsafeCachePlugin
在這裏會直接執行 UnsafeCachePlugin.prototype.apply
方法會重寫原有 Resolver
實例的 resolve
方法,會加載一層路徑結果 cache
,以及在完成原有方法後更新 cache
當調用 resolver.resolve
時,會首先判斷是否在 UnsafeCachePlugin
實例的 cache
屬性中已經存在結果,存在則直接返回,不存在則執行原有 resolve
方法
當原有 resolve
方法完成後,會根據加載 UnsafeCachePlugin
時傳入的 regExps
來判斷是否須要緩存,若是須要則經過 callback(null, cache[id] = result);
返回結果的同時,更新UnsafeCachePlugin
的 cache
緩存對象。
在完成了編譯文件路徑查找以後,即將開始對文件進行編譯,由輸入輸出來看能夠粗略的當作字符串轉換流程,而這個流程是 webpack
中最耗時的流程,webpack
在開始實際的 loader
處理編譯以前,進行是否已有緩存的判斷。
webpack/lib/Compilation.js Compilation.prototype.addModule = function(module, cacheGroup) { cacheGroup = cacheGroup || "m"; var identifier = module.identifier(); if(this.cache && this.cache[cacheGroup + identifier]) { var cacheModule = this.cache[cacheGroup + identifier]; var rebuild = true; if(!cacheModule.error && cacheModule.cacheable && this.fileTimestamps && this.contextTimestamps) { rebuild = cacheModule.needRebuild(this.fileTimestamps, this.contextTimestamps); } if(!rebuild) { cacheModule.disconnect(); this._modules[identifier] = cacheModule; this.modules.push(cacheModule); cacheModule.errors.forEach(function(err) { this.errors.push(err); }, this); cacheModule.warnings.forEach(function(err) { this.warnings.push(err); }, this); return cacheModule; } else { module.lastId = cacheModule.id; } } //省略緩存不存在的處理 };
這裏有一個上下文是,每個完成路徑查找以後的編譯文件,會生成對應的一個邏輯編譯模塊 module
,而編譯過程當中的每個編譯模塊,都會關聯到 compilation
上的 modules
數組中。
執行 addModule
的時機正式完成路徑查找生成模塊以後,完成 compilation
添加 module
的過程。
首先調用 module.identifier();
得到編譯文件的絕對路徑,賦值爲 identifier
,而且以 cacheGroup + identifier
爲 存儲的 key
,在 cacheGroup
值以及自定義 loader
參數不變的狀況下,cache
對象中的模塊緩存就由文件的絕對路徑保證惟一性。
而後判斷是否已經生成過該路徑的 module
, this.cache && this.cache[cacheGroup + identifier]
判斷是否須要從新編譯
var rebuild = true; if(!cacheModule.error && cacheModule.cacheable && this.fileTimestamps && this.contextTimestamps) { rebuild = cacheModule.needRebuild(this.fileTimestamps, this.contextTimestamps); }
在進入 cacaheModule.needRebuild
以前,有四個前置條件
cacheModule.error
:模塊編譯過程出現錯誤,則會將錯誤對象複製到 module
的 error
屬性上
cacheModule.cacheable
:模塊是否能緩存,在一些不能緩存的狀況,例如在編譯過程增長對其餘未添加到 module
的 fileDependencies
的文件依賴,依賴文件變動,可是引用原文件沒有變動。在 loader
的函數中調用 this.cacheable()
實際上就是申明設置編譯能夠緩存。後續還會詳細提到。
this.fileTimestamps
、this.contextTimestamps
:首次活或前一次編譯存儲的文件最後變動記錄
在前置條件知足的狀況下,進入 module
的 needRebuild
方法,根據前置條件參數進行邏輯判斷
webpack/lib/NormalModule.js NormalModule.prototype.needRebuild = function needRebuild(fileTimestamps, contextTimestamps) { var timestamp = 0; this.fileDependencies.forEach(function(file) { var ts = fileTimestamps[file]; if(!ts) timestamp = Infinity; if(ts > timestamp) timestamp = ts; }); this.contextDependencies.forEach(function(context) { var ts = contextTimestamps[context]; if(!ts) timestamp = Infinity; if(ts > timestamp) timestamp = ts; }); return timestamp >= this.buildTimestamp; };
這裏以 NormalModule
爲例,會針對 this.fileDependencies
、this.contextDependencies
進行相同邏輯的判斷。
fileDependencies
指的是編譯 module
所關聯的文件依賴,通常會包含模塊初始化傳入的本來編譯文件,也可能包含經過在 loader
中調用 this.addDependency
增長的其餘的文件依賴,例如在樣式文件中的 import
語法引入的文件,在模塊邏輯上,模塊以入口樣式文件爲入口做爲標識,以 import
進入的樣式文件爲 fileDependency
。
contextDependencies
相似,是 module
關聯的文件夾依賴,例如在 WatchMissingNodeModulesPlugin 實現中就是對 contextDependencies
操做,完成對目標目錄的監聽。
var ts = contextTimestamps[context]; if(!ts) timestamp = Infinity; if(ts > timestamp) timestamp = ts;
經過這段通用邏輯獲取兩類依賴的最後變動時間的最大值,與上次構建時間(buildTimestamp
)比較 return timestamp >= this.buildTimestamp;
判斷是否須要從新編譯。那麼若是最後變動時間大於模塊自己上次的編譯時間,則代表須要從新編譯。
若是判斷緩存過時失效,則須要進行編譯。在編譯流程中,會看到不少 loader
會有 this.cacheable();
調用,一樣也會看到 this.addDependency
或 this.dependency
以及不多見的 this.addContextDependency
;同時也會在 module
和 compilation
裏面看到兩個常見的變量 fileDependencies
、contextDependencies
。下面會進行一些深刻。
承接上面提到在判斷是否須要從新編譯時的條件 cacheModule.cacheable
,上面提到
每個完成路徑查找以後的編譯文件,會生成對應的一個邏輯編譯模塊
module
換一種較爲好理解的方式,在通常狀況下,每個 require(dep)
依賴,在 webpack
中都會生成與之對應的 module
,其中以 module.request
爲惟一標識,而 module.request
就是爲 dep
在文件系統中的 路徑
和 編譯參數
的拼接字符串。
這裏的 cacheModule.cacheable
就是模塊的 cacheable
屬性,代表 module
當前對應的文件以及編譯參數(request
)上下文的狀況下能夠進行緩存。
拿常見的 less-loader
舉例子
less-loader/index.js module.exports = function(source) { var loaderContext = this; var query = loaderUtils.parseQuery(this.query); var cb = this.async(); // 省略其餘配置操做 this.cacheable && this.cacheable(); }
首先肯定 this
指向,less-loader
代碼中,其實有一句進行了說明 var loaderContext = this;
,在 loader
文件邏輯中,this
綁定的是上層 module
建立的 loaderContext
對象
webpack-core/lib/NormalModuleMixin.js var loaderContextCacheable; var loaderContext = { cacheable: function(flag) { loaderContextCacheable = flag !== false; }, dependency: function(file) { this.fileDependencies.push(file); }.bind(this), addDependency: function(file) { this.fileDependencies.push(file); }.bind(this), addContextDependency: function(context) { this.contextDependencies.push(context); }.bind(this), // 省略其餘屬性 }
這裏列了 loaderContext
其中的一些與目前討論話題相關的屬性,能夠看到 cacheable
其實是經過閉包來修改 loaderContextCacheable
這個變量的值,而 loaderContextCacheable
是最終影響 module.cacheable
的決定因素。
module.cacheable
webpack
提供給 loader
模塊兩個接口,一個是默認 module.exports
的導出方法,一個是 module.exports.pitch
的導出方法,對應兩套不一樣的邏輯。按照在 webpack
中執行順序
module.exports.pitch
導出方法邏輯
webpack-core/lib/NormalModuleMixin.js // Load and pitch loaders (function loadPitch() { // 省略其餘判斷、處理邏輯 loaderContextCacheable = false; runSyncOrAsync(l.module.pitch, privateLoaderContext, [remaining.join("!"), pitchedLoaders.join("!"), l.data = {}], function(err) { if(!loaderContextCacheable) this.cacheable = false; if(args.length > 0) { nextLoader.apply(this, [null].concat(args)); } else { loadPitch.call(this); } }.bind(this)); }.call(this));
runSyncOrAsync
是執行 loader
具體實現的函數,在開始 pitch
流程以前,會首先設置 loaderContextCacheable
爲 false
,而後經過 runSyncOrAsync
進入 loader
的具體 pitch
實現,這樣只有在 loader
方法中手動調用 this.cacheable()
纔會將保證loaderContextCacheable
的值設置成 true
從而不會進入 if(!loaderContextCacheable) this.cacheable = false;
,標明 module
的 cacheable
爲 false
。
module.exports
導出方法邏輯
webpack-core/lib/NormalModuleMixin.js function nextLoader(err/*, paramBuffer1, param2, ...*/) { if(!loaderContextCacheable) module.cacheable = false; // 省略 privateLoaderContext 環境建立 loaderContextCacheable = false; runSyncOrAsync(l.module, privateLoaderContext, args, function() { loaderContext.inputValue = privateLoaderContext.value; nextLoader.apply(null, arguments); }); }
在完成 pitch
流程以後,會進入默認邏輯的流程,也相似 pitch
的流程,在調用 runSyncOrAsync
進入 loader
邏輯前,先設置 loaderContextCacheable
爲 false
,在遞歸循環中判斷 loader
是否在執行中調用 this.cacheable()
將 loaderContextCacheable
設置成 true
,從而保證module.cacheable
的值爲 true
。
綜合上面的環節,就是若是要保證 module
可被緩存,則必定須要 loader
中調用 this.cacheable()
觸發如圖的邏輯鏈路。
addDependency
、dependency
、addContextDependency
在 loaderContext
還會提供兩類方法
增長文件依賴,addDependency
、dependency
:目的是在編譯過程當中,增長對沒有生成對應 module
的文件的依賴關係,例如 import common.less
這樣的引用文件
增長文件夾依賴,addContextDependency
:類比文件依賴,增長對文件夾的依賴
而從上面的實現中,能夠看到,兩類方法調用以後,會將文件(夾)路徑放在 fileDependencies
,contextDependencies
中
fileDependencies
、contextDependencies
與 compilation
在完成全部模塊的編譯以後,在 Compilation.js
中會調用 Compilation.prototype.summerizeDependencies
,其中會將 fileDependencies
、contextDependencies
聚集到 compilation
實例上
webpack/lib/Compilation.js Compilation.prototype.summarizeDependencies = function summarizeDependencies() { this.modules.forEach(function(module) { if(module.fileDependencies) { module.fileDependencies.forEach(function(item) { this.fileDependencies.push(item); }, this); } if(module.contextDependencies) { module.contextDependencies.forEach(function(item) { this.contextDependencies.push(item); }, this); } }, this); this.fileDependencies.sort(); this.fileDependencies = filterDups(this.fileDependencies); this.contextDependencies.sort(); this.contextDependencies = filterDups(this.contextDependencies); // 省略其餘操做 };
從實現中能夠看到,首先把全部編譯 module
的 fileDependencies
與 contextDependencies
都聚集到 compilation
對象,而且進行排序、去重。
可是可能看到這裏關於這兩個 dependency
的內容有個疑問,跟緩存更新有啥關係呢?
watch
流程webpack/lib/Compiler.js Watching.prototype._done = function(err, compilation) { // 省略其餘流程 if(!this.error) this.watch(compilation.fileDependencies, compilation.contextDependencies, compilation.missingDependencies); };
銜接上篇文章,在 watch
模式下,在完成編譯以後,傳入 watch
方法正是上面 Compilation.prototype.summarizeDependencies
聚集到 compilation
中的 fileDependencies
、contextDependencies
屬性,代表上一次編譯結果中得出的做爲 編譯流程中的文件(夾)依賴做爲須要進行變動監聽的依據。
整個流程下來,就能將編譯中涉及的文件進行管控,在下一次編譯觸發監控中,保證對涉及文件的監控,快速響應文件改動變動。
在完成了以前的編譯邏輯以後,webpack
便開始要渲染(render
) 代碼,而這個拼接過程,是字符串不斷分割拼接的過程,對應一樣的輸入得到一樣的輸出。webpack
在這裏也一樣設置了一個緩存機制
webpack/lib/Compilation.js Compilation.prototype.createChunkAssets = function createChunkAssets() { // 省略其餘邏輯 for(i = 0; i < this.chunks.length; i++) { var useChunkHash = !chunk.entry || (this.mainTemplate.useChunkHash && this.mainTemplate.useChunkHash(chunk)); var usedHash = useChunkHash ? chunkHash : this.fullHash; if(this.cache && this.cache["c" + chunk.id] && this.cache["c" + chunk.id].hash === usedHash) { source = this.cache["c" + chunk.id].source; } else { if(chunk.entry) { source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates); } else { source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates); } if(this.cache) { this.cache["c" + chunk.id] = { hash: usedHash, source: source = (source instanceof CachedSource ? source : new CachedSource(source)) }; } } } };
在 Compilation.prototype.createChunkAssets
中,會判斷每一個 chunk
是否有代碼生成以後保留的緩存
這裏的 chunk
簡化來說,能夠看作對應的是配置在 webpack
中的 entry
。
從 this.cache && this.cache["c" + chunk.id] && this.cache["c" + chunk.id].hash === usedHash
看出,以 chunk.id
爲標識,若是整個 chunk
的 webpack
生成 hash
沒有變化,說明在 chunk
中的各個 module
等參數都沒有發生變化。則可使用上一次的代碼渲染緩存。
同時若是緩存失效,則會將生成以後的代碼儲存在 this.cache["c" + chunk.id]
對象中。
webpack
中的緩存機制保證了在屢次編譯的場景下,以增量變動編譯的方式保證編譯速度。文章內容大體截取了 webpack
編譯流程的部分結點進行分析。