gulp源碼分析

一.總體結構分析

總體結構html

經過在nodejs環境對源碼的打印,咱們最終獲得的gulp實例行以下圖。那麼咱們gulp實例上的屬性和方法是如何生成的呢?node

Gulp { domain: null, _events: [Object: null prototype] {}, _eventsCount: 0, _maxListeners: undefined, _registry: DefaultRegistry { _tasks: {} }, _settle: false, watch: [Function: bound ], task: [Function: bound task], series: [Function: bound series], parallel: [Function: bound parallel], registry: [Function: bound registry], tree: [Function: bound tree], lastRun: [Function: bound lastRun], src: [Function: bound src], dest: [Function: bound dest], symlink: [Function: bound symlink] }

(1)類的實現git

源碼index.js分爲三個部分,第一部分是gulp構造函數,添加實例方法,第二部分是Gulp原型方法添加,第三部分是導出gulp實例。github

//第一部分是gulp構造函數,添加實例方法
function Gulp(){ .......
}
//第二部分是原型方法添加 Gulp.prototype.src=....... //第三部分是導出gulp實例 Gulp.prototype.Gulp = Gulp; var inst = new Gulp(); module.exports = inst;

咱們知道實現一個類,經過構造函數往this對象添加實例屬性和方法,或者添加原型方法,類的實例自動擁有實例屬性、實例方法和原型方法。gulp

//People類
function People () { this.name = "Yorhom"; } People.prototype.getName = function () { console.log(this.name);// "Yorhom"
}; var yorhom = new People(); yorhom.getName() console.log(yorhom.name)// "Yorhom"

因此咱們在構造函數中this對象定義的watch等10個方法具體指的哪些方法呢?其實它包括util.inherits(Gulp, Undertaker); 實現了undertaker原型方法tree()、task()、series()、lastRun()、parallel()、registry()的繼承,而後經過prototype定義了src()、dest()、symlink()、watch()。最後要說的是 Undertaker.call(this); ,它經過Call繼承實現了對Undertaker實例屬性_registry和_settle api

//https://github.com/gulpjs/undertaker,index.js
function Undertaker(customRegistry) { EventEmitter.call(this); this._registry = new DefaultRegistry(); if (customRegistry) { this.registry(customRegistry); } this._settle = (process.env.UNDERTAKER_SETTLE === 'true'); } inherits(Undertaker, EventEmitter); Undertaker.prototype.tree = tree; Undertaker.prototype.task = task; Undertaker.prototype.series = series; Undertaker.prototype.lastRun = lastRun; Undertaker.prototype.parallel = parallel; Undertaker.prototype.registry = registry; Undertaker.prototype._getTask = _getTask; Undertaker.prototype._setTask = _setTask;

要單獨的說一下,call()和bind()。數組

call用來實現繼承。服務器

//MDN文檔U call() 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數。

bind方法建立一個函數,並且函數的this替換成當前Gulp。例如, this.watch = this.watch.bind(this); 該行代碼建立了watch方法。app

bind()方法建立一個新的函數,在bind()被調用時,這個新函數的this被bind的第一個參數指定,其他的參數將做爲新函數的參數供調用時使用。

(2)導出實例dom

導出實例天然是爲了使用gulp,咱們先來看一個gulpfile.js的代碼結構。

var gulp = require('gulp') ......... gulp.task('uglifyjs', function () { var combined = combiner.obj([ gulp.src('src/js/**/*.js'), sourcemaps.init(), uglify(), sourcemaps.write('./'), gulp.dest('dist/js/') ]) combined.on('error', handleError) }) ......... gulp.task('default', [ // build
    'uglifyjs',....... ] )

那麼咱們是經過require引入gulp的。咱們已經實現了一個Gulp的類,new Gulp()得到的實例自動擁有繼承的屬性和方法。但是 Gulp.prototype.Gulp = Gulp; 爲何要作這個賦值呢?做用是將構造函數掛載到Gulp原型上,那麼實例就能經過Gulp屬性訪問到構造函數了。

Gulp.prototype.Gulp = Gulp; var inst = new Gulp(); //console.log(inst)
module.exports = inst;

 

二.接口gulp.src()和gulp.dest()

gulp的src和dest接口直接引用了vinyl-fs模塊https://github.com/gulpjs/vinyl-fs

(1)vinyl是什麼?

首先,vinyl-fs是針對文件系統的vinyl 適配器。

那麼vinyl是什麼呢?

What is Vinyl? Vinyl is a very simple metadata object that describes a file. When you think of a file, two attributes come to mind: path and contents. These are the main attributes on a Vinyl object. A file does not necessarily represent something on your computer’s file system. You have files on S3, FTP, Dropbox, Box, CloudThingly.io and other services. Vinyl can be used to describe files from all of these sources.

