整个 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
编译流程的部分结点进行分析。