《新时期的Node.js入门》学习日记-常用模块(下)

0xGeekCat · 2020-8-28 · 次阅读


WebSocket

WebSocket可以看作是HTTP协议的升级版,它同样是基于TCP协议的应用层协议,主要是为了弥补HTTP协议的无持久化和无状态等缺陷而诞生的。WebSocket提供了客户端和服务器之间全双工的通信机制

保持通话

当客户端发起一个请求时,客户端和服务器之间首先需要建立TCP连接,然后才能使用更高层的HTTP协议来解析数据

HTTP是非持久化的,当用户发起一个request,服务器会随之返回一个response,那么这个HTTP连接就结束了,TCP连接也随之关闭。如果客户端想要继续访问服务器的内容,还需要重新建立TCP连接。对于连续的请求来说,这样会在TCP握手上浪费不少时间

为了改进这一问题,浏览器在请求头里增加了Connection: Keep-Alive字段,当服务器收到带有这一头部的请求时会保持TCP连接不断开,同时也会在response中增加这一字段,这样浏览器和服务器之间只要建立一次TCP连接就可以进行多次HTTP通信

在HTTP 1.1版本中,Connection: Keep-Alive被加入到标准之中,同时所有的连接都会被默认保持,除非手动指定Connection: Close。此外为了避免无限制的长连接,服务器也会设置一个timeout属性,用来指定该连接最长可以保持时间

keep-alive的最大优点在于其避免了多次的TCP握手带来的性能浪费;但还是有一些缺陷,其本质上还是使用HTTP进行通信,对于协议本身没有什么改进

为什么要有WebSocket

假设要开发一个新闻类网站,该网站会一直将最新的新闻推送到页面上而不需要用户进行刷新操作,在WebSocket之前的HTTP协议中,服务器无法主动向客户端推送数据,对于这种情况有两种解决方案

  • 客户端每隔几秒就发起Ajax请求,如果返回不为空,就将内容展示在页面上
  • 使用长轮询,服务器收到客户端的请求后,如没有新的内容,就保持阻塞,当新内容产生后再发送response给客户端。

这两种做法的缺点都很明显。第一种可能客户端发送了很多请求才能得到一个新内容,第二种则是在服务器获得新内容前都无法关闭socket,会占用很多系统资源

WebSocket可以实现浏览器与服务器的全双工通信,它和传统的jsonp、comet等解决方案不同,不必浏览器发送请求后再由服务器返回消息,而是可以由服务器主动发起向浏览器的数据传输

WebSocket的请求头和HTTP很相似,👇是一个WebSocket请求头

截屏2020-08-28 下午2.06.53

服务器返回👇消息

截屏2020-08-28 下午2.07.27

在请求头中,Connection字段必须设置成Upgrade,表示客户端希望连接升级,Upgrade字段必须设置websocket,表示希望升级到WebSocket协议

Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。服务器会给这个随机的字符串再加上一个特殊字符串258EAFA5-E 914-47DA-95CA-C5AB0DC85B11,然后计算SHA-1摘要,之后进行BASE-64编码,将结果作为Sec-WebSocket-Accept头的值返回给客户端

258EAFA5-E914-47DA-95CA-C5AB0DC85B11这一字符串是一个GUID,其本身并没有特别的含义,选择这个字符串的原因只是这个字符串不大可能被WebSocket之外的协议用到

Sec-WebSocket-Version表示支持的WebSocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当被弃用

Origin字段是可选的,通常用来表示在浏览器中发起此WebSocket连接所在的页面,类似于Referer。但是与Referer不同的是,Origin只包含了协议和主机名称

WebSocket与Node

npm中有很多支持WebSocket的第三方模块,此处学习ws

JavaScript连接WebScoket

<script type="text/javascript">
    var ws = new WebSocket("ws://localhost:3004")
    ws.onopen = function () {
        ws.send('hello')
    }
    ws.onmessage = function (msg) {
        console.log(msg.data)
    }
</script>

WebSocket服务器

var WebSocketServer = require('ws').Server
wss = new WebSocketServer({port: 3004})
wss.on('connection', function (ws) {
    ws.on('message', function (message) {
        console.log('received: %s', message)
    })
    ws.send("I'm a message send from a ws server")
})

👇服务器和控制器回显

截屏2020-08-28 下午3.24.21

![截屏2020-08-28 下午3.24.45](《新时期的Node-js入门》学习日记-常用模块-下/截屏2020-08-28 下午3.24.45.png)