vinyl是vitual  file formate虛擬文件格式,用於描述一個文件。它有兩個主要的屬性,path屬性和contents屬性。每個Vinyl實例表明一個獨立的文件、目錄或者symlink符號鏈接。

實際項目中,除了咱們須要一個簡潔的方式去描述一個文件以外,咱們還須要訪問這些文件,因此vinyl適配器對外暴露接口src()和dest(),他們都最終返回一個流(stream),src接口生產一個Vinyl 對象,dest接口消費一個Vinyl 對象。

(2)stream流的工做方式

流(stream)是node.js處理流式數據的抽象接口。流是可讀可寫的,全部的流都是EventEmitter實例。

#流的類型# Node.js 中有四種基本的流類型: Writable - 可寫入數據的流(例如 fs.createWriteStream())。 Readable - 可讀取數據的流(例如 fs.createReadStream())。 Duplex - 可讀又可寫的流(例如 net.Socket)。 Transform - 在讀寫過程當中能夠修改或轉換數據的 Duplex 流(例如 zlib.createDeflate())。 此外,該模塊還包括實用函數 stream.pipeline()、stream.finished() 和 stream.Readable.from()。

流有緩衝(暫停)機制。

可寫流可讀流都會在內部的緩衝器中存儲數據。

當調用 stream.push(chunk) 時,數據會被緩衝在可讀流中。 若是流的消費者沒有調用 stream.read(),則數據會保留在內部隊列中直到被消費。一旦內部的可讀緩衝的總大小達到 highWaterMark 指定的閾值時,流會暫時中止從底層資源讀取數據,直到當前緩衝的數據被消費。

當調用 writable.write(chunk) 時,數據會被緩衝在可寫流中。 當內部的可寫緩衝的總大小小於 highWaterMark 設置的閾值時,調用 writable.write() 會返回 true。 一旦內部緩衝的大小達到或超過 highWaterMark 時,則會返回 false

stream API 的主要目標,特別是 stream.pipe(),是爲了限制數據的緩衝到可接受的程度,也就是讀寫速度不一致的源頭與目的地不會壓垮內存。

可寫流

可寫流是對數據要被寫入的目的地的一種抽象。 可寫流的例子包括: 客戶端的 HTTP 請求 服務器的 HTTP 響應 fs 的寫入流 zlib 流 crypto 流 TCP socket 子進程 stdin process.stdout、process.stderr

當在可讀流上調用 stream.pipe() 方法時會發出 'pipe' 事件,並將此可寫流添加到其目標集。

const writer = getWritableStreamSomehow(); const reader = getReadableStreamSomehow(); writer.on('pipe', (src) => { console.log('有數據正經過管道流入寫入器'); assert.equal(src, reader); }); reader.pipe(writer);

可讀流

可讀流是對提供數據的來源的一種抽象。 可讀流的例子包括: 客戶端的 HTTP 響應 服務器的 HTTP 請求 fs 的讀取流 zlib 流 crypto 流 TCP socket 子進程 stdout 與 stderr process.stdin 全部可讀流都實現了 stream.Readable 類定義的接口。

對於大多數用戶,建議使用 readable.pipe(),由於它是消費流數據最簡單的方式。若是開發者須要精細地控制數據的傳遞與產生,可使用 EventEmitter readable.on('readable')/readable.read() 或 readable.pause()/readable.resume()

(3)src接口代碼結構以及讀取實現

function src(glob, opt) { var optResolver = createResolver(config, opt); if (!isValidGlob(glob)) { throw new Error('Invalid glob argument: ' + glob); } var streams = [      
gs(glob, opt).... readContents(optResolver), .... ];

var outputStream = pumpify.obj(streams); return toThrough(outputStream); } module.exports = src;

其餘關於讀取的功能添加咱們本篇文章就不詳細說了,只是集中在讀取內容的核心代碼上。

