.14-淺析webpack源碼之Watchpack模塊

来源:http://www.cnblogs.com/QH-Jimmy/archive/2017/12/20/8073713.html
-Advertisement-
Play Games

解決掉了最頭疼的DirectoryWatcher內部實現,這一節可以結束NodeWatchFileSystem模塊。 關於watch的應用場景,仔細思考了下,這不就是熱重載的核心嘛。 首先是監視文件,觸發文件change事件後收集變動文件信息,重新進行打包,更新JS後觸發頁面重新渲染,perfect ...


  解決掉了最頭疼的DirectoryWatcher內部實現,這一節可以結束NodeWatchFileSystem模塊。

  關於watch的應用場景,仔細思考了下,這不就是熱重載的核心嘛。

  首先是監視文件,觸發文件change事件後收集變動文件信息,重新進行打包,更新JS後觸發頁面重新渲染,perfect!

 

  首先重新回憶一下NodeWatchFileSystem模塊:

"use strict";

const Watchpack = require("watchpack");

class NodeWatchFileSystem {
    constructor(inputFileSystem) {
        this.inputFileSystem = inputFileSystem;
        this.watcherOptions = {
            aggregateTimeout: 0
        };
        this.watcher = new Watchpack(this.watcherOptions);
    }
    watch(files, dirs, missing, startTime, options, callback, callbackUndelayed) {
        // ...
        const oldWatcher = this.watcher;
        this.watcher = new Watchpack(options);
        // 當新監視器生成時立即調用的函數
        if (callbackUndelayed)
            this.watcher.once("change", callbackUndelayed);
        // callback在這裡調用
        this.watcher.once("aggregated", (changes, removals) => { /**/ });
        // 分別傳入 文件目錄數組 文件夾目錄數組 時間標記
        this.watcher.watch(files.concat(missing), dirs.concat(missing), startTime);
        // 關閉舊監視器
        if (oldWatcher) {
            oldWatcher.close();
        }
        return {
            close: () => {
                if (this.watcher) {
                    this.watcher.close();
                    this.watcher = null;
                }
            },
            pause: () => {
                if (this.watcher) {
                    this.watcher.pause();
                }
            }
        };
    }
}

module.exports = NodeWatchFileSystem;

  在構造函數中會初始化一個Watchpack實例賦給watcher,每一次調用watch方法會重新生成一個的watcher並同時關閉舊的watcher。

  模塊只有一個原型方法watch,參數解析如下:

1.files、dirs、missing為文件路徑相關

2.options為初始化watchpack實例的參數

3.callback與callbackUndelayed為回調函數

  這裡有兩個回調函數,一個在新監視器生成時立即調用,一個在監視器觸發aggregated事件時調用。

  返回的對象有兩個方法,一個用來關閉監視器,一個用來暫停監視器。

 

  下麵看Watchpack模塊源碼,先從構造函數開始講解:

class Watchpack {
    constructor(options) {
        EventEmitter.call(this);
        // 參數處理
        if (!options) options = {};
        // 設置定時器參數預設值
        if (!options.aggregateTimeout) options.aggregateTimeout = 200;
        this.options = options;
        this.watcherOptions = {
            ignored: options.ignored,
            poll: options.poll
        };
        // 文件監視器容器
        this.fileWatchers = [];
        // 文件夾監視器容器
        this.dirWatchers = [];
        // 指定文件修改時間容器
        this.mtimes = Object.create(null);
        // 暫停標記
        this.paused = false;
        // 定時器ID收集容器
        this.aggregatedChanges = [];
        this.aggregatedRemovals = [];
        // 本地定時器參數
        this.aggregateTimeout = 0;
        this._onTimeout = this._onTimeout.bind(this);
    };
    // prototype methods...
}

  大體上可分為容器、標記、參數三部分。

  容器包括文件與文件夾的監視器容器以及幾個定時器ID相關的容器,標記只有一個暫停標記,參數為定時器的時間參數。

  下麵是核心方法watch,源碼整理如下:

