详解Nodejs的timers模块

本模块,属于来模拟一些浏览器自带方法的模块,比如setTimeout,clearTimeout等方法,之所以会有该模块,在我看来,也是为了能让前端工程师使用起来,更简单,使用一个单独的模块,来把浏览器上的功能来模拟出来,那么就可以直接减少学习的成本,这样就可以花更少的时间,学习到更多的东西。

timers模块中,使用的C++的方法

timers模块中,调用了C++实现的方法,这些方法,在该模块中,占据了很重要的位置,所以,这里我们先来看下,在C++的方法中,提供了哪些方法。

var Timer = process.binding('timer_wrap').Timer;
console.log(Timer); 

运行之后,在控制台,就会打印出如下的内容,它的格式如下

{
 [Function: Timer]
 // Timer构造函数,可以进行实例化
 kOnTimeout: 0,
 // 静态属性,公用,更改会影响其他的调用
 now: [Function: now]
 // 静态方法,获取类似时间戳的一个数字
} 

其中,Timer本身是一个构造函数,而这个构造函数中,还包含了一个静态属性和一个静态方法,关于静态属性和方法,基本上,这两个只是拿来使用的,是禁止修改的,并且,其使用方法比较简单,所以这里不多说了。
Timer既然还是一个构造函数,那么久是可以被实例化的,接下来,看下实例化之后的对象:

var Timer = process.binding('timer_wrap').Timer,
 timer = new Timer(),
 i = ""; 

console.log("obj has attribute:");
console.log(timer); 

console.log("prototype method and attribute:");
for(i in timer){
 console.log(i+"="+timer[i]);
}

把上面的代码,执行的结果如下:

obj has attribute:
{} 

prototype method and attribute:
close=function close() { [native code] }
ref=function ref() { [native code] }
unref=function unref() { [native code] }
start=function start() { [native code] }
stop=function stop() { [native code] }
setRepeat=function setRepeat() { [native code] }
getRepeat=function getRepeat() { [native code] }
again=function again() { [native code] }

从上面的结果中可以看出,在Timer实例化之后,在对象本身,是没有属性和方法的,在原型链上,是有一些方法,至于这些方法,有什么用,就需要慢慢去看一下了。

timers模块中的一个基础--构造函数Timeout

之所以这里要把这个构造函数以单小节的形式给出,是因为在我看来,如果想要对整个timers模块中的逻辑有更好的认识,那么该模块的基础一个私有的构造函数的理解,还是很有必要的。

这里,我们首先来看一下源码:

var Timeout = function(after) {
 // 定义内部属性,过时时间
 this._idleTimeout = after; 

 // 循环链表中的两个属性,可以参考前篇文章linklist私有模块
 this._idlePrev = this;
 this._idleNext = this; 

 // 记录开始计时时间的属性
 this._idleStart = null; 

 // 当时间到了,执行的回调函数
 this._onTimeout = null; 

 // 该计时器,是否需要repeat,setInterval方法,该属性为true
 this._repeat = false;
}; 

function unrefdHandle() {
 // unref方法的回调函数,内部this指向Timeout._handle属性
 // 在该属性上,定义了owner属性,保存Timeout的实例化后的对象
 this.owner._onTimeout();
 if (!this.owner._repeat)
  this.owner.close();
} 

Timeout.prototype.unref = function() {
 // 这个方法,是用来暂停计时器的
 // 添加一个新的属性_handle用来对接C++提供的API接口
 if (!this._handle) {
  // 做一些初始的判断属性,设置初始值等
  var now = Timer.now();
  if (!this._idleStart) this._idleStart = now;
  var delay = this._idleStart + this._idleTimeout - now;
  if (delay < 0) delay = 0; 

  // 把this指向的计时器对象,清理掉,从计时器链表中清理掉
  exports.unenroll(this); 

  // 介入C++提供的API方法
  this._handle = new Timer(); 

  // 添加一些属性,用来保存一些信息
  this._handle.owner = this;
  this._handle[kOnTimeout] = unrefdHandle; 

  // 开始计时,在delay后执行改方法的回调
  this._handle.start(delay, 0);
  this._handle.domain = this.domain; 

  // 调用C++提供的方法,停止计时器的执行
  this._handle.unref();
 } else {
  // 如果之前有_handle属性,那么则直接停止
  this._handle.unref();
 }
}; 

Timeout.prototype.ref = function() {
 // 该方法,只有在unref之后,才起作用,恢复计时器的工作
 // 如果在unref中,生成了_handle属性,那么使用该属性
 // 调用C++提供的API,ref,恢复计时器的运行
 if (this._handle)
  this._handle.ref();
}; 

Timeout.prototype.close = function() {
 // 当要关闭计时器对象时,如果定义过接入C++饿API的方法时
 // 直接使用C++的方法,关闭
 // 否则,把该方法,清理出去
 // 不让它再lists链表中,那么当计时器执行到时,也不会执行该计时器的回调函数
 this._onTimeout = null;
 if (this._handle) {
  this._handle[kOnTimeout] = null;
  // 调用C++中提供的close方法,见前面构造函数Timer的原型链方法中
  this._handle.close();
 } else {
  exports.unenroll(this);
 }
};

上面的源码,就是在timers模块中,内部的一个私有构造函数,在timers公开的一些方法,占据了一个很重要的位子,因为,这个方法,是timers模块,与C++代码链接的重要部分。该部分,是没有示例可以给出的,只有在后面使用timers模块对外公开的API中,来看下对应的使用效果。

这里之所以,要先把这个构造函数放在这里,因为,在我看来,如果能先对这个构造函数有所了解的话,那么接下来看timers模块中的其他方法时,就会变的简单很多。

当然,也有可能是,因为没有看其他的源代码,而导致对于该构造函数的一些方法和属性,很没用感觉的,那么,接下来,就继续看下去吧。

timers模块的源码

timers中的源码,可以分为两部分,在这里,只会看下其中的一部分,还有另外一部分,是和延时执行相反的立即执行的回调函数,这是我们不常用到的,所以这里就不在占用篇幅。

这里,依然使用源码来开始:

'use strict'; 

// timer_wrap模块,为底层C++实现的模块
var Timer = process.binding('timer_wrap').Timer;
 // Timer在控制台打印出的数据如下:
 // {[Function: Timer] 是一个构造函数
 //  kOnTimeout: 0,
 //  now: [Function: now]
 // } 

// Nodejs模拟的双向链表的操作模块,请查看前一篇关于linklist的文章
var L = require('_linklist'); 

// 断言的管理模块中的ok方法
var assert = require('assert').ok; 

var kOnTimeout = Timer.kOnTimeout | 0; 

// Timeout values > TIMEOUT_MAX are set to 1.
var TIMEOUT_MAX = 2147483647; // 2^31-1 

// 把timer添加到debug的模块中,并生成一个函数,命名为debug
// 在之后,直接调用,该函数,即可把官员timer的错误信息,打印到控制台
var util = require('util');
var debug = util.debuglog('timer');
// 注,debuglog方法,应该是最近的版本中,新添加的,因为在一年前,刚接触nodejs时,
// util模块中,还没有该方法 

// Object containing all lists, timers
// key = time in milliseconds
// value = list
var lists = {}; 

// the main function - creates lists on demand and the watchers associated
// with them.
// 把item存入到一个链表中去,并且把msecs对应的链表,存入到lists对象中去
// lists的格式是这样的:
 // {
 // "1000": 这里是一个循环链表,该链表内,包含了所有msecs=1000的list对象
 // "2000":{}
 // } 

function insert(item, msecs) {
 // 给item定义两个私有属性
 // 一个记录当前时间
 item._idleStart = Timer.now();
 // 一个记录毫秒时间,类似于过期时间
 item._idleTimeout = msecs; 

 // 如果定义的毫秒,是负值,则直接返回,不做后面的处理
 if (msecs < 0) return; 

 var list; 

 // 如果该过期时间,已经缓存在了lists对象中,则直接找到缓存的数据
 if (lists[msecs]) {
  list = lists[msecs];
 } else {
  // 否则,执行新建一个list数据
  // 并把item和msecs的数据初始化到新创建的对象中去
  list = new Timer();
  // 下面这些,就是Timer实例化之后,包含的方法
  // close
  // ref
  // unref
  // start
  // stop
  // setRepeat
  // getRepeat
  // again 

  // 实例化之后,调用start方法
  list.start(msecs, 0); 

  // 把list对象,改为一个循环链表
  L.init(list); 

  // 把该list添加到lists对象中缓存
  // 并设置一些属性,这些属性,在其他方法中被用到
  lists[msecs] = list;
  list.msecs = msecs;
  list[kOnTimeout] = listOnTimeout;
 } 

 // 把item插入到list的下一个节点去
 L.append(list, item);
 assert(!L.isEmpty(list)); // list is not empty
} 

// 每一个list的kOnTimeout的属性值,应该是一个回调函数
// 所以,其内部指向的是list本事
function listOnTimeout() {
 var msecs = this.msecs;
 var list = this; 

 debug('timeout callback %d', msecs); 

 // 类似一个时间戳,但是又和Date.now()的毫秒级时间戳不同,不知道是如何判断这个的
 var now = Timer.now();
 debug('now: %d', now); 

 var diff, first, threw; 

 // 当时间到了之后,把对应该时间的链表中的所有元素执行
 // 如果出现异味,则等一会再次执行,请看源码中的具体注释
 while (first = L.peek(list)) {
  // If the previous iteration caused a timer to be added,
  // update the value of "now" so that timing computations are
  // done correctly. See test/simple/test-timers-blocking-callback.js
  // for more information.
  // 本处的while是,把list的所有前置列表,都处理一遍,直到list所处的链表中,只有list时结束
  if (now < first._idleStart) {
   // 当first元素,当执行insert时,会操作_idleStart的属性值
   // 如果Timer.now的值,是一直增加的,那么这里为神马会执行?
   // 那么又为什么要有这个判断?只是打了一个log,难道只是为了做个通知?
   now = Timer.now();
   debug('now: %d', now);
  } 

  // 求这个差值?并且与list的msecs值进行判断
  diff = now - first._idleStart;
  if (diff < msecs) {
   // 执行到这里,那边把list继续延时一段时间,因为当前的一个item没有被执行
   // 所以重新计时,再执行一次
   list.start(msecs - diff, 0);
   debug('%d list wait because diff is %d', msecs, diff); 

   // 并且直接return,结束本回调函数,等待msecs-diff时间之后,再次执行
   return;
  } else {
   // 把first从它所在的链表中移除
   L.remove(first); 

   // 我觉得,这里是在判断,是否移除成功
   assert(first !== L.peek(list)); 

   // 如果当前的first没有回调函数,那么不需要再向下执行,继续while循环
   if (!first._onTimeout) continue; 

   // 接下来,就是执行回调的处理了,处理的逻辑还行,看起来不算复杂
   // 只是,有些判断,我现在无法理解到,为什么要这么判断 

   // v0.4 compatibility: if the timer callback throws and the
   // domain or uncaughtException handler ignore the exception,
   // other timers that expire on this tick should still run.
   //
   // https://github.com/joyent/node/issues/2631
   var domain = first.domain;
   if (domain && domain._disposed)
    continue; 

   try {
    if (domain)
     domain.enter();
    threw = true;
    first._onTimeout();
    if (domain)
     domain.exit();
    threw = false;
   } finally {
    if (threw) {
     // We need to continue processing after domain error handling
     // is complete, but not by using whatever domain was left over
     // when the timeout threw its exception.
     var oldDomain = process.domain;
     process.domain = null;
     process.nextTick(function() {
      list[kOnTimeout]();
     });
     process.domain = oldDomain;
    }
   }
  }
 } 

 debug('%d list empty', msecs);
 assert(L.isEmpty(list));
 list.close();
 delete lists[msecs];
} 

var unenroll = exports.unenroll = function(item) {
 L.remove(item); 

 // _idleTimeout中保存着msecs的值,
 // 所有可以根据该属性,直接找到该对象在lists中的缓存数据
 // 不过,item的msecs中,也保存了list本身的msecs的 

 var list = lists[item._idleTimeout];
 // if empty then stop the watcher
 debug('unenroll');
 if (list && L.isEmpty(list)) {
  debug('unenroll: list empty');
  // list调用C++的接口
  list.close();
  delete lists[item._idleTimeout];
 }
 // if active is called later, then we want to make sure not to insert again
 item._idleTimeout = -1;
 // 本方法,其实就是在清理一些默认的数据了
 // 属于,当一个方法执行完之后,把其对应的数据,都直接清理掉 

}; 

// Does not start the time, just sets up the members needed.
exports.enroll = function(item, msecs) {
 // 给item重新设置一些属性
 // msecs的值,需要时number类型,并且有效的正整数和零
 if (!util.isNumber(msecs)) {
  throw new TypeError('msecs must be a number');
 } 

 if (msecs < 0 || !isFinite(msecs)) {
  throw new RangeError('msecs must be a non-negative finite number');
 } 

 // if this item was already in a list somewhere
 // then we should unenroll it from that
 // 保证,item不会存在于两个链表中,
 // 比如,我最初把item设置为1000之后执行,那么item在lists[1000]所在的链表中
 // 接下来,我又把item设置为2000之后执行,那么就要先吧item从原来的lists[1000]的链表中删除
 // 然后,添加到lists[2000]所指向的链表去
 if (item._idleNext) unenroll(item); 

 // Ensure that msecs fits into signed int32
 // 保证是在最大值之内的,否则,设置为一个系统设置的最大值
 if (msecs > TIMEOUT_MAX) {
  msecs = TIMEOUT_MAX;
 } 

 // 设置信息,并初始化item本身的链表
 item._idleTimeout = msecs;
 L.init(item);
}; 

// call this whenever the item is active (not idle)
// it will reset its timeout.
exports.active = function(item) {
 // 把item插入到缓存的lists对象中,
 // 或者把已经存在的于对象中的item,进行一次数据更新
 var msecs = item._idleTimeout;
 if (msecs >= 0) {
  // 看上面的函数,enroll可以知道,msecs是必须大于等于0的
  var list = lists[msecs];
  // 如果list存在于lists中,找到对应的链表 

  if (!list || L.isEmpty(list)) {
   // 如果list为空,或者list为空链接,则执行insert方法,创建一个新的链表
   // 并且把该链表,保存到lists[msecs]中去
   insert(item, msecs);
  } else {
   // 如果有,那么更新item属性的当前时间,把item插入到list链表中去
   item._idleStart = Timer.now();
   L.append(list, item);
  }
 }
}; 

/*
* DOM-style timers
*/ 

exports.setTimeout = function(callback, after) {
 // setTimeout的实现源代码
 // 前两个参数必须是固定的
 var timer; 

 // after转化为数字,或者NaN
 after *= 1; // coalesce to number or NaN 

 // 如果不在合法范围之内,则把after设置为1
 if (!(after >= 1 && after <= TIMEOUT_MAX)) {
  after = 1; // schedule on next tick, follows browser behaviour
 } 

 // 根据Timerout构造函数,生成一个实例,该构造函数完成的功能
 // 只是创建一个对象,设置了一些属性和一些方法
 timer = new Timeout(after);
 // 实例化后的timer包含以下内部属性
 // _idleTimeout = after;
 // _idlePrev = this;
 // _idleNext = this;
 // _idleStart = null;
 // _onTimeout = null;
 // _repeat = false;
 // 以及一下几个原型链方法
 // unref
 // ref
 // close 

 // 如果传入的参数,小于等于2个,说明没有多余的默认参数传入
 if (arguments.length <= 2) {
  timer._onTimeout = callback;
 } else { 

  // 如果有多余的默认参数传入,那么就要把多余的参数缓存一下
  // 使用闭包,重新设置一个回调函数
  var args = Array.prototype.slice.call(arguments, 2);
  timer._onTimeout = function() {
   callback.apply(timer, args);
  }
 } 

 // 设置timer的domain属性为process的domain属性
 // 该属性,暂时还不知道为什么存在
 if (process.domain) timer.domain = process.domain; 

 // 把timer设置为启动,并在active中,插入到等待执行的列表中去
 exports.active(timer); 

 // 返回Timeout的实例对象,所以,可以想象setTimeout的返回值,到底有哪些属性和方法了吧
 return timer;
}; 

exports.clearTimeout = function(timer) {
 // 只有timer存在
 // 回调存在,回调时间存在的情况下,才需要把该方法清理掉
 // 至于为什么要判断这些条件,请参考listOnTimeout方法内部的注释及逻辑
 if (timer && (timer[kOnTimeout] || timer._onTimeout)) {
  timer[kOnTimeout] = timer._onTimeout = null;
  // 清除回调,时间等属性,然后把timer自lists链表中,去除掉
  // 这样减少在每次调用时,对lists中对象的无意义的循环
  if (timer instanceof Timeout) {
   timer.close(); // for after === 0
  } else {
   exports.unenroll(timer);
  }
 }
}; 

exports.setInterval = function(callback, repeat) {
 // 前期的处理,和setTimeout方法相同,唯一不同的是,回调
 // 在本方法中,回调之后,再添加另外一个计时器
 // 在我看来,就像是每次去调用setTimeout方法一样 

 repeat *= 1; // coalesce to number or NaN 

 if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) {
  repeat = 1; // schedule on next tick, follows browser behaviour
 } 

 var timer = new Timeout(repeat);
 var args = Array.prototype.slice.call(arguments, 2);
 timer._onTimeout = wrapper;
 timer._repeat = true; 

 if (process.domain) timer.domain = process.domain;
 exports.active(timer); 

 return timer; 

 function wrapper() {
  callback.apply(this, args);
  // If callback called clearInterval().
  if (timer._repeat === false) return;
  // If timer is unref'd (or was - it's permanently removed from the list.) 

  // 下面的处理,是因为在net模块中,和在本模块中,重新启用一个计时器的方法有区别
  if (this._handle) {
   // 该分支处理,应该是为了net模块中做的处理
   // 在本模块中,暂时是没有提及到该属性的
   this._handle.start(repeat, 0);
  } else {
   // 当前的模块中的回调函数
   timer._idleTimeout = repeat;
   exports.active(timer);
  }
 }
}; 

exports.clearInterval = function(timer) {
 // 基本上,就是只有timer和repeat属性存在的情况下
 // 才表示timer对象,是出于Interval方法中,
 // 这个时候,才去清理掉repeat属性,然后clearTimeout的方法
 // 清理掉该计时器
 if (timer && timer._repeat) {
  timer._repeat = false;
  clearTimeout(timer);
 }
}; 

timers中的源码,就是这样了,篇幅有限,本篇到这里就结束了,接下来的一篇关于timers模块的文章,将就本篇的源码,结合一些示例,进行一些说明。

总结

像这样的一些模块,感觉突然不知道怎么写了,如果整篇的去放这个源码,感觉这样的文章,完全没有意义的,这样的话,还是应该分开写的吧。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Nodejs极简入门教程(二):定时器

    setTimeout 和 clearTimeout 复制代码 代码如下: var obj = setTimeout(cb, ms); setTimeout 用于设置一个回调函数 cb,其在最少 ms 毫秒后被执行(并非在 ms 毫秒后马上执行).setTimeout 返回值可以作为 clearTimeout 的参数,clearTimeout 用于停止定时器,这样回调函数就不会被执行了. setInterval 和 clearInterval 复制代码 代码如下: var obj = setInt

  • 详解Nodejs的timers模块

    本模块,属于来模拟一些浏览器自带方法的模块,比如setTimeout,clearTimeout等方法,之所以会有该模块,在我看来,也是为了能让前端工程师使用起来,更简单,使用一个单独的模块,来把浏览器上的功能来模拟出来,那么就可以直接减少学习的成本,这样就可以花更少的时间,学习到更多的东西. timers模块中,使用的C++的方法 timers模块中,调用了C++实现的方法,这些方法,在该模块中,占据了很重要的位置,所以,这里我们先来看下,在C++的方法中,提供了哪些方法. var Timer

  • 详解nodejs 文本操作模块-fs模块(五)

    fs模块是一个比较庞大的模块,在前面也介绍了该模块中最核心的一点东西,虽然核心的这点东西,在整个fs模块中占据的比例比较小,但是如果只是我们平常使用的话,基本已经够用了,其他的一些方法,属于能力提升时需要学习的的内容了,所以在后面就不再继续了,本篇属于fs模块中的最后一篇,也不是把fs模块中的其他API都给一一列举出来,这里再说最后一个我看来很重要的方法,监听文件或者目录的的方法watchFile. 概总 这里之所以在最后把这个watchFile方法写入到这里,是因为在前端的一个流行的构建工具g

  • 详解nodejs 文本操作模块-fs模块(一)

    JS的安全性问题,就决定了JS想要取操作数据库操作文件是不可实现的,而Nodejs作为服务端的JS,如果依然不能操作文件,那么又如何称之为服务端语言呢,所以在Nodejs中,提供了一个fs(File System)模块,以实现文件及目录的读写操作. 写在前面 Nodejs的一大优势就在于,支持异步调用,不管是在读取数据库,还是在读取文件时,都可以使用异步的方式进行处理,这样就可以处理高并发的情况,从本篇开始,开始对Nodejs的fs模块中,一些重要的API,结合源码,进行一些说明学习. fs模块

  • 详解Nodejs基于mongoose模块的增删改查的操作

    MongoDB MongoDB是基于Javascript语言的数据库,存储格式是JSON,而Node也是基于JavaScript的环境(库),所以node和mongoDB的搭配能减少因为数据转换带来的时间空间开销. Mongoose 是MongoDB的一个对象模型工具,它将数据库中的数据转换为JavaScript对象以供你在应用中使用,封装了MongoDB对文档的的一些增删改查等常用方法,让NodeJS操作Mongodb数据库变得更加灵活简单. 安装模块mongoose npm install

  • 详解nodejs 文本操作模块-fs模块(二)

    前一篇学习了文件的打开和关闭,文件操作总不能只包含打开和关闭吧,这里就开始文件的读写操作. fs模块方法 1:read和readSync方法 该方法,是从文件的指定位置处读取文件,一直读取到文件底部,然后江都区到的内容输出到一个缓存区,使用方法如下: fs.read(fd,buffer,offset,length,position,callback); 在read方法中,支持6个参数: fd参数,是文件描述符,是open方法的回调函数中获取到的,是一个数字. buffer,是一个buffer对象

  • 详解nodejs 文本操作模块-fs模块(三)

    下面继续nodejs的学习,在前两篇中,已经把文件操作的打开,关闭读写这两个最基本的功能进行了简单的说明,它们的强大之处,让我觉得知道这几种方法之后,基本上就可以随意的操作文件了,但是open,read,write等方法,需要操作的参数确实是有点多的,所以,基于让使用者更简单的完成读写操作,开发者们,继续给这些方法做了进一步的封装,也就是本文接下来将要说的readFile,和writeFile方法,当然也有他们的同步执行方法,只是篇幅有限,并且同步的方法和异步的方法,在内部实现和参数使用中,差别

  • 详解nodejs 文本操作模块-fs模块(四)

    在前文中,提到了一个概念,就是当我在读取文件时,我需要知道这个文件的数据量的大小,而在readFile的源码中,是使用的fa.fstat方法,获取到了文件的相关数据,而对于使用fstat获取到的一个对象中,所包含的属性和方法代表的具体含义,前文中并没有涉及,本篇就看下,这个State对象中,包含的数据都有哪些,并且他们分别代表的含义是什么. 方法集合 方法集合,不是说的State对象中包含的方法集合,而是说,在调用哪些API时,返回的值时一个State的实例,比如,在fstate就是其中之一.

  • 详解nodejs内置模块

    概述 nodejs内置模块指的是除默认提供的语法之外,提供的美容,无需下载,直接引入,引入只写名称即可. nodejs内置模块: 1.path模块  用于处理文件路径. path.normalize(路径解析,得到规范路径): path.join(路径合并): path.resolve(获取绝对路径): path.relative(获取相对路径). ...... 2.until模块  弥补js功能不足,新增API. util.format(格式化输出字符串); util.isArray(检查是否

  • 详解NodeJS模块化

    目录 一.前言 二.正文 2.1.什么是模块 2.2.Resolving 2.3.require.resolve 2.4.模块间的父子依赖关系 2.5.exports, module.exports 2.6.模块循环依赖 2.7..json和.node 2.8.Wrapping 2.9.Cache 三.总结 一.前言 我们知道,Node.js是基于CommonJS规范进行模块化管理的,模块化是面对复杂的业务场景不可或缺的工具,或许你经常使用它,但却从没有系统的了解过,所以今天我们来聊一聊Node

  • 详解Python中string模块除去Str还剩下什么

    string模块可以追溯到早期版本的Python. 以前在本模块中实现的许多功能已经转移到str物品. 这个string模块保留了几个有用的常量和类来处理str物品. 字符串-文本常量和模板 目的:包含用于处理文本的常量和类. 功能 功能capwords()将字符串中的所有单词大写. 字符串capwords.py import string s = 'The quick brown fox jumped over the lazy dog.' print(s) print(string.capw

随机推荐