gulp源码剖析
2019-11-18杂谈搜奇网41°c
A+ A-一.团体组织剖析
团体组织
经由过程在nodejs环境对源码的打印,我们终究取得的gulp实例行如下图。那末我们gulp实例上的属性和要领是怎样生成的呢?
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)类的完成
源码index.js分为三个部份,第一部份是gulp组织函数,增加实例要领,第二部份是Gulp原型要领增加,第三部份是导出gulp实例。
//第一部份是gulp组织函数,增加实例要领 function Gulp(){ .......
} //第二部份是原型要领增加 Gulp.prototype.src=....... //第三部份是导出gulp实例 Gulp.prototype.Gulp = Gulp; var inst = new Gulp(); module.exports = inst;
我们晓得完成一个类,经由过程组织函数往this对象增加实例属性和要领,或许增加原型要领,类的实例自动具有实例属性、实例要领和原型要领。
//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
//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要领。
bind()要领建立一个新的函数,在bind()被挪用时,这个新函数的this被bind的第一个参数指定,其他的参数将作为新函数的参数供挪用时运用。
(2)导出实例
导出实例天然是为了运用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要领。