Node中console.log的同步實現
console.log相信使用過js的朋友都不會陌生,對於我這種前端轉過來的node開發者,用起這個函式更是毫不手軟,使用它把需要的資訊列印到標準輸出,覺得就是1+1=2那麼正常,但是有天在網上看到一個問題console.log到底是非同步還是同步?我覺得很詫異,這還是個問題麼?當然是同步啦。但是問題的答案出乎我的意料,上面告訴我是要分情況的,根據process.stdout的情況可能會出現非同步的情況。我當時眉頭一皺,才發現問題確實不是我想的那麼簡單,於是在Node的文件中發現了這一段提示:
Writes may be synchronous depending on what the stream is connected to and whether the system is Windows or POSIX: Files: synchronous on Windows and POSIX TTYs (Terminals): asynchronous on Windows, synchronous on POSIX Pipes (and sockets): synchronous on Windows, asynchronous on POSIX 複製程式碼
當我發現自己對這個知識存在盲區後,趕緊深入核心去看看到底是啥情況,我選擇了最常用的POSIX上的TTYs來深入理解。
從console.log出發
首先我在node原始檔中從lib/console.js找到了console.log的程式碼:
Console.prototype.log = function log(...args) { write(this._ignoreErrors, this._stdout, util.format.apply(null, args), this._stdoutErrorHandler, this[kGroupIndent]); }; 複製程式碼
這個中間包含了一些格式化字串之類的東西,不過其核心還是很明顯的就是write函式中的
stream.write(string, errorhandler); 複製程式碼
而stream就是this._stdout
,從程式碼:
module.exports = new Console(process.stdout, process.stderr); module.exports.Console = Console; 複製程式碼
中我們又可以知道,這個this._stout
就是process.stdout
,那上面的問題也解釋的通了,所以console.log到底是同步輸出還是非同步輸出還真得看情況了。
process.stdout的實現
現在我們將目光轉向process.stdout
,對於這個屬性的定義在lib/internal/process/stdiso.js中,通過分析該檔案,我們可以發現stdout的stream是這樣定義的:
const tty_wrap = process.binding('tty_wrap'); switch (tty_wrap.guessHandleType(fd)) { case 'TTY': var tty = require('tty'); stream = new tty.WriteStream(fd); stream._type = 'tty'; break; case 'FILE': var fs = require('internal/fs'); stream = new fs.SyncWriteStream(fd, { autoClose: false }); stream._type = 'fs'; break; case 'PIPE': case 'TCP': var net = require('net'); stream = new net.Socket({ fd: fd, readable: false, writable: true }); stream._type = 'pipe'; break; default: // Probably an error on in uv_guess_handle() throw new errors.Error('ERR_UNKNOWN_STREAM_TYPE'); } // For supporting legacy API we put the FD here. stream.fd = fd; stream._isStdio = true; 複製程式碼
從上面就可以看出文件中的提示,file狀態使用了fs.SyncWriteStream
自然是同步的,而PIPE是用net.Socket
實現的,在Posix標準的機器上自然是非同步的。而讓我最困惑的是TTY的實現方式,其中的tty.WriteStream
在lib/tty.js中是這樣實現的:
function WriteStream(fd) { ... net.Socket.call(this, { handle: new TTY(fd, false), readable: false, writable: true }); this._handle.setBlocking(true); ... } inherits(WriteStream, net.Socket); 複製程式碼
可以看到TTY時的stream是繼承net.Socket
的,連new的時候建構函式都是直接呼叫它的建構函式,只是handle是TTY物件的。剛剛才說過net.Socket
不應該是非同步的嗎?到這兒來咋就成非同步的了呢?這個時候我就產生了一絲不解,想知道它是如何使TTY方式下的stdout變成同步的。於是翻起了原始碼,既然tty的stream是繼承net.Socket
所以,而net.Socket
物件是一個標準的node流物件,他直接繼承自stream.Duplex
這個雙全工的流物件,所以我們可以直接到lib/_stream_writable.js中找到方法Writable.prototype.write
,通過分析它的程式碼我們可以知道實際上呼叫的是Socket.prototype._writeGeneric
函式,而在這個函式中會根據不同的字元型別選擇呼叫不同的stream方法:
switch (encoding) { case 'latin1': case 'binary': return handle.writeLatin1String(req, data); case 'buffer': return handle.writeBuffer(req, data); case 'utf8': case 'utf-8': return handle.writeUtf8String(req, data); case 'ascii': return handle.writeAsciiString(req, data); case 'ucs2': case 'ucs-2': case 'utf16le': case 'utf-16le': return handle.writeUcs2String(req, data); default: return handle.writeBuffer(req, Buffer.from(data, encoding)); } 複製程式碼
而這個例子中的handle為TTY的例項,TTY是通過process.bingding
得到的,所以這些方法是NODE_BUILTIN_MODULE
的方法。上面的這些方法都是呼叫src/stream_base.cc中的模板函式
template <enum encoding enc> int StreamBase::WriteString(const FunctionCallbackInfo<Value>& args) 複製程式碼
從這個函式中我們可以看到,雖然編碼不同會造成在生成stack_storage
值時所用的處理方式不同,但是最後都是通過
err = DoWrite( req_wrap, &buf, 1, reinterpret_cast<uv_stream_t*>(send_handle)); 複製程式碼
操作來完成寫操作的,DoWrite
是個純虛擬函式,這個函式的實際定義實在TTYWrap的基類,streamBase的派生類中定義的,在檔案src/stream_wrap.cc中定義,在其中使用了libuv的方法uv_write
來執行io真正的寫操作。緊接著我又把目光轉向了檔案deps/uv/src/unix/stream.c中的這個方法,其中呼叫了uv_write2
,這是libuv中一個很經典的非同步方法,但是也有特例從其中執行寫操作的實際函式uv__write
中我們可以看到,如果當前的這個流設定了UV_STREAM_BLOCKING
標記,則會一直同步寫完,並不會出現非同步操作。那我們的TTY是在哪兒設定的這個標記?我們可以回到lib/tty.js中的這句話:
net.Socket.call(this, { handle: new TTY(fd, false), readable: false, writable: true }); 複製程式碼
這裡TTY物件剛剛我們說了是node的內部物件,所以這裡實際會呼叫的是src/tty_wrap.cc中的void TTYWrap::New(const FunctionCallbackInfo<Value>& args)
函式,其中通過:
TTYWrap* wrap = new TTYWrap(env, args.This(), fd, args[1]->IsTrue(), &err); 複製程式碼
生成TTYWrap的例項,而TTYWrap物件的建構函式中通過:
uv_tty_init(env->event_loop(), &handle_, fd, readable); 複製程式碼
初始化tty的libuv stream handle,從uv_tty_init
的程式碼中可以知道,當readable引數為false時就會給handle設定UV_STREAM_BLOCKING
標記,而readable引數是通過new TTY(fd, false)
第二個引數傳入的,剛好是false,所以process.stdout自然是同步的咯。
總結
以前一直覺得自己對node已經很熟悉了,發現是net.Socket
的流操作時,雖然有點困惑,但也是覺得可能handle是TTY的例項,寫操作會不一樣,但是在原始碼中一步步探索,最後發現還是通過libuv的uv_write2的時候,變得異常困惑,因為之前一直覺得它就是通過非同步來完成寫操作的,而忽略了設定UV_STREAM_BLOCKING
的情況,最後是在通過在uv_tty_init和其他的流初始化中比較,發現了tty中出現了設定UV_STREAM_BLOCKING
的情況,再回過頭去找,才發現了設定該標誌的寫操作是同步的情況。通過這件事還是明白了,很多東西不能想當然,得自己多探索瞭解才能在技術上面沉澱的更多,希望我的這篇文章也同時能幫助到大家。