This article first appeared on blog.flqin.com. If you have any errors, please contact the author. Analysis code word is not easy, reproduced please indicate the source, thank you!

The normal main flow build of WebPack was examined earlier, and by setting the Watch mode, WebPack can listen for file changes and recompile when they are modified. The document

Watch mode is enabled by default in Webpack-dev-server and Webpack-dev-Middleware.

Next, set the CLI command and add –watch to analyze the main process in watch mode (mode = development).

The first building

Resources to build

After executing the code, similar to the main process, execute the preparation before compiling described in the previous article -> go back to cli.js, read the watch configuration such as options.watchOptions, and then go to compiler.watch:

/ /...
compiler.watch(watchOptions, compilerCallback);
Copy the code

After this method initializes some properties, a new Watching instance is returned.

In Watching instantiation (webpack/lib/ watch.js), after processing watchOptions, execute _go in compiler. ReadRecords callback:

//Watching.js
this.compiler.readRecords((err) = > {
  / /...
  this._go();
});
Copy the code

_go is similar to run in Compiler. In _go, compiler.hooks:watchRun is triggered, and CachePlugin is executed to set this.watching = true. As with the normal WebPack build, compiler.compile is executed in the hook watchRun callback to start the build and onCompiled is executed after the resource build.

// Watching.js
_go(){
  / /...
  this.compiler.hooks.watchRun.callAsync(this.compiler, err= > {
    / /...
    const onCompiled = (err, compilation) = > {
  / /...
    };
  this.compiler.compile(onCompiled); }}Copy the code

OnCompiled is roughly the same as onCompiled in Compiler. run, except that all callbacks are changed from finalCallback to _done and stats statistics are compiled in _done:

/ /... Watching.js
_done(err, compilation){
  / /...
  this.compiler.hooks.done.callAsync(stats, () = > {
    this.handler(null, stats); / / with compilerCallback
    if (!this.closed) {
      this.watch(
        Array.from(compilation.fileDependencies),
        Array.from(compilation.contextDependencies),
        Array.from(compilation.missingDependencies));
    }
    for (const cb of this.callbacks) cb();
    this.callbacks.length = 0;
  });
}

Copy the code

After stats is set in this method, compiler.hooks: done callback will execute this.handler (actually finalCallback) to compilerCallback, which will print build related information in the CLI. At this point, the initial build is complete.

Add to monitor

This. Watch and fileDependencies, contextDependencies, MissingDependencies (compilation. In this seal. SummarizeDependencies generated) the need to monitor the files and directories.

This. Watch that executes this.com piler. WatchFileSystem. Watch case watch NodeWatchFileSystem method (file Webpack/lib/node/NodeWatchFileSystem js, NodeEnvironmentPlugin set), the method of parameters for the first format after judgment, instantiate the Watchpack. Watchpack inherited EventEmitter from the Events module, and then registered change, aggregated events on this.watcher (Watchpack instance), Execute the watchpack instance method watch, which executes:

//watchpack.js
/ /...
this.fileWatchers = files.map(function (file) {
  return this._fileWatcher(file, watcherManager.watchFile(file, this.watcherOptions, startTime));
}, this);
this.dirWatchers = directories.map(function (dir) {
  return this._dirWatcher(dir, watcherManager.watchDirectory(dir, this.watcherOptions, startTime));
}, this);
Copy the code

This loop executes this._filewatcher on each file.

The normal listener only involves this._fileWatchers. The directory class this._dirWatchers will be listened in require.context.

Watchermanager. watchFile:

//watcherManager.js
var directory = path.dirname(p);
return this.getDirectoryWatcher(directory, options).watch(p, startTime);
Copy the code

GetDirectoryWatcher instantiates DirectoryWatcher based on the corresponding directory directory and executes the watch method.

DirectoryWatcher, like Watchpack, inherits EventEmitter from the Events module and executes during instantiation:

//DirectoryWatcher.js
this.watcher = chokidar.watch(directoryPath, {
  ignoreInitial: true.persistent: true.followSymlinks: false.depth: 0.atomic: false.alwaysStat: true.ignorePermissionErrors: true.ignored: options.ignored,
  usePolling: options.poll ? true : undefined.interval: interval, Options. poll Indicates the polling interval of the file system. The larger the poll interval, the better the performance
  binaryInterval: interval,
  disableGlobbing: true});Copy the code

Webpack uses the NPM package Chokidar to listen for folders, and then bind events according to different operations (add, delete, modify, etc.). Run this.doInitialScan to read all files and folders under the path (directory corresponding to the file). If it is a file, run this.setFileTime to collect the modification time of the file according to whether it is the first watch. If it is a folder, this. SetDirectory is called to record all subpaths.

This. GetDirectoryWatcher (directory, options). Watch (p, startTime)

/ /... DirectoryWatcher.js
var watcher = new Watcher(this, filePath, startTime);
Copy the code

The Watcher class still inherits EventEmitter from the Events module. Here we instantiate a Watcher, subscribe to its close method, push the Watcher to this.watchers, and return an instance of Watcher.

Then back to:

//watchpack.js
/ /...
return this._fileWatcher(file, watcherManager.watchFile(file, this.watcherOptions, startTime)); // watcherManager.watchfile returns a watcher instance
Copy the code

The this._fileWatcher is executed to subscribe to the change and remove events for the corresponding watcher. Finally this.FileWatchers gets a Watcher array.

Then we go back to _done, and this round of code execution is over.

Then switch to the asynchronous callback of fs.readdir in doInitialScan to collect the file modification time, and by the end of the initial build of this Webpack Watch, the file is being listened on.

Modify file to trigger listening

After modifying the file, the change event of Chokidar, this.onChange, is raised. After verifying the path in the method, this.setFileTime is executed. After updating this. Files [filePath] method with the last modification time, execute:

//DirectoryWatcher.js
if (this.watchers[withoutCase(filePath)]) {
  this.watchers[withoutCase(filePath)].forEach(function (w) {
    w.emit('change', mtime, type);
  });
}
Copy the code

After checking whether the file was in the “this.watchers” column, trigger its change for each watcher of the file, execute the event registered in _fileWatcher:

//watchpack.js
this._onChange(file, mtime, file, type);
Copy the code

Method to execute:

//watchpack.js
this.emit('change', file, mtime); / / trigger ` this.com piler. WatchFileSystem. Watch ` callback: this.com piler. Hooks. Invalid. Call (fileName, changeTime)
if (this.aggregateTimeout) clearTimeout(this.aggregateTimeout);
if (this.aggregatedChanges.indexOf(item) < 0) this.aggregatedChanges.push(item);
this.aggregateTimeout = setTimeout(this._onTimeout, this.options.aggregateTimeout);
Copy the code

Function debounce. You can set the interval by setting the options.aggregateTimeout configuration item. The longer the interval, the better the performance.

Execute information about this._onTimeout to trigger aggregated events (registered in NodeWatchFileSystem), then:

//NodeWatchFileSystem.js
const times = objectToMap(this.watcher.getTimes());
Copy the code

Get times:

{
  / /... Structure of the map
  "0": {
    "key": "/Users/github/webpack-demo/src/a.js"."value": "1578382937093"
  },
  "1": {
    "key": "/Users/github/webpack-demo/src/a.js"."value": "1578382937093"
  },
  "2": {
    "key": "/Users/github/webpack-demo/src/a.js"."value": "1578382937093"
  },
  "3": {
    "key": "/Users/github/webpack-demo/src/a.js"."value": "1578382937093"
  },
  "4": {
    "key": "/Users/github/webpack-demo/src/a.js"."value": "1578382937093"
  },
  "5": {
    "key": "/Users/github/webpack-demo/src/a.js"."value": "1578382937093"}}Copy the code

After the latest modification time of each file, perform the callback callback, namely Watching. Js this.com piler. WatchFileSystem. Watch the penultimate parameters method, In the method to assign fileTimestamps times namely this.com piler. After fileTimestamps, execute this. _invalidate that executes this. _go to open a new building.

Watch optimization

In the moduleFactory.create callback (including factory.create as in addModuleDependencies), execute:

const addModuleResult = this.addModule(module);
Copy the code

In addition to checking that the Module is loaded, the compilation may also check if the module exists in this. Cache.

let rebuild = true;
if (this.fileTimestamps && this.contextTimestamps) {
  rebuild = cacheModule.needRebuild(this.fileTimestamps, this.contextTimestamps);
}
Copy the code

Get (file) and build time this.buildTimestamp (obtained at module.build) to determine whether the module needs to be rebuilt. If the modification time is longer than the build time, rebuild is required. Otherwise, skip build and run afterBuild to resolve recursive build dependencies. This greatly improves the build process by rebuilding only modified modules when listening.