在寫 Flutter 和 Serverless 查資料的時候,在某個博客裏看到了 CLS 的相關內容,感受實際上是個很不錯的軟件工程的解耦想法,因而保存了下來。今天回過頭來仔細研究了一下並決定給本身留下一些記錄。javascript
不管是在瀏覽器,仍是在服務端 Node.js,咱們常常會碰到打點上報,追蹤錯誤這樣的需求,即便不對特定用戶進行追蹤,咱們也會給某個 session 分配惟一 ID 以在 log / analytics 界面可以看到用戶的完整行爲,對於產品分析與錯誤再現是十分重要的。html
假設咱們須要寫一個 error handling ,這個 error handling 會 hold 住全部的請求的異常,咱們如何分辨哪一個錯誤是哪一個請求形成的呢?java
log.error("Error occured", req);
複製代碼
那麼這個 error handling 就跟 req 耦合了node
假設咱們須要追蹤某個錯誤,是哪一個 user 產生的,又或者是哪一個錯誤,user 幹了什麼致使的?git
log.info("User has done xxx", user);
log.error("Error occured by", user);
複製代碼
因而跟 user 也深深的耦合了。github
單單這樣的例子好像沒有什麼大問題,不過多兩個參數嘛。但寫過大型應用的同窗,後期不斷增長功能的時候,你必定寫過那種長長的參數列表的函數,又或者是好幾百行的一個函數,實在是太不優雅,重構起來也太難。express
函數若是是同步的,那麼咱們能夠直接掛到全局變量(某個對象)下編程
const global = {};
$("button").click((event) => {
global.event = event;
log("button clicked");
});
function log(...args) {
console.log(global.event, ...args); // { x: xxx, y: xxx, target: xxx } 'button clicked'
// other logic
}
複製代碼
顯然這在異步中行不通json
const global = {};
$("button").click((event) => {
global.event = event;
setTimeout(() => {
log("button clicked");
}, 1000);
});
function log(...args) {
console.log(global.event, ...args);
// other logic
}
複製代碼
你會發現打印的 global.event 全變成了同一個對象api
咱們須要可以從始至終在同一個異步調用鏈中一個持續化的存儲, 又或者是咱們須要可以辨識當前的異步函數他的惟一辨識符,以和一樣內容的異步函數但並非自己的運行的這個做區分。
在其餘語言中,有一個叫作 Thread-local storage 的東西,然而在 Javascript 中,並不存在多線程這種概念(相對而言,Web Worker 等與主進程並不衝突),因而 CLS ,Continuation-local Storage,一個相似於 TLS,得名於函數式編程中的 Continuation-passing style,旨在鏈式函數調用過程當中維護一個持久的數據。
先看看是怎麼解決的
$('button').click(event => {
Zone.current.fork({
name: 'clickZone',
properties: {
event
}
}).run(
setTimeout(() => {
log('button clicked');
}, 1000);
);
});
function log(...args) {
console.log(global.event, ...args);
// other logic
}
複製代碼
Zone.js
是 Angular 2.0 引入的,固然它的功能不僅是提供 CLS,他還有其餘相關 API。
咱們試着思考一下, Zone.js
是如何作到這些的。若是瀏覽器沒有提供異步函數運行環境的惟一標識,那麼只剩下惟一的一條路,改寫全部會產生異步的函數,包裝了一層後也就能加入hook了。
我嘗試本身寫了一下 zone-simulate.js
const Zone = {
_currentZone: {},
get current() {
return {
...this._currentZone,
fork: (zone) => {
this._currentZone = {
...this._currentZone,
...zone,
};
return this;
},
set: (key, value) => {
this._currentZone[key] = value;
},
};
},
};
(() => {
const _setTimeout = global.setTimeout;
global.setTimeout = (cb, timeout, ...args) => {
const _currentZone = Zone._currentZone;
_setTimeout(() => {
const __after = Zone._currentZone;
Zone._currentZone = _currentZone;
cb(...args);
Zone._currentZone = __after;
}, timeout);
};
})();
for (let i = 0; i < 10; i++) {
const value = Math.floor(Math.random() * 100);
console.log(i, value);
Zone.current.fork({ i, value });
setTimeout(() => {
console.log(Zone.current.i, Zone.current.value);
}, value);
}
複製代碼
看似好像沒什麼問題,不過
angular with tsconfig target ES2017 async/await will not work with zone.js
咱們能夠作個實驗,在 console 裏敲下以下代碼
const _promise = Promise;
Promise = function () { console.log('rewrite by ourselves') };
new Promise(() => {}) instanceof Promise
// rewrite by ourselves
// true
async function test() {}
test() instanceof Promise
// false
test() instanceof _promise
// true
async function test() { return new Promise() }
test() instanceof Promise
// rewrite by ourselves
// false
test() instanceof _promise
// rewrite by ourselves
// true
複製代碼
也就是說瀏覽器會把 async 函數的返回值用原生 Promise 包裝一層,由於是原生語法,也就沒法 hook async 函數。 固然咱們能夠用 transpiler 把 async 函數改寫成 generator 或者 Promise,不過這並不表明是完美的。
Node.js 8後出現的 async_hook
模塊,到了版本14仍然沒有移去他身上的 Experimental
狀態。以及在剛出現的時候是有性能問題的討論(3年後的今天雖然不知道性能怎麼樣,不過既然沒有移去 Experimental
的標籤,若是追求高性能的話仍是應該保持觀望)
雖然沒有移去 Experimental 的狀態,可是穩定性應該沒有什麼太大問題,大量的 Node.js 的追蹤庫 / APM 依賴着 async_hooks 模塊,若是有重大問題,應該會及時上報並修復
對於性能問題,不展開篇幅討論,取決於你是否願意花一點點的性能降低來換取代碼的低耦合。
async_hooks 提供了一個 createHook 的函數,他能夠幫助你監聽異步函數的運行時建立以及退出等狀態,而且附帶了這個運行時的惟一辨識id,咱們能夠簡單地用它來建立一個 CLS。
const {
executionAsyncId,
createHook,
} = require("async_hooks");
const { writeSync: fsWrite } = require("fs");
const log = (...args) => fsWrite(1, `${args.join(" ")}\n`);
const Storage = {};
Storage[executionAsyncId()] = {};
createHook({
init(asyncId, _type, triggerId, _resource) {
// log(asyncId, Storage[asyncId]);
Storage[asyncId] = {};
if (Storage[triggerId]) {
Storage[asyncId] = { ...Storage[triggerId] };
}
},
after(asyncId) {
delete Storage[asyncId];
},
destroy(asyncId) {
delete Storage[asyncId];
},
}).enable();
class CLS {
static get(key) {
return Storage[executionAsyncId()][key];
}
static set(key, value) {
Storage[executionAsyncId()][key] = value;
}
}
// --- seperate line ---
function timeout(id) {
CLS.set('a', id)
setTimeout(() => {
const a = CLS.get('a')
console.log(a)
}, Math.random() * 1000);
}
timeout(1)
timeout(2)
timeout(3)
複製代碼
在社區中已經有了那麼多優秀實現的前提下,Node.js 13.10 後新增了一個 AsyncLocalStorage
的 API
實際上他已是開箱可用的 CLS 了
const {
AsyncLocalStorage,
} = require("async_hooks");
const express = require("express");
const app = express();
const session = new AsyncLocalStorage();
app.use((_req, _res, next) => {
let userId = Math.random() * 1000;
console.log(userId);
session.enterWith({ userId });
setTimeout(() => {
next();
}, userId);
});
app.use((_req, res, next) => {
const { userId } = session.getStore();
res.json({ userId });
});
app.listen(3000, () => {
console.log("Listen 3000");
});
const fetch = require('node-fetch')
new Array(10).fill(0).forEach((_, i) => fetch('http://localhost:3000/test', {
method: 'GET',
}).then(res => res.json()).then(console.log))
// Output:
// Listen 3000
// 355.9573987560112
// 548.3773445851497
// 716.2437886469793
// 109.84756385607896
// 907.6261832949347
// 308.34659685842513
// 407.0145853469649
// 525.820449114568
// 76.91502437038133
// 997.8611964598299
// { userId: 76.91502437038133 }
// { userId: 109.84756385607896 }
// { userId: 308.34659685842513 }
// { userId: 355.9573987560112 }
// { userId: 407.0145853469649 }
// { userId: 525.820449114568 }
// { userId: 548.3773445851497 }
// { userId: 716.2437886469793 }
// { userId: 907.6261832949347 }
// { userId: 997.8611964598299 }
複製代碼