首先,建立createResolver對默認配置和傳入的options(接收哪些選項,請參照https://www.gulpjs.com.cn/docs/api/src/)建立了一個resolver。而後當咱們經過 resolver.resolve(optionKey, [...arguments]) 就能夠解析相關選項了。

// libs/src/options.js 默認選項
var config = { buffer: { type: 'boolean', default: true, }, read: { type: 'boolean', default: true, }, since: { type: 'date', }, removeBOM: { type: 'boolean', default: true, }, sourcemaps: { type: 'boolean', default: false, }, resolveSymlinks: { type: 'boolean', default: true, }, }; module.exports = config;

而後,glob流讀取 gs(glob, opt), 

//glob-stream使用示例
var gs = require('glob-stream'); var readable = gs('./files/**/*.coffee', { /* options */ }); var writable = /* your WriteableStream */ readable.pipe(writable);

接下來,得到輸出流 var outputStream = pumpify.obj(streams); pumpify的功能是組裝一個流的數組成爲一個Duplex流(這個在Nodejs中是可讀可寫的流)。若是在管道中其中一個流被關掉或者發生錯誤,那麼全部的流都會被銷燬。

pumpify Combine an array of streams into a single duplex stream using pump and duplexify. If one of the streams closes/errors all streams in the pipeline will be destroyed.

最後, return toThrough(outputStream); toThrough包裝輸出流爲一個Transform流。Transform 流是在讀寫過程當中能夠修改或轉換數據的 Duplex 流

to-through,Wrap a ReadableStream in a TransformStream.

因此咱們看到,使用src接口獲得的是一個Transform流。

(3)dest接口代碼結構以及寫入實現

function dest(outFolder, opt) { var optResolver = createResolver(config, opt); var folderResolver = createResolver(folderConfig, { outFolder: outFolder }); function dirpath(file, callback) { var dirMode = optResolver.resolve('dirMode', file); callback(null, file.dirname, dirMode); } var saveStream = pumpify.obj( 。。。 mkdirpStream.obj(dirpath) 。。。 writeContents(optResolver) ); // Sink the output stream to start flowing
  return lead(saveStream); } module.exports = dest;

  mkdirpStream.obj(dirpath) 功能是確保目錄存在。

fs-mkdirp-stream //確保在寫入這個目錄以前它是存在的
 Ensure directories exist before writing to them.

最後寫入 return lead(saveStream); 

//lead方法的做用
Takes a stream to sink and returns the same stream. Sets up event listeners to infer if the stream is being used as a Transform or Writeable stream and sinks it on nextTick if necessary. If the stream is being used as a Transform stream but becomes unpiped, it will be sunk. Respects pipe, on('data') and on('readable') handlers.

 

三.接口gulp.task()

gulp中的task是繼承了undertaker中的task方法。undertaker有關task的源碼有個部分。

(1)lib/task.js

//task使用 //第一種,傳入函數
const { task } = require('gulp'); function build(cb) { // body omitted
 cb(); } task(build); //第二種,第一個參數爲字符串,第二個參數爲function
task('build', function(cb) { // body omitted
 cb(); });

因此task.js對參數進行了判斷。

function task(name, fn) { //這是針對第一種直接傳入函數的參數轉化,轉化後name爲任務名,fn爲函數體,與第二種方式就一致了
  if (typeof name === 'function') { fn = name; name = fn.displayName || fn.name; } //若沒有第二個參數,那麼就是獲取task
  if (!fn) { return this._getTask(name); } //第二個參數存在,那麼就是設置task
  this._setTask(name, fn); }

(2)lib/get-task.js、lib/set-task.js

//調用的是_registry的方法
function get(name) { return this._registry.get(name); }
function set(name, fn) { //參數判斷
  assert(name, 'Task name must be specified'); assert(typeof name === 'string', 'Task name must be a string'); assert(typeof fn === 'function', 'Task function must be specified'); //undertakder繼承task自定義的函數
  function taskWrapper() { return fn.apply(this, arguments); } //return task函數體
  function unwrap() { return fn; } taskWrapper.unwrap = unwrap; taskWrapper.displayName = name; //metadata是一個weak map的實例
  var meta = metadata.get(fn) || {}; var nodes = []; if (meta.branch) { nodes.push(meta.tree); } //建立task
  var task = this._registry.set(name, taskWrapper) || taskWrapper; //metadata設置task map數據。
 metadata.set(task, { name: name, orig: fn, tree: { label: name, type: 'task', nodes: nodes, }, }); } module.exports = set;

 lib/registry.js

function setTasks(inst, task, name) { inst.set(name, task); return inst; } function registry(newRegistry) { if (!newRegistry) { return this._registry; } validateRegistry(newRegistry); // 
  var tasks = this._registry.tasks(); //什麼是object.reduce //Reduces an object to a value that is the accumulated result of running each property in the object through a callback. //第一個參數是遍歷的對象,第二個參數是每次迭代時運行的函數,第三個參數是初始化的value值
  this._registry = reduce(tasks, setTasks, newRegistry); this._registry.init(this); } module.exports = registry;

它對tasks數組迭代,每次迭代運行一次setTask方法。

 

原文出處:https://www.cnblogs.com/chenmeng2062/p/11774131.html

相關文章
相關標籤/搜索