2009 年 Node.js 出現時,JavaScript 的回呼模型是唯一的非阻塞 I/O 機制。等到 ES6 引入 Promise,ES2017 引入 async/await 時,大多數生產代碼庫已經運行了多年的基於回調的邏輯:嵌套的錯誤優先處理程序、通過閉包傳遞的共享可變狀態、嵌入在三層匿名函數中的重試邏輯。語法變了,但運行的程式碼卻沒變。如今,大多數工程團隊都面臨著同樣的問題:程式碼庫中新舊模式並存,團隊想要遷移,但係統卻無法在遷移過程中停止運作。
好消息是,遷移不需要重寫程式碼。回調、Promise 和 async/await 在明確定義的邊界內可以互通。 Node.js 也支援這些特性。 util.promisify 正是為了彌合錯誤優先回調和 Promise 返回函數之間的鴻溝。包裝層使得新舊程式碼在過渡期間能夠共存。每次遷移一個模組,逐步推進,確保生產環境持續運行,同時程式碼庫也不斷更新。真正的挑戰不在於轉換本身,而是如何系統地進行轉換:理解哪些回呼可以優先轉換,哪些反模式如果簡單地重寫會導致 bug,以及如何驗證每個轉換後的函數與它所替換的函數行為完全一致。
了解回調模型及其在規模化應用中失效的原因
Node.js 的回呼約定很簡單:執行非同步操作的函數接受一個回呼函數作為最後一個參數。回調函數接收錯誤作為第一個參數,結果作為第二個參數。 Node.js 中的每個標準函式庫函數都遵循此約定: fs.readFile, http.get, child_process.exec以及數百個其他問題。
JavaScript的
// Standard Node.js error-first callback pattern
const fs = require('fs');
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error('Failed to read config:', err);
return;
}
const config = JSON.parse(data);
console.log('Config loaded:', config);
});
對於單一操作來說,這種模式很簡單。但當操作需要鍊式執行時,它就失效了,因為每個後續操作都必須在前一個回呼函數內部啟動。一個三步驟操作(讀取、轉換和寫入)會產生三層嵌套的回呼函數。五步驟操作則會產生五層。開發者將這種結構稱為“回調地獄”或“死亡金字塔”,而這不僅僅是美觀問題。深度嵌套的回呼函數會導致錯誤處理不一致(每一層都必須獨立檢查其錯誤參數),使執行順序難以判斷,並且由於資料流是隱式的而非顯式的,重構也變得非常危險。
對於生產系統而言,更關鍵的問題在於回呼函數本身沒有原生的協調機制。運行兩個非同步操作並等待它們都完成需要手動追蹤計數器。對陣列執行一系列操作則需要遞歸模式或第三方函式庫,例如 async所有這些複雜性在函數簽名中都看不出來:基於回調的 API 和基於它構建的並發編排從外部看起來完全相同,直到它們失敗為止。
回調地獄的真實面貌
這是一個來自 Node.js 服務的真實回調金字塔,該服務讀取使用者資料、驗證權限並記錄存取:
JavaScript的
// Three-level callback pyramid -- representative of real legacy code
function getUserReport(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, user) => {
if (err) return callback(err);
if (!user) return callback(new Error('User not found'));
permissions.check(userId, 'read:reports', (err, allowed) => {
if (err) return callback(err);
if (!allowed) return callback(new Error('Permission denied'));
auditLog.write({ userId, action: 'read:reports' }, (err) => {
if (err) return callback(err);
// Finally, the actual work
callback(null, buildReport(user));
});
});
});
}
錯誤處理在每一層都重複進行。縮排使得資料流在視覺上顯得複雜。新增第四步需要再嵌套一層。測試此函數需要依序模擬所有三個依賴項,而模擬第三層的錯誤則需要模擬前兩個依賴項才能成功。隨著嵌套層級的成長,所有這些問題都會變得更加嚴重。
使用 util.promisify 橋接回調函數和 Promise
Node.js 8.0 引入 util.promisify它會將任何遵循標準錯誤優先回呼約定的函數轉換為傳回 Promise 的函數。這是在 Node.js 程式碼庫中進行任何 async/await 遷移的正確起點。
JavaScript的
const { promisify } = require('util');
const fs = require('fs');
// Convert Node.js built-ins
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
// Now usable with async/await
async function processConfig(path) {
const data = await readFile(path, 'utf8');
const config = JSON.parse(data);
config.lastLoaded = Date.now();
await writeFile(path, JSON.stringify(config, null, 2), 'utf8');
return config;
}
util.promisify 它會自動處理錯誤優先約定:如果回呼函數接收到非空的第一個參數,則傳回的 Promise 會因該錯誤而拒絕。如果回呼函數接收到空的第一個參數和一個結果,則 Promise 會因該結果而解析。對於絕大多數遵循此約定的 Node.js 核心 API 和第三方函式庫,無需進行任何自訂封裝。
承諾自訂回調函數
對於遵循錯誤優先原則但並非Node.js內建函數的自訂函數, util.promisify 工作原理完全相同:
JavaScript的
const { promisify } = require('util');
// Your existing callback-based function
function fetchUserFromDB(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, rows) => {
if (err) return callback(err);
callback(null, rows[0] || null);
});
}
// Promisified version -- no changes to the original function needed
const fetchUser = promisify(fetchUserFromDB);
// Use in async context
async function getUser(userId) {
const user = await fetchUser(userId);
if (!user) throw new Error(`User ${userId} not found`);
return user;
}
這種方法很重要: util.promisify 它對原始函數進行封裝,但不對其進行任何修改。原始回調版本仍然有效。尚未遷移的呼叫者繼續使用回呼版本。已遷移的呼叫者使用 Promism 化的版本。這種共存模式使得增量遷移成為可能。
處理非標準回調簽名
一些較舊的函式庫會將多個結果值傳遞給它們的回呼函數,這 util.promisify 僅解析為第一個值。對於這些情況, util.promisify.custom 該符號允許定義自訂承諾:
JavaScript的
const { promisify } = require('util');
// A function that passes two results to its callback
function parseData(input, callback) {
// callback(err, parsedData, metadata)
callback(null, { value: input.trim() }, { length: input.length });
}
// Custom promisification that returns both results
parseData[promisify.custom] = (input) => {
return new Promise((resolve, reject) => {
parseData(input, (err, data, meta) => {
if (err) reject(err);
else resolve({ data, meta });
});
});
};
const parseDataAsync = promisify(parseData);
const result = await parseDataAsync(' hello ');
// result === { data: { value: 'hello' }, meta: { length: 9 } }
將回呼函數轉換為 Promise:逐步指南
對於無法由以下方式處理的程式碼 util.promisify 直接來說,手動編寫 Promise 包裝器是遷移路徑。模式是一致的:
JavaScript的
// Step 1: Original callback-based function
function checkPermission(userId, resource, callback) {
acl.check({ userId, resource }, (err, result) => {
if (err) return callback(err);
callback(null, result.allowed);
});
}
// Step 2: Promise wrapper (coexists with the original)
function checkPermissionAsync(userId, resource) {
return new Promise((resolve, reject) => {
checkPermission(userId, resource, (err, allowed) => {
if (err) reject(err);
else resolve(allowed);
});
});
}
// Step 3: async/await consumer
async function authorizeRequest(userId, resource) {
const allowed = await checkPermissionAsync(userId, resource);
if (!allowed) {
throw new Error(`${userId} does not have access to ${resource}`);
}
}
之前提到的三級回呼函數金字塔,用 async/await 重寫後:
JavaScript的
// After migration: same logic, linear structure
async function getUserReport(userId) {
const user = await db.queryAsync('SELECT * FROM users WHERE id = ?', [userId]);
if (!user) throw new Error('User not found');
const allowed = await permissions.checkAsync(userId, 'read:reports');
if (!allowed) throw new Error('Permission denied');
await auditLog.writeAsync({ userId, action: 'read:reports' });
return buildReport(user);
}
現在,錯誤處理由呼叫點的單一 try/catch 語句完成,而不是在每一層都重複處理。縮排是扁平的。新增第四步需要再增加一步。 await 測試需要獨立模擬每個依賴項,順序不限。
使用 Promise.all 和 Promise.allSettled 實作並行執行
從回呼函數遷移到 async/await 時最常見的錯誤之一是順序執行本來可以並行執行的操作。回調函數本身就使並行執行變得相當複雜,以至於許多開發者預設使用順序鏈。而 async/await 又讓平行執行看起來像是順序執行,這可能會誤導開發者寫出比之前的回呼函數版本更慢的程式碼。
JavaScript的
// WRONG: sequential execution -- each awaits the previous result
async function loadDashboardData(userId) {
const profile = await fetchProfile(userId); // 100ms
const orders = await fetchOrders(userId); // 150ms
const reviews = await fetchReviews(userId); // 80ms
return { profile, orders, reviews }; // Total: ~330ms
}
// RIGHT: parallel execution with Promise.all
async function loadDashboardData(userId) {
const [profile, orders, reviews] = await Promise.all([
fetchProfile(userId),
fetchOrders(userId),
fetchReviews(userId),
]);
return { profile, orders, reviews }; // Total: ~150ms
}
Promise.all 如果數組中的任何 Promise 被拒絕,則此方法也適用。當獨立的操作可能失敗而不互相影響,並且呼叫者需要了解所有失敗情況時, Promise.allSettled 是正確的工具:
JavaScript的
// Promise.allSettled: runs all, reports success or failure per operation
async function syncAllSources(userId) {
const results = await Promise.allSettled([
syncFromGitHub(userId),
syncFromJira(userId),
syncFromSlack(userId),
]);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason.message);
if (failed.length > 0) {
console.warn('Some syncs failed:', failed);
}
return results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
}
這種模式在基於回呼的程式碼中沒有自然的對應物,除非使用手動計數器或類似函式庫的工具。 async.parallel遷移到 Promise.allSettled 這通常是傳統非同步程式碼庫中最具槓桿效應的變更之一。
避免循環等待反模式
這種順序執行而非並行執行的錯誤最常出現在迴圈內部:
JavaScript的
// WRONG: sequential -- processes items one at a time
async function processOrders(orderIds) {
const results = [];
for (const id of orderIds) {
const result = await processOrder(id); // blocks until each completes
results.push(result);
}
return results;
}
// RIGHT: parallel -- all orders processed concurrently
async function processOrders(orderIds) {
return Promise.all(orderIds.map(id => processOrder(id)));
}
// RIGHT (with concurrency limit): parallel but bounded
const pLimit = require('p-limit');
const limit = pLimit(5); // max 5 concurrent
async function processOrders(orderIds) {
return Promise.all(
orderIds.map(id => limit(() => processOrder(id)))
);
}
在基於回呼的程式碼從 async/await 遷移到 await 的過程中,循環等待模式是最常見的迴歸問題之一,因為回呼迫使開發人員明確地考慮並發性,而 async/await 則將其隱藏起來。
Async/Await 中的錯誤處理:取代回呼錯誤傳播
回呼函數依照約定傳遞錯誤:每個回呼函數的第一個參數都是錯誤或 null。這種方式雖然可行,但需要每個呼叫者手動檢查錯誤參數。而 async/await 透過 Promise 的拒絕機制傳遞錯誤,該機制與 JavaScript 原生的 try/catch 語句整合。
JavaScript的
// Callback error propagation: repeated at every level
function processPayment(orderId, callback) {
validateOrder(orderId, (err, order) => {
if (err) return callback(err); // propagate
chargeCard(order.amount, (err, charge) => {
if (err) return callback(err); // propagate again
updateInventory(orderId, (err) => {
if (err) return callback(err); // propagate again
callback(null, charge.id);
});
});
});
}
// Async/await: error propagation is automatic
async function processPayment(orderId) {
const order = await validateOrder(orderId); // throws on error
const charge = await chargeCard(order.amount); // throws on error
await updateInventory(orderId); // throws on error
return charge.id;
}
遷移過程中一個關鍵的考慮因素是保留錯誤上下文。基於回呼的程式碼通常會在每一層傳遞包含上下文資訊的錯誤。遷移到 async/await 時,請確保錯誤包裝能夠保留這些上下文資訊:
JavaScript的
// Preserving error context during async migration
async function processPayment(orderId) {
let order;
try {
order = await validateOrder(orderId);
} catch (err) {
throw new Error(`Payment validation failed for order ${orderId}: ${err.message}`);
}
try {
const charge = await chargeCard(order.amount);
await updateInventory(orderId);
return charge.id;
} catch (err) {
// Attempt rollback, then rethrow with context
await refundCharge(order.amount).catch(console.error);
throw new Error(`Payment processing failed for order ${orderId}: ${err.message}`);
}
}
未處理的承諾拒絕
基於回呼的程式碼會在忽略錯誤參數時靜默地吞掉錯誤。 Async/await 會產生未處理的 Promise 拒絕,這在 Node.js 15 及更高版本中預設會導致進程終止。這是一個遷移時的重大變更:之前靜默失敗的程式碼現在會崩潰。
JavaScript的
// This produces an unhandled rejection in Node.js 15+
async function riskyOperation() {
throw new Error('Something failed');
}
riskyOperation(); // Promise rejected, but rejection is not caught
// Fix: always await or chain .catch()
await riskyOperation(); // throws, caller handles it
riskyOperation().catch(console.error); // handles inline
遷移期間審核所有「即發即棄」的非同步函數呼叫。任何未等待或未鍊式呼叫返回值的非同步函數呼叫都應進行審核。 .catch() 在回呼函數中,這可能是靜默失敗;但在 async/await 中,則是未處理的拒絕崩潰。
將 EventEmitter 模式遷移到 Promise 和非同步迭代器
Node.js 的 EventEmitters 是一種回呼模式,它允許為已命名的事件註冊多個回呼函數。它們常見於串流、網路連線和自訂事件匯流排中。直接遷移到 async/await 需要對基於事件的 API 進行封裝。
JavaScript的
const { EventEmitter } = require('events');
// Original EventEmitter-based pattern
function fetchDataLegacy(source) {
const emitter = new EventEmitter();
setTimeout(() => {
emitter.emit('data', { records: [1, 2, 3] });
emitter.emit('end');
}, 100);
return emitter;
}
// Usage: callback registration
const stream = fetchDataLegacy('api');
stream.on('data', chunk => console.log('received', chunk));
stream.on('error', err => console.error('error', err));
stream.on('end', () => console.log('done'));
將一次性事件轉換為 Promise 非常簡單:
JavaScript的
// Converting a single-event completion to Promise
function waitForEvent(emitter, successEvent, errorEvent = 'error') {
return new Promise((resolve, reject) => {
emitter.once(successEvent, resolve);
emitter.once(errorEvent, reject);
});
}
async function fetchData(source) {
const emitter = fetchDataLegacy(source);
const data = await waitForEvent(emitter, 'data');
await waitForEvent(emitter, 'end');
return data;
}
對於發出多個資料事件的流,Node.js 提供 events.on 它會傳回一個非同步迭代器,允許使用整個流進行消費。 for await...of:
JavaScript的
const { on } = require('events');
async function processStream(readable) {
for await (const chunk of on(readable, 'data')) {
await processChunk(chunk);
}
}
自 Node 10 起,Node.js 可讀流也可以作為非同步迭代器直接迭代:
JavaScript的
const fs = require('fs');
async function countLines(filePath) {
let lines = 0;
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
for await (const chunk of stream) {
lines += chunk.split('\n').length - 1;
}
return lines;
}
TypeScript 非同步/等待遷移模式
TypeScript 程式碼庫在遷移到 async/await 時需要考慮一些額外因素。返回類型必須從回呼簽名更新為 Promise<T>編譯器強制規定 await 只能在非同步函數內部使用。
打字稿
// Before: callback signature
function fetchUser(
id: string,
callback: (err: Error | null, user: User | null) => void
): void {
db.findOne({ id }, callback);
}
// After: async/await signature with proper return type
async function fetchUser(id: string): Promise<User> {
const user = await db.findOneAsync<User>({ id });
if (!user) throw new Error(`User ${id} not found`);
return user;
}
TypeScript 嚴格的空值檢查以一種能夠捕捉常見遷移錯誤的方式與非同步程式碼互動。如果函數之前傳回空值,則 TypeScript 會檢查該函數是否傳回空值。 User | null 透過回調函數,遷移將其更改為 Promise<User> (拋出異常而不是返回 null),TypeScript 將捕獲那些檢查過 null 但不再需要檢查的呼叫者,以及那些沒有檢查錯誤但現在需要處理的呼叫者。
對於使用以下程式碼的舊版 TypeScript 程式碼 @types/node 回調函數簽名, util.promisify 它具有完全的型別特徵,並能自動推斷 Node.js 內建函數的正確 Promise 回傳類型。
生產系統的增量遷移策略
生產系統無法一次全部遷移。增量式遷移方法是每次轉換一個模組,驗證其有效性後再進行下一個模組的遷移。關鍵在於在每個階段都要保持已轉換代碼和未轉換代碼之間的清晰界限。
遷移順序應遵循依賴關係方向:先轉換最深層的依賴項,然後向上轉換呼叫者。這樣可以確保在轉換更高層級的函數時,其相依性已經傳回 Promise,不再需要包裝層。
JavaScript的
// Stage 1: Promisify the data layer (deepest dependency)
const db = {
queryAsync: promisify(db.query.bind(db)),
insertAsync: promisify(db.insert.bind(db)),
};
// Stage 2: Convert the service layer (depends on db)
class UserService {
async getUser(id) {
return db.queryAsync('SELECT * FROM users WHERE id = ?', [id]);
}
async createUser(data) {
return db.insertAsync('users', data);
}
}
// Stage 3: Convert the controller layer (depends on service)
// -- only after Stage 2 is validated and deployed
async function handleGetUser(req, res) {
try {
const user = await userService.getUser(req.params.id);
res.json(user);
} catch (err) {
res.status(500).json({ error: err.message });
}
}
這種分階段的方法得到了分析工具的直接支持。正如在…中所討論的 數據和控制流分析在修改非同步層之前,先理解資料如何在非同步層中流動是安全增量重構的前提。依賴關係的方向決定了哪些模組可以先安全地進行轉換,哪些模組必須等到其依賴項遷移完畢後才能轉換。
為了向後相容而添加的包裝層
在遷移過程中,有些呼叫者仍然會期望使用基於回呼的 API。 util.callbackify 函數是以下函數的反函數 util.promisify它將非同步函數轉換回錯誤優先回調介面:
JavaScript的
const { callbackify } = require('util');
// New async implementation
async function fetchUserAsync(id) {
return db.queryAsync('SELECT * FROM users WHERE id = ?', [id]);
}
// Backward-compatible callback version for unconverted callers
const fetchUser = callbackify(fetchUserAsync);
// Old callers continue to work unchanged
fetchUser(userId, (err, user) => {
if (err) return handleError(err);
render(user);
});
// New callers use the async version directly
const user = await fetchUserAsync(userId);
這種雙向相容性意味著轉換並非一項強制性事件。各個模組可以在任何迭代周期內進行轉換,無需同時與所有呼叫方協調。
遷移過程中常見的非同步/等待陷阱
非同步函數呼叫中缺少 await 關鍵字。
最常見的遷移錯誤是在呼叫非同步函數時沒有等待其完成。除非函數被拒絕,否則 JavaScript 執行時不會察覺到這種錯誤,而且這種拒絕會變成未處理的 Promise 拒絕,而不是拋出的錯誤。
JavaScript的
// Bug: missing await -- function runs but result is a Promise, not the user
async function updateUserName(id, name) {
const user = fetchUser(id); // BUG: forgot await, user is a Promise object
user.name = name; // setting .name on a Promise, not a user
await saveUser(user); // saves the Promise object
}
// Fix
async function updateUserName(id, name) {
const user = await fetchUser(id);
user.name = name;
await saveUser(user);
}
TypeScript 和 ESLint 的 no-floating-promises 此規則會自動捕獲此模式。強烈建議在遷移過程中加入此 lint 規則。
數組方法中的非同步函數
Array.prototype.forEach 不會等待異步回調。這會產生與循環中使用 await 相同的順序執行但錯誤的行為,只不過這段程式碼表面上看起來可以工作,但它會在不等待任何回調的情況下靜默地並發執行所有回調:
JavaScript的
// Bug: forEach does not await async callbacks
async function processAll(ids) {
ids.forEach(async (id) => {
await processItem(id); // these run concurrently, forEach completes immediately
});
// function returns before any processItem completes
}
// Fix: use Promise.all with map
async function processAll(ids) {
await Promise.all(ids.map(id => processItem(id)));
}
try/catch 無法捕捉 await 以外的非同步錯誤
try/catch 程式碼區塊只會捕捉 await 表達式中的錯誤。沒有 await 的非同步函數呼叫不會被外面的 try/catch 程式碼區塊捕獲:
JavaScript的
// Bug: the rejection from riskyOp() is not caught
async function run() {
try {
riskyOp(); // not awaited -- rejection escapes the try/catch
} catch (err) {
console.error(err); // never reached
}
}
// Fix: await inside the try block
async function run() {
try {
await riskyOp();
} catch (err) {
console.error(err);
}
}
遷移後測試非同步程式碼
遷移後的非同步函數需要非同步測試用例。現代測試框架原生支援這一點。
JavaScript的
// Jest async test patterns
describe('UserService', () => {
// Pattern 1: async/await in test
test('fetches user by id', async () => {
const user = await userService.getUser('user-123');
expect(user.id).toBe('user-123');
});
// Pattern 2: testing rejection
test('throws when user not found', async () => {
await expect(userService.getUser('nonexistent'))
.rejects.toThrow('User nonexistent not found');
});
// Pattern 3: parallel setup
beforeAll(async () => {
await db.connect();
await db.seed(testData);
});
afterAll(async () => {
await db.cleanup();
await db.disconnect();
});
});
在驗證遷移後的函數是否與其回呼函數的前身產生相同的輸出時,比較測試是有效的:
JavaScript的
// Comparison test: callback version vs. async version must agree
test('async version matches callback version output', async () => {
const callbackResult = await promisify(fetchUserLegacy)('user-123');
const asyncResult = await fetchUserAsync('user-123');
expect(asyncResult).toEqual(callbackResult);
});
這種測試模式在過渡時期尤其有用,因為此時兩個版本並行運行。如同在以下背景所考察的: 在進行程式碼變更之前,請先進行影響分析。驗證重構後的函數是否產生與其前身相同的輸出,是在刪除舊版之前,對遷移是否正確的證據性確認。
SMART TS XL 支援大規模安全異步遷移。
對於回調鏈跨越多個檔案、服務和團隊的企業程式碼庫而言,安全遷移的首要要求是現有非同步依賴關係的完整映射:哪些函數呼叫哪些函數,它們之間傳遞哪些數據,哪些鍊是應用程式核心操作的關鍵路徑,以及哪些是可以獨立遷移的隔離實用程式。
SMART TS XL 它透過解析整個程式碼庫(而不僅僅是單一檔案)來建立此依賴關係圖。它解析了基於回調的函數如何在模組邊界之間相互引用,識別了哪些共享狀態透過閉包傳遞,並將必須在遷移過程中保持完整的執行鏈視覺化。這種結構分析為本指南中所述的分階段遷移方法提供了輸入:哪些模組位於依賴關係圖的底部,可以先進行轉換,哪些模組必須等到其依賴項遷移完畢後才能轉換。
平台的 影響分析 此功能擴展到變更評估。在將基於回呼的模組轉換為傳回 Promise 之前,影響分析會識別程式碼庫中所有使用回呼介面呼叫該模組的其他模組。這些呼叫者是下一階段的遷移範圍:它們必須同時轉換,或使用向後相容的包裝器。 util.callbackify 必須維護這些枚舉,直到它們被轉換為止。如果沒有這種枚舉,遷移將在未知範圍的情況下進行,並且當未被識別的呼叫者遇到預期回呼的 Promise 時,會產生意外的故障。
對於混合使用 JavaScript 和 TypeScript 的程式碼庫,或呼叫其他語言編寫的後端服務的程式碼庫, SMART TS XL“ 跨語言依賴分析 它提供了完整執行路徑的可見性,而不僅僅是 JavaScript 層。如果回呼鏈最終調用的是用 Java 或 Python 編寫的外部服務,那麼它就存在單語言工具無法識別的依賴關係,而忽略這些依賴關係的遷移規劃是不完整的。 依賴關係可視化 每 SMART TS XL 提供此功能,使這些跨邊界關係在進行任何遷移變更之前可見。
持續推進遷移:從回呼到整個程式碼庫的 Async/Await
從回呼函數到 async/await 的轉換並非在第一個模組轉換完成後就完成。只有當最後一個包裝層被移除,且程式碼庫中不再有任何殘留時,轉換才算完成。 callback 其核心邏輯遵循約定。要實現這一點,需要在整個遷移過程中嚴格遵守規範:新程式碼必須使用 async/await 編寫,包裝層必須視為臨時層,且 ESLint 規則必須強制要求轉換後的模組中不得引入回調式函數。
完成遷移的實際標誌是:否 util.promisify 應用程式程式碼中的呼叫(僅在過渡期需要),無 (err, result) => 核心業務邏輯中的模式(它們已被非同步函數中的 try/catch 語句取代),不再需要手動編寫 Promise 建構函數。 async/await 這就足夠了,而且 Promise.all 在所有以前依序獨立運作的場所。
這些指標均可透過靜態分析進行衡量,這意味著可以客觀地追蹤和報告進度,而非僅依靠估算。對於大規模團隊而言,自動化靜態分析(用於尋找回呼模式)與依賴關係分析(用於確定每個遷移階段的範圍)相結合,是決定遷移能否按時完成的關鍵所在,否則遷移將因範圍未完全明確而無限期地持續下去。