Watchpack.prototype.watch = function watch(files, directories, startTime) {
    // 暫停標記置false
    this.paused = false;
    // 取出舊的監視器
    var oldFileWatchers = this.fileWatchers;
    var oldDirWatchers = this.dirWatchers;
    // 分別調用watchFile與watchDirectory對文件與文件夾進行監視
    // 將監視器賦值給對應的容器
    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);
    // 關閉所有舊的監視器
    oldFileWatchers.forEach(function(w) {
        w.close();
    }, this);
    oldDirWatchers.forEach(function(w) {
        w.close();
    }, this);
};

  過了watchManager模塊後,這裡就變得十分簡單明瞭,分別取出數組的目錄元素,分別進行監視操作,將返回的監視器數組賦值給容器。

  同樣,每一次調用watch會關閉所有舊的監視器。

 

  接下里是關於file與dir的不同處理:

_fileWatcher

// 傳入文件路徑與watcher 
Watchpack.prototype._fileWatcher = function _fileWatcher(file, watcher) {
    // 綁定change、remove事件的觸發事件
    watcher.on("change", function(mtime, type) {
        this._onChange(file, mtime, file, type);
    }.bind(this));
    watcher.on("remove", function(type) {
        this._onRemove(file, file, type);
    }.bind(this));
    return watcher;
};

  如果看了上一節會發現,DirectoryWatcher模塊內部源碼只有emit觸發事件,並沒有任何on來處理事件。

  這裡就是處理模塊內部事件觸發的地方,觸發change調用本地的_onchange方法,觸發remove調用本地的_onRemove方法,參數沒有什麼解釋的。

_dirWatcher

Watchpack.prototype._dirWatcher = function _dirWatcher(item, watcher) {
    // 只是觸發change事件
    watcher.on("change", function(file, mtime, type) {
        this._onChange(item, mtime, file, type);
    }.bind(this));
    return watcher;
};

  文件夾只有增加和刪除,一個change事件就足夠了。

_onChange

// item、file都是文件路徑
Watchpack.prototype._onChange = function _onChange(item, mtime, file) {
    file = file || item;
    // 新增或更新對應文件的修改時間
    this.mtimes[file] = mtime;
    // 暫停時不觸發change事件
    if (this.paused) return;
    this.emit("change", file, mtime);
    // 清除本地定時器
    if (this.aggregateTimeout)
        clearTimeout(this.aggregateTimeout);
    // 變動文件
    if (this.aggregatedChanges.indexOf(item) < 0)
        this.aggregatedChanges.push(item);
    // 設置定時器
    this.aggregateTimeout = setTimeout(this._onTimeout, this.options.aggregateTimeout);
};

  變動的文件信息會被加入到對應的容器,最後會調用一個定時器,定時器間隔為穿進來的參數。

_onRemove

Watchpack.prototype._onRemove = function _onRemove(item, file) {
    file = file || item;
    // 刪除容器中對應的文件信息
    delete this.mtimes[item];
    if (this.paused) return;
    // 觸發remove事件
    this.emit("remove", item);
    if (this.aggregateTimeout)
        clearTimeout(this.aggregateTimeout);
    // 刪除文件的信息加入容器
    if (this.aggregatedRemovals.indexOf(item) < 0)
        this.aggregatedRemovals.push(item);
    // 觸發aggregated事件
    this.aggregateTimeout = setTimeout(this._onTimeout, this.options.aggregateTimeout);
};

_onTimeout

Watchpack.prototype._onTimeout = function _onTimeout() {
    // 定時器ID置0
    this.aggregateTimeout = 0;
    // 變動與刪除的文件信息數組
    var changes = this.aggregatedChanges;
    var removals = this.aggregatedRemovals;
    // 清空
    this.aggregatedChanges = [];
    this.aggregatedRemovals = [];
    // 觸發aggregated事件
    this.emit("aggregated", changes, removals);
};

  簡單概括就是會在給定時間後調觸發aggregated事件,將變動與刪除的文件信息數組作為參數傳遞出去並清空數組。

 

  總體來說,文件的增加與內容修改會觸發change事件,刪除會觸發remove事件。文件夾只有change事件。無論是觸發change還是remove,都會將對應的文件信息用aggregated事件傳遞出去。

  調用pause方法時,所有的操作將不會觸發任何事件,但是文件修改信息仍然會被收集。

  值得註意的是,源碼內部並沒有任何繼續監視的方法,雖然有一個resume函數,但是:

Watchpack.prototype.close = function resume() { /**/ }

  看到沒,假的,雖然名字叫resume,但是實際上關掉了監視。繼續監視唯一的辦法是重新調用watch方法,但是會清空所有watcher容器並重新生成一批新的。也就是說,pause相當於stop。

  源碼中還有個getTimes的原型方法,有興趣自己去看,暫時不講了。

 

  最後來用小案例模擬這些模塊的使用,目錄如圖:

  測試代碼如下:

// 模塊引入
// 我都複製過來了!
const Watchpack = require('./lib/watchpack');
const fs = require('fs');
const path = require('path');
// 實例化一個Watchpack類 不傳參
const el = new Watchpack();
// 需要監視的文件夾
const rootPath = path.join(process.cwd(), 'test');
fs.readdir(rootPath, (err, items) => {
    // 文件夾中的文件全部做監視
    items = items.map((v) => path.join(rootPath, v));
    // 對所有文件做監視
    el.watch(items, [], 1);
});
// 監視change事件
el.on('change', (...args) => {
    console.log('Detect file change\nthe filename is:' + args[0] + '\nthe filename mtime is:' + args[1]);
});
// 監視remove事件
el.on('remove', (...args) => {
    console.log('Detect file remove\nthe filename is:' + args[0]);
});

  這裡暫時先不對文件夾進行監視,遍歷test文件夾,將所有文件路徑包裝成數組傳入watch方法(第三個參數真不懂啥意思,傳1反正沒錯)。

  在node指令執行的時候,就會列印出一連串的信息:

  在初始化的時候,每一次生成一個監視器,就會先觸發一次change事件,並初始化文件的mtime,觸發的源碼如下:

DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) {
    // ...
    if (!old) {
        if (mtime) {
            //  watch方法會設置該屬性
            if (this.watchers[withoutCase(filePath)]) {
                this.watchers[withoutCase(filePath)].forEach(function(w) {
                    if (!initial || w.checkStartTime(mtime, initial)) {
                        // 觸發事件
                        w.emit("change", mtime, initial ? "initial" : type);
                    }
                });
            }
        }
    }
    // ...
}

  這個地方的事件只會在初始化的時候被調用。

  這裡有一個小問題,在每一次初始化的時候會進行doInitScan掃描,掃描的文件信息會被填充到files容器中,即

// files
['D:\\workspace\\doc\\test\\a.js', 'D:\\workspace\\doc\\test\\b.js', 'D:\\workspace\\doc\\test\\c.js']

  然後在watch方法有這麼一段代碼:

DirectoryWatcher.prototype.watch = function watch(filePath, startTime) {
    //...
    var data;
    if (filePath === this.path) { /**/ }
    // 獲取文件
    else {
        data = this.files[filePath];
    }
    process.nextTick(function() {
        if (data) {
            var ts = data[0] === data[1] ? data[0] + FS_ACCURACY : data[0];
            // 這裡的startTime為1 肯定能進去
            if (ts >= startTime)
                watcher.emit("change", data[1]);
        }
        // ...
    }.bind(this));
    return watcher;
};

  watch方法會在後面會嘗試獲取容器中的文件信息並處罰change事件,理論上這裡會觸發兩次change,然而實際上只有一次。

  原因就在初始化掃描時候使用了async模塊的方法,即:

fs.readdir(this.path, function(err, items) {
    // 掃描文件並將信息填入容器中
    async.forEach(items, function(item, callback) {
        // ...
    })
});

  該模塊的方法全是非同步調用,所以在watch方法調用的第一時刻,此時初始化掃描還在進行中,files容器仍然為空,在watch與doInitScan方法中加log,可以發現:

  在watch方法完成後,掃描才開始。

 

  掃描開始後,進程掛起等待文件操作行為,這裡分別對文件進行各種操作:

修改文件內容

  觸發了change事件。

刪除文件

  觸發了remove事件。

修改文件名

  這裡僅僅觸發了remove事件。

  原因在於,這個操作被系統認為是刪除一個文件再增加一個文件,但是文件增加在監視文件時是不會觸發任何事件的,也不會生成該文件的watcher,只會將該文件信息收集進files容器中,這個在之前講過。

  註意,順序是先刪後增,這裡可以簡單的log一下,因為在setFileTime中傳了對應的事件類型,雖然沒有用上,這裡測試可以用用:

  改名後,列印:

  過程為,先觸發了觸發unlink事件,將文件刪除,然後將新文件的信息加入到files容器中,然後觸發文件的change事件。

 

  接下來是文件夾監視操作,測試代碼如下:

// 模塊引入
// 我都複製過來了!
const Watchpack = require('./lib/watchpack');
const fs = require('fs');
const path = require('path');
// 實例化一個Watchpack類 不傳參
const el = new Watchpack();
// 需要監視的文件夾
const directory = path.join(process.cwd(), 'test');
el.watch([], [directory], 1);
// 監視change事件
el.on('change', (...args) => {
    console.log('change: ' + args.join(','));
});

  註意,文件夾無論怎樣都只會觸發change事件。這裡搞兩張圖片特別累,所以直接展示傳過來的參數。

新建文件

  文件路徑與修改時間。

刪除文件

  只有文件路徑,因為文件被刪了。

文件改名

  這個地方事就多了,這三處觸發全部來源於setFileTime方法中: 

DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) {
    // ...

    if (this.watchers[withoutCase(this.path)]) {
        this.watchers[withoutCase(this.path)].forEach(function(w) {
            if (!initial || w.checkStartTime(mtime, initial)) {
                w.emit("change", filePath, mtime, initial ? "initial" : type);
            }
        });
    }
};
DirectoryWatcher.prototype.watch = function watch(filePath, startTime) {
    this.watchers[withoutCase(filePath)] = this.watchers[withoutCase(filePath)] || [];
    var watcher = new Watcher(this, filePath, startTime);
    this.watchers[withoutCase(filePath)].push(watcher);
    // ...
}

  因為watch的是一個文件夾,所以在watcher容器中會有對應的鍵,所以任何文件的變動都會觸發文件夾的change事件。

  這裡改文件名會涉及:刪除文件觸發一次,增加文件觸發一次,change事件觸發一次。

 

  至此,基本上該watch模塊的內容基本處理完畢,撒花!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、ADB是什麼? ADB,即Android Debug Bridge,它是Android提供的一個通用的調試工具。藉助這個工具可以很好的調試開發程式。它是客戶端/服務端架構的命令工具,主要分三個部分: (1)adb客戶端:運行在我們的開發機器上 (2)adb 伺服器:在開發機器後臺運行的進程 (3 ...
  • 框架依舊在快速更新著:在重構、簡化代碼,統一標準的過程中。中間也遇到各種坑,不過好在一步一腳印的解決了。雖然還有些功能還在思考,不過教程,還是得補上:這篇就寫寫StartController,實現的代碼雖少,但原理很精彩!!! ...
  • private String isRoot(){ String bool = "Root:false"; try{ if ((!new File("/system/bin/su").exists()) && (!new File("/system/xbin/su").exists())){ bool ...
  • 1、APP跳轉 2、APP功能跳轉 3、系統功能調用 1、APP跳轉 1、被打開方 設置APP的URL Types(設置位置在 “項目 - TARGETS - APP icon - info - (拉到最下)URL Types ”)。 URL Schemes:填上,你要給其他應用調用的URL地址(自 ...
  • 先開一篇,以後再補充。。。 1、如果一個 tableView 對應多個 dataSource 。那麼要考慮,點擊/滑動 切換時,請求返回的數據,是否是當前 “功能選中”的位置。 對比請求前後的欄位、狀態若不是,1)、可丟棄。 2)、可刷新該狀態對應的 dataSource 數組(有的話),下次切換, ...
  • 下午001-動態操作元素 創建元素:$('標簽字元串')添加元素:append(),appendTo():追加子元素prepend(),prependTo():前加子元素after(),insertAfter():後加兄弟元素before(),insertBefore():前加兄弟元素動態刪除元素e ...
  • 什麼也不想說 對應的WEB頁面 ...
  • css的註釋 /*.......*/ 直接在html代碼中寫css <p style="color: rebeccapurple;font-size: 18px">Hao</p> css代碼寫在當前文件中 <head> <meta charset="UTF-8"> <title>Title</tit ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...