gulp源码分析
一.整体结构分析
整体结构
通过在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方法。
gulp源码分析的更多相关文章
- 鸿蒙内核源码分析(构建工具篇) | 顺瓜摸藤调试鸿蒙构建过程 | 百篇博客分析OpenHarmony源码 | v59.01
百篇博客系列篇.本篇为: v59.xx 鸿蒙内核源码分析(构建工具篇) | 顺瓜摸藤调试鸿蒙构建过程 | 51.c.h.o 编译构建相关篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿 ...
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- nginx源码分析之网络初始化
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- zookeeper源码分析之三客户端发送请求流程
znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...
- java使用websocket,并且获取HttpSession,源码分析
转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
随机推荐
- 虚拟机VMware14 pro下安装REHL5U11
1. 创建虚拟磁盘,自定义,磁盘类型选IDE,确保安装系统过程中只有一个物理光盘驱动/ISO镜像: 2. 安装VMware Tools 2.1 虚拟机>安装VMware Tools 2.2 在光 ...
- Python学习笔记整理总结【Django】【MVC/MTV/路由分配系统(URL)/视图函数 (views)/表单交互】
一.Web框架概述 Web框架本质上其实就是一个socket服务端,用户的浏览器其实就是一个socket客户端. #!/usr/bin/env python # -*- coding:utf-8 ...
- maven 打包构建相关命令
1.命令 mvn clean package 依次执行clean.resources.compile.testResources.testCompile.test.jar(打包)等7个阶段. mvn ...
- java8 Optional使用总结
[前言] java8新特性 java8 函数接口 java8 lambda表达式 Java 8 时间日期使用 java8 推出的Optional的目的就是为了杜绝空指针异常,帮助开发者开发出更优雅的代 ...
- 通过父级id获取到其下所有子级(无穷级)——Mysql函数实现
[需求]某用户只能查看其自己信息及其下级信息,涉及通过该用户所在部门获取其下所有部门(多层)id集合. 步骤一:对数据库进行设置: set global log_bin_trust_function_ ...
- 序列标注(HMM/CRF)
目录 简介 隐马尔可夫模型(HMM) 条件随机场(CRF) 马尔可夫随机场 条件随机场 条件随机场的特征函数 CRF与HMM的对比 维特比算法(Viterbi) 简介 序列标注(Sequence Ta ...
- eShopOnContainers学习系列(三):RabbitMQ消息总线实践
今天研究了下eShopOnContainers里的RabbitMQ的使用,在项目里是以封装成消息总线的方式使用的,但是仍然是以其发布.订阅两个方法作为基础封装的,我们今天就来实际使用一下. 为了简单起 ...
- ELK 学习笔记之 elasticsearch bool组合查询
elasticsearch bool组合查询: 相当于sql:where _type = 'books' and (price = 500 or title = 'bigdata') Note: mu ...
- Rust入坑指南:常规套路
搭建好了开发环境之后,就算是正式跳进Rust的坑了,今天我就要开始继续向下挖了. 由于我们初来乍到 ,对Rust还不熟悉,所以我决定先走一遍常规套路. 变不变的变量 学习一门语言第一个要了解的当然就是 ...
- 小游戏:200行python代码手写2048
#-*- coding: utf-8 -*- import curses from random import randrange, choice from collections import de ...