Koa 依赖的库 on-finished 和 destroy

593 阅读3分钟

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

本文为 koa 依赖系列文章,前几篇也可以在站内查看:

on-finished 可以监听 response 的结束事件,在 koa application 的 handleRequest 中,通过 onFinished 来监听 res 提前结束的异常场景,在 response 的 body 中,如果传输的是 stream,通过 onFinished 来监听 res 结束来做清理工作。

on-finished 默认导出 onFinished 事件监听,同时也导出了判断方法 isFinished。

先看 isFinished 的实现:

function isFinished (msg) {
  var socket = msg.socket

  if (typeof msg.finished === 'boolean') {
    // OutgoingMessage
    return Boolean(msg.finished || (socket && !socket.writable))
  }

  if (typeof msg.complete === 'boolean') {
    // IncomingMessage
    return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable))
  }

  // don't know
  return undefined
}

isFinished 接收 msg 作为参数,这个 msg 可以传 IncomingMessage 或 OutgoingMessage,这里通过 finished 属性来判断消息类型,之后根据 socket 状态来判断是否为 finish 状态。

再看 onFinished 的实现,如果添加监听时已经是 isFinished 状态,这里会立即执行一次异步回调,这里的异步是使用 setImmediate 或 process.nextTick 实现的:

var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }

之后添加监听事件,这里会把事件监听器挂在 msg 对象的 __onFinished 属性上,多个事件使用 queue 保存。实际的 finish 事件绑定位于 attachFinishedListener 函数中:

function attachFinishedListener (msg, callback) {
  var eeMsg
  var eeSocket
  var finished = false

  function onFinish (error) {
    eeMsg.cancel()
    eeSocket.cancel()

    finished = true
    callback(error)
  }

  // finished on first message event
  eeMsg = eeSocket = first([[msg, 'end', 'finish']], onFinish)

  function onSocket (socket) {
    // remove listener
    msg.removeListener('socket', onSocket)

    if (finished) return
    if (eeMsg !== eeSocket) return

    // finished on first socket event
    eeSocket = first([[socket, 'error', 'close']], onFinish)
  }

  if (msg.socket) {
    // socket already assigned
    onSocket(msg.socket)
    return
  }

  // wait for socket to be assigned
  msg.on('socket', onSocket)

  if (msg.socket === undefined) {
    // istanbul ignore next: node.js 0.8 patch
    patchAssignSocket(msg, onSocket)
  }
}

这里首先监听 msg 的 end 和 finish 事件,如果有一个触发就直接执行回调,之后监听 socket 上的事件,如果收到 error 或 close 也会触发回调。对于旧版本 node,socket 为 assignSocket,因此这里做了一个 patchAssignSocket 的逻辑。

on-finished 库主要是做一些内部事件的监听,阅读源码可以了解事件的实际发生源。而前面提到,koa 在收到 on-finished 结束事件是会进行流的 destroy 处理,这里又实用了另一个库 destroy。

destroy 的用途是销毁一个 stream,在 koa 中用在 response body 的 setter 中:

if (val instanceof Stream) {
    onFinish(this.res, destroy.bind(null, val))
    // ...
}

如果是 Stream 类型数据,在收到 onFinish 时直接把数据传入 destroy 来清理。destroy 默认导出一个函数,接收 stream 参数:

function destroy (stream) {
  if (stream instanceof ReadStream) {
    return destroyReadStream(stream)
  }

  if (!(stream instanceof Stream)) {
    return stream
  }

  if (typeof stream.destroy === 'function') {
    stream.destroy()
  }

  return stream
}

内部最终调用的还是 stream 的 destroy 方法,只是对于 ReadStream 进行了一些特殊处理:如果是 ReadStream 类型,调用 destroy 后可能还会收到 open 事件,因此这里添加了一个 onOpenClose 方法来确保流可以被彻底销毁:

function destroyReadStream (stream) {
  stream.destroy()

  if (typeof stream.close === 'function') {
    // node.js core bug work-around
    stream.on('open', onOpenClose)
  }

  return stream
}

function onOpenClose () {
  if (typeof this.fd === 'number') {
    // actually close down the fd
    this.close()
  }
}

以上就是这两个库的全部源码了,这一部分其实比较简单,这两个库主要是对一些边界 case 进行统一化的处理和封装,逻辑比较简单。