在Node里,比较出名的WebSocket模块还有Socket.IO,常被拿来做在线的聊天或者推送服务

Stream

Stream模块为Node操作流式数据提供了支持。Stream的思想最早见于早期的UNIX,在UNIX中使用|符号会创建一个匿名管道,其本质上也是一个Stream,用于两个程序或者是设备之间的数据传输

Stream的种类

在Nodejs中,一共有四种基础的stream类型

  • Readable:可读流(for example fs.createReadStream())
  • Writable:可写流(for example fs.createWriteStream())
  • Duplex:既可读,又可写(for example net.Socket)
  • Transform:操作写入的数据,然后读取结果,通常用于输入数据和输出数据不要求匹配的场景,例如zlib.createDeflate()

Readable Stream

var fs = require('fs')

var readStream = fs.createReadStream('foo.txt', 'utf-8')
readStream.on('data', function (data) {
    console.log(data)
})

readStream.on('close', function () {
    console.log('closed')
})

readStream.on('error', function () {
    console.log('error')
})

Writeable Stream

write方法同样是异步的,假设我们创建一个可读流读取一个较大的文件,再调用pipe方法将数据通过一个可写流写入另一个位置。如果读取的速度大于写入的速度,那么Node将会在内存中缓存这些数据;缓冲区有大小限制(state.highWatermark),当达到阈值后,write方法会返回false,可读流也进入暂停状态,当writeable stream将缓冲区清空之后,会触发drain事件,上游的readable重新开始读取数据

🔔pipe方法相当于在可读流和可写流之间架起了桥梁,使得数据可以通过管道由可读流进入可写流

使用pipe改写的静态文件服务器

var http = require('http')
var fs = require('fs')

var server = http.createServer(function (req, res) {
    if (req.url === '/') {
        var fileList = fs.readdirSync('./')
        res.writeHead(200, {'Content-Type': 'text/plain'})
        res.end(fileList.toString())
    } else {
        var path = '.' + req.url
        var readStream = fs.createReadStream(path).pipe(res)
    }
})

server.listen(3000)
console.log('Listening on 3000')

process.on('uncaughtException', function () {
    console.log('got error')
})

ReadLine

ReadLine是一个Node原生模块,该模块比较不起眼,提供了按行读取Stream中数据的功能

var readline = require('readline')
var fs = require('fs')

var rl = readline.createInterface({
    input: fs.createReadStream('foo.txt')
})

rl.on('line', function (data) {
    console.log(data)
})

rl.on('close', function () {
    console.log('closed')
})

readLine并没有提供形如new readline()形式的构造方法,而是使用createInterface方法初始化了一个rl对象

如果一个可读流中包含了很多条独立的信息需要逐条处理,这可能是一个消息队列,这时使用readline模块就比较方便

自定义Stream

在实际开发中,如果想要使用流式API,而原生的Stream又不能满足需求时,可以考虑自定义Stream类,常用的方法是继承原生的Stream类然后做一些扩展

var Readable = require('stream').Readable
var util = require('util')

util.inherits(MyReadable, Readable)

function MyReadable(array) {
    Readable.call(this, {objectMode: true})
    this.array = array
}

MyReadable.prototype._read = function () {
    if (this.array.length) {
        this.push(this.array.shift())
    } else {
        this.push(null)
    }
}

👆实现了名为MyReadable的类,它继承自Readable类,并且接受一个数组作为参数

想要继承Readable类,就要在自定义的类内部实现_read方法,该方法内部使用push方法往可读流添加数据。

当我们给可读流对象注册data事件后,可读流会在nextTick中调用_read方法,并触发第一次data事件,可读流开始读取时机并不在调用构造函数之后,此时data事件还未注册,可能会捕获不到最初的事件,因此可读流开始产生数据的操作是放在nextTick中的,当有消费者从readable中取数据时会自动调用该方法

例子中在_read方法里调用了push方法,该方法用来向可读流中填充数据

const array = ['a', 'b', 'c', 'd', 'e']
const read = new MyReadable(array)

read.on('data', function (data) {
    console.log(data)
})

read.on('end', function () {
    console.log('end')
})

❗️对于自定义Stream的例子并不是很理解,以后再仔细研究

Events

Node的Events模块只定义了一个类,就是EventEmitter(以下简称Event),这个类在很多Node本身以及第三方模块中大量使用,通常是用作基类被继承

事件和监听器

Node程序中的对象会产生一系列的事件,这些对象被称为事件触发器;所有能触发事件的对象都是EventEmitter类的实例。EventEmitter定义了on方法

截屏2020-08-28 下午6.00.45

注册一个事件并触发

var eventEmitter = require('events')

var myEmitter = new eventEmitter()
myEmitter.on('begin', function () {
    console.log('0xGeekCat')
})

myEmitter.emit('begin')

首先初始化了一个EventEmitter实例,然后注册了一个名为begin的事件,之后调用emit方法触发begin事件

用户可以注册多个同名的事件,如果注册两个名为begin的事件,那么它们都会被触发

如果想获取当前的emitter一共注册了哪些事件,可以使用eventNames方法,该方法会输出包括全部事件名称的数组

console.log(myEmitter.eventNames())

截屏2020-08-28 下午8.19.04

如果注册了两个同名的event,输出结果也只有一个,该方法的结果集并不包含重复结果

❗️在Node v6.x及之前的版本中,event模块可以通过👇方法引入

var myEmitter = process.eventEmitter

在新的版本中这种写法已经被废弃并会抛出一个异常,只能统一由require进行引入,有时能在一些旧版本的第三方模块中还能看到

处理error事件

由于Node代码运行在单线程环境中,那么运行时出现的任何错误都有可能导致整个进程退出。利用事件机制可以实现简单的错误处理功能

当Node程序出现错误的时候,通常会触发一个错误事件,如果代码中没有注册相应的处理方法,会导致Node进程崩溃退出

myEmitter.emit('error', new Error('crash'))

截屏2020-08-28 下午8.31.06

👆Node程序主动抛出了一个error并打印出整个错误栈,相当于

throw new Error('crash')

如果不想因为抛出一个error而使进程退出,那么可以让uncaughtException事件作为最后一道防线来捕获异常

process.on('uncaughtException', function () {
    console.log('got error')
})

throw new Error('Error occurred')

这种错误处理的方式虽然可以捕获异常,避免了进程的退出,但实际上并不值得提倡

继承Events模块

在实际的开发中,通常不会直接使用Event模块来进行事件处理,而是选择将其作为基类进行继承的方式来使用Event,在Node的内部实现中,凡是提供了事件机制的模块,都会在内部继承Event模块;util.inherits是用来继承的方法

假设要用Node来开发一个网页上的音乐播放器应用,关于播放和暂停的处理,就可以考虑通过继承events模块来实现

var util = require('util')
var event = require('events')

function Player() {
    event.call(this)
}

util.inherits(Player, event) 👈

var player = new Player()

player.on('pause', function () {
    console.log('paused')
})

player.on('play', function () {
    console.log('playing')
})

player.emit('play')

另一种场景,假设要利用原生的数组来模拟一个消息队列,该队列会在新增消息和弹出消息时触发对应的事件,也可以考虑继承Events模块

多进程服务

child_process模块

Node是单线程运行的,这表示潜在的错误有可能导致线程崩溃,然后进程也会随着退出,无法做到企业追求的稳定性;另一方面,单进程也无法充分多核CPU,这是对硬件本身的浪费

Node社区本身也意识到了这一问题,于是从0.1版本就提供了child_process模块,用来提供多[进]程的支持

spawn

会使用指定的command来生成一个新进程,执行完对应的command后子进程会自动退出。该命令返回一个child_process对象,这代表开发者可以通过监听事件来获得命令执行的结果

使用spwan来执行ls命令

var spwan = require('child_process').spawn
var ls = spwan('ls', ['-la', '/usr'])

ls.stdout.on('data', function (data) {
    console.log('stdout:', data.toString())
})

ls.stderr.on('data', function (data) {
    console.log('stderr:', data.toString())
})

ls.on('close', function (code) {
    console.log('child process exited with code', code)
})

截屏2020-08-28 下午9.24.16

其中spawn的第一个参数虽然是command,但实际接收的却是一个file

截屏2020-08-28 下午9.24.52

在Windows环境中用于ls同含义的命令dir替代后执行代码会出现形如Error: spawn dir ENOENT的错误;原因很简单,这与操作系统本身有关,在Linux中万物皆文件,命令行的命令也不例外,例如ls命令是一个名为ls的可执行文件;而在Windows中并没有名为dir的可执行文件

fork

在Linux环境下,创建一个新进程的本质是复制一个当前的进程,当用户调用fork后,操作系统会先为这个新进程分配空间,然后将父进程的数据原样复制一份过去,父进程和子进程只有少数值不同,例如进程标识符PID

❗️对于Node来说,父进程和子进程都有独立的内存空间和独立的V8实例,它们和父进程唯一的联系是用来进程间通信的IPC Channel;此外Node中forkPOSIX系统调用的不同之处在于Node中的fork并不会复制父进程

Node中的fork是上面提到的spawn的一种特例,前面也提到了Node中的fork并不会复制当前进程。多数情况下,fork接收的第一个参数是一个文件名,使用fork("xx.js")相当于在命令行下调用node xx.js,并且父进程和子进程之间可以通过process.send方法来进行通信

master.js调用fork来创建一个子进程

var child_process = require('child_process')
var worker = child_process.fork('worker.js', ['args1'])

worker.on('exit', function () {
    console.log('child process exit')
})

worker.send({hello: 'child'})
worker.on('message', function (msg) {
    console.log('from child', msg)
})

worker.js

var begin = process.argv[2]
console.log('I am worker', begin)
process.on('message', function (msg) {
    console.log('from parent', msg)
    process.exit()
})
process.send({hello: 'parent'})

fork内部会通过spawn调用process.executePath,即Node的可执行文件地址/usr/local/bin/node来生成一个Node实例,然后再用这个实例来执行fork方法

截屏2020-08-28 下午9.56.52

exec和execFile

如果开发一种系统,那么对于不同的模块可能会用到不同的技术来实现,例如Web服务器使用Node,然后再使用Java的消息队列提供发布订阅服务,这种情况下通常使用进程间通信的方式来实现

但有时开发者不希望使用这么复杂的方式,或者要调用的干脆是一个黑盒系统,即无法通过修改源码来实现进程间通信,这时候往往采用折中的方式,例如通过shell来调用目标服务,然后再拿到对应的输出

execFile方法

截屏2020-08-28 下午10.06.26

可以看出,execfilespawn在形式上的主要区别在于execfile提供了一个回调函数,通过这个回调函数可以获得子进程的标准输出/错误流

使用shell进行跨进程调用长久以来被认为是不稳定的,这大概源于人们对控制台不友好的交互体验的恐惧,输入命令后,很可能长时间看不到一个输出,尽管后台可能在一直运算,但在用户看来和死机无异

在Linux下执行exec命令后,原有进程会被替换成新的进程,进而失去对新进程的控制,这代表着新进程的状态也没办法获取了,此外还有shell本身运行出现错误,或者因为各种原因出现长时间卡顿甚至失去响应等情况

Node.js提供了比较好的解决方案,timeout解决了长时间卡顿的问题,stdoutstderr则提供了标准输出和错误输出,使得子进程的状态可以被获取

spawn和execfile的比较

先写一段简单的C语言代码,并将其命名为example.c

#include <stdio.h>

int main()
{
    printf("%s", "hello, world");
    return 5;
}

使用gcc编译该文件

截屏2020-08-28 下午10.37.54

使用spwan来调用

var spawn = require('child_process').spawn
var ls = spawn('/Users/max/Webstorm/test/example') 👈 

ls.stdout.on('data', function (data) {
    console.log('stdout:', data.toString())
})

ls.stderr.on('data', function (data) {
    console.log('stderr:', data.toString())
})

ls.on('close', function (code) {
    console.log('child process exited with code', code)
})

❗️spawn的执行目录和node所处目录相同,如果是spawn('example')程序会报错,除非example程序也位于/usr/local/bin目录

截屏2020-08-28 下午11.09.01

程序正确打印出了hello,world,此外还可以看到example最后的return 5会被作为子进程结束的code被返回

使用execFile来调用

const exec = require('child_process').exec
const child = exec('/Users/max/Webstorm/test/example', ((error, stdout, stderr) => {
    if (stderr) 👈 作者似乎写错了 本人在这里进行了修改
        throw stderr
    console.log(stdout)
}))

截屏2020-08-28 下午11.17.45

同样打印出hello,world,可见除了调用形式不同,二者相差不大

在子进程的信息交互方面,spawn使用流式处理的方式,当子进程产生数据时,主进程可以通过监听事件来获取消息;而exec是将所有返回的信息放在stdout里面一次性返回的,也就是该方法的maxBuffer参数,当子进程的输出超过这个大小时,会产生一个错误

此外注意到spawn有一个名为shell的参数,其类型为一个布尔值或者字符串,如果这个值被设置为true,就会启动一个shell来执行命令,这个shell在UNIX上是bin/sh,在Windows上则是cmd.exe

exec在内部也是通过调用execFile来实现的,可以从源码中验证这一点,在早期的Node源码中,exec命令会根据当前环境来初始化一个shell,例如cmd.exe或者/bin/sh,然后在shell中调用作为参数的命令

通常execFile的效率要高于exec,这是因为execFile没有启动shell,而是直接调用spawn来实现的

进程间通信

👆几个用于创建进程的方法,都属于child_process的类方法,此外childProcess类继承了EventEmitter,在childProcess中引入事件给进程间通信带来很大的便利

childProcess中的事件

截屏2020-08-29 上午8.53.59

childProcess模块定义的send方法,用于进程间通信

截屏2020-08-29 上午9.01.46

通过send方法发送的消息,可以通过监听message事件来获取

父进程发送一个Socket对象

const child = require('child_process').fork('worker.js')
const server = require('net').createServer()

server.on('connection', (socket) => {
    socket.end('handled by parent')
})

server.listen(1337, function () {
    child.send('server', server)
})

子进程接收socket对象

process.on('message', function (m, server) {
    if (m === 'server') {
        server.on('connection', function (socket) {
            socket.end('handled by child')
        })
    }
})

❌此处没有太理解例子

Cluster

child_process的一个重要使用场景是创建多进程服务来保证服务稳定运行

为了统一Node创建多进程服务的方式,Node在0.6之后的版本中增加了Cluster模块,Cluster可以看作是做了封装的child_Process模块。

❗️Cluster模块的一个显著优点是可以共享同一个socket连接,这代表可以使用Cluster模块实现简单的负载均衡

const cluster = require('cluster')
const http = require('http')
const numCPUs = require('os').cpus().length

if (cluster.isMaster) {
    console.log('Master process id is', process.pid)

    for (let i = 0; i < numCPUs; i++) {
        cluster.fork()
    }

    cluster.on('exit', function (worker, code, signal) {
        console.log('worker process died, id', worker.process.pid)
    })
} else {
    // Worker可以共享一个TCP连接
    http.createServer(function (req, res) {
        res.writeHead(200)
        res.end('hello, world\n')
    }).listen(3000)

    console.log('Worker started, process id', process.pid)
}

为了充分利用多核CPU,先调用OS模块的cpus()方法来获得CPU的核心数

截屏2020-08-29 上午9.33.07

本人电脑为1个6核CPU,但由于电脑操作系统使用了超线程技术,所以实际拥有12个核,从👆代码的运行结果不难看出有12条进程

截屏2020-08-29 上午9.53.50

Cluster模块采用的是经典的主从模型,由master进程来管理所有的子进程,可以使用cluster.isMaster属性判断当前进程是master还是worker,其中主进程不负责具体的任务处理,其主要工作是负责调度和管理,上面的代码中,所有的子进程都监听3000端口

通常情况下如果多个Node进程监听同一个端口时会出现Error: listen EADDRINUS的错误,而Cluster模块能够让多个子进程监听同一个端口的原因是master进程内部启动了一个TCP服务器,而真正监听端口的只有这个服务器,当来自前端的请求触发服务器的connection事件后,master会将对应socket句柄发送给子进程

Process对象

Process是一个全局对象无须声明即可访问,每个Node进程都有独立的process对象。该对象中存储当前进程的环境变量

console.log(process.getuid()) // 用户ID
console.log(process.argv) // argv[0]表示Node本身 argv[1]表示当前文件路径
console.log(process.pid) // 进程ID
console.log(process.cwd()) // 当前目录
console.log(process.version) // Node版本

截屏2020-08-29 上午10.02.20

环境变量

console.log(process.env)

截屏2020-08-29 上午10.04.40

开发者可以在代码中判断当前正在运行的Node所属版本,并根据结果来决定是否运行含有一些最新特性的代码

var version = process.version

if (version > "v07.6.0") { 👈 当前版本为V12.18.3 题目的代码在比较中会出现问题
    console.log('Higher version than v6.0.0')
}

方法和事件

process模块定义👇事件

截屏2020-08-29 上午10.17.22

unhandledRejectionuncaughtException通常用做错误处理的最后一层保险,但不代表开发者可以省略具体错误处理的代码,beforeExit比较有意思,它仅会在进程准备退出时触发,准备退出是指目前的事件循环没有要执行的任务,如果我们手动捕获这一事件并在回调中增加一些额外动作,进程就不会退出

process.on('beforeExit', function () {
    setInterval(function () {
        console.log('Process will not exit')
    }, 1000)
})

截屏2020-08-29 上午10.24.57

process.on("exit", function (code) {
    setInterval(function () {
        console.log('Process will exit what ever you do')
    }, 1000)
})

process.exit()

程序直接退出,没有执行exit事件

修改所在的时区

这个需求可能并不常见,但在某些情况下可能十分有用

假设开发者要向某台服务器提交数据,但没有和该服务器处在同一个时区内,这就导致开发者的时间和服务器的时间可能会相差几个小时,有的服务器会拒绝这样的请求

在旧的版本中,打印date对象返回的是当前时区的时间,但在新版本中直接返回的就是世界时,即greenwich时间,相比东八区要早8个小时,格式也不再是GMT格式,这代表就算要获取当前时间都要做一下额外转换

👇此时实际上为上午10点58分

截屏2020-08-29 上午11.00.06

使用Date对象提供的全局方法进行转换

截屏2020-08-29 上午11.03.49

getTimezoneOffset的方法可以得到当前的时区

截屏2020-08-29 上午11.07.32

在上面的代码中,虽然直接打印date对象显示的是greenwich时间,但执行getTimezoneOffset()方法返回的却是-480,表示偏移的分钟数,可以看出偏差8个小时;这代表Node其实知道当前位于哪个时区,但返回的还是greenwich时间

修改timezone

首先在Date对象的prototype上声明一个map结构作为属性,用于存储时区名称和偏移量的关系,然后对Date类的Date方法进行修改,如果没有声明process.env.TZ变量,就默认返回原来的date对象;如果声明了该属性,就先到对应的数组中进行搜索,然后返回修改后的date对象

process.env.TZ = "Asia/Shanghai";

Date.prototype.TimeZone = new Map([
    ['Europe/London',0],
    ['Asia/Shanghai',8],
    ['America/New_York',-5]
])
Date.prototype.zoneDate = function(){
    if(process.env.TZ === undefined){
        return new Date();
    }else{
        for (let item of this.TimeZone.entries()) {
            if(item[0] === process.env.TZ){
                let d = new Date();
                d.setHours(d.getHours()+item[1]);
                return d;
            }
        }
        return new Date();
    }
}

var date = new Date().zoneDate();
console.log(date);

开发者可能会担心d.getHours()+item[1]会出现大于24的情况,所幸setHours方法已经内置了对这种情况的处理,如果小时的范围小于0或者大于24,会对日期进行相应的加减

Timer

Node中的定时器都是全局方法,无须通过require来引入

常用API

setTimeout

使用setTimeout方法最简单的例子是延迟一个函数的执行时间

setTimeout(function () {
    console.log('hello')
}, 1000)

如果想要在回调执行前清除定时器,可以使用clearTimeout方法

var timeout = setTimeout(function () {
    console.log('hello')
}, 1000)

clearTimeout(timeout) 👈 hello不会被打印

setInterval

如果想要以一个固定的时间间隔运行回调函数,可以使用setInterval方法

setInterval(function () {
    console.log('hello')
}, 1000)

同样可以用clearInterval方法来清除定时器

var i = 0
var interval = setInterval(function () {
    console.log('hello')
    if (++i === 5) {
        clearInterval(interval)
    }
}, 1000)

回调函数的参数

在定时器中,第一个参数是回调方法,第二个参数是定时器的超时时间,其后面还可以定义更多的参数,多余的参数会被作为回调函数的参数

setTimeout(function (args) {
    console.log(args)
}, 1000, 'timeout') 👈 一秒后打印timeout

定时器中的this

在JavaScript中,setTimeoutsetInterval中的this均指向Windows。原因也很简单,定时器方法的第一个参数是一个匿名函数,而JavaScript中所有匿名函数的this都指向Windows

截屏2020-08-29 上午11.56.50

在Node中,setTimeoutsetInterval的this会指向timeout类,该类在setTimeoutsetInterval内部创建并返回,开发者通常不会直接用到两个类,但是可以打印出来

截屏2020-08-29 上午11.58.20

如果在setTimeout方法内部涉及this的指向问题,通常会使用bind或者call方法来重新绑定this

reference

《新时期的Node.js入门》 李锴