Node Js Callback trap

本文探讨了Node.js中常见的回调陷阱,即回调嵌套问题,并介绍了几种解决方法,包括递归、第三方库如Async和Promise,以及使用ES6 Generator特性来优雅地处理异步流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言|nodeJs 回调陷阱是一种回调嵌套现象,给调试代码阅读造成困扰,利用Promise 可以并列书写回调方式

Nodejs最大的亮点就在于事件驱动, 非阻塞I/O 模型,这使得Nodejs具有很强的并发处理能力,非常适合编写网络应用。在Nodejs中大部分的I/O操作几乎都是异步的,也就是我们处理I/O的操作结果基本上都需要在回调函数中处理,比如下面的这个读取文件内容的函数:

复制代码代码如下:

fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  console.log(data);
});

那,我们读取两个文件,将这两个文件的内容合并到一起处理怎么办呢?大多数接触js不久的人可能会这么干:

复制代码代码如下:

fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  fs.readFile('/etc/passwd2', function (err, data2) {
    if (err) throw err;
    // 在这里处理data和data2的数据
  });
});

那要是处理多个类似的场景,岂不是回调函数一层层的嵌套啊,这就是大家常说的回调金字塔或回调地狱(http://callbackhell.com/)的问题,也是让js小白最为头疼的问题。

这种层层嵌套的代码给开发带来了很多问题,主要体现在:

1.代码可能性变差
2.调试困难
3.出现异常后难以排查

本文主要是介绍如何优雅的处理以上异步回调问题。

初级方案:通过递归处理异步回调

我们可以使用递归作为代码的执行控制工具。把需要执行的操作封装到一个函数中,在回调函数中通过递归调用控制代码的执行流程,废话不多说,上个代码吧:

复制代码代码如下:

var fs = require('fs');
// 要处理的文件列表
var files = ['file1', 'file2', 'file3'];

function parseFile () {
  if (files.length == 0) {
    return;
  }
  var file = files.shift();
  fs.readFile(file, function (err, data) {
    // 这里处理文件数据
    parseFile();  // 处理完毕后,通过递归调用处理下一个文件
  });
}

// 开始处理
parseFile();

以上代码已依次处理数组中的文件为例,介绍了通过递归的方式控制代码的执行流程。

应用到一些简单的场景中还是不错的,比如:我们将一个数组中的数据,依次保存到数据库中就可以采用这种方式。

通过递归的方式可以解决一些简单的异步回调问题。不过对于处理复杂的异步回调还是显得有些无能为力(如需要同步多个异步操作的结果)。

华丽点:采用Async、Q、Promise等第三方库处理异步回调

为了更好的处理嵌套回调的问题,可以考虑采用一些第三方专门处理异步的库,当然有能力的完全可以自己写个异步处理的辅助工具。

比较常用的处理异步的库有:async,q还有promise。从npmjs.org网站上来看,async的火热程度最高。以前用过async,确实也挺方便的,各种异步处理的控制流实现的也挺好。

我们将最初的同时读取两个文件的代码使用async处理下,示例如下:

复制代码代码如下:

var async = require('async')
  , fs = require('fs');

async.parallel([
  function(callback){
    fs.readFile('/etc/passwd', function (err, data) {
      if (err) callback(err);
      callback(null, data);
    });
  },
  function(callback){
    fs.readFile('/etc/passwd2', function (err, data2) {
      if (err) callback(err);
      callback(null, data2);
    });
  }
],
function(err, results){
  // 在这里处理data和data2的数据,每个文件的内容从results中获取
});

通过async模块,可以很好的控制异步的执行流程了,也算是解决了层层回调的问题,代码比以前算是清晰了些,不过依旧还是离不开回调函数。

想想如果能够在不使用回调函数的情况下,处理异步,岂不是很爽,接下来,我们谈谈使用ES6的新特性来实现这一目标。

优雅点:拥抱ES6,替代回调函数,解决回调地狱问题

话说EcmaScript Harmony (ES6)给js引入了不少新特性,对ES6不太了解的同学,可以自行百度一下。

在nodejs中使用ES6的新特性,需要用v0.11.x以上的版本才行。

本文介绍的是使用Generator特性替代回调函数,对Generator不了解?可以看看这里。

这里用到了co和thunkify两个模块,大家使用npm install命令安装之。

还是以本文刚开始提到的问题为例,使用generator特性的实例代码如下:

复制代码代码如下:

var fs = require('fs')
  , co = require('co')
  , thunkify = require('thunkify');

var readFile = thunkify(fs.readFile);

co(function *() {
  var test1 = yield readFile('test1.txt');
  var test2 = yield readFile('test2.txt');
  var test = test1.toString() + test2.toString();
  console.log(test);
})();

处理代码中的异常也是很简单的,只需要这样就OK了:

复制代码代码如下:

try {
  var test1 = yield readFile('test1.txt');
} catch (e) {
  // 在这里处理异常
}

这种代码是不是优雅很多了?像写同步代码一样处理异步,是不是很爽!

nodejs领域中进行Web开发,最火的框架莫过于express了,值得一提的是express的核心成员TJ大神有领导了一个新的Web框架——koa,宣称是下一代的Web开发框架,koa真是借助了ES6的generator这一特性,让我们在开发Web系统的时候避免陷入层层的回调用。

总结

class LazyLoader { constructor(dataManager) { this.dataManager = dataManager; this.cache = new Map(); this.entityTypeMap = { bancai: 'bancais', dingdan: 'dingdans', mupi: 'mupis', chanpin: 'chanpins', kucun: 'kucuns', dingdan_bancai: 'dingdan_bancais', chanpin_zujian: 'chanpin_zujians', zujian: 'zujians', caizhi: 'caizhis', dingdan_chanpin: 'dingdan_chanpins', user: 'users', jinhuo: 'jinhuos' }; this.proxies = new WeakMap(); } createProxy(entity, entityType) { // 1. 优先检查缓存 if (this.proxies.has(entity)) { return this.proxies.get(entity); } const cacheKey = `${entityType}_${entity.id}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } // 2. 代理检测简化 const handler = { get: (target, prop, receiver) => { // 特殊属性处理 if (prop === 'id') return target.id; const value = Reflect.get(target, prop, receiver); // 基本类型直接返回 if (typeof value !== 'object' || value === null) { return value; } // 处理数组引用 if (Array.isArray(value)) { const refType = this.getReferenceType(prop); return refType ? this.loadReferences(value, refType) : value; } // 处理对象引用 const refType = this.getReferenceType(prop); if (refType) { //console.log(refType) return this.loadReference(value, refType); } // 检查是否已经代理过这个对象 if (this.proxies.has(value)) { return this.proxies.get(value); } return value; } }; const proxy = new Proxy(entity, handler); // 3. 创建后立即缓存 this.proxies.set(entity, proxy); this.cache.set(cacheKey, proxy); return proxy; } getEntityTypeFromRef(prop) { const baseProp = prop.replace(/\d/g, ''); return baseProp in this.entityTypeMap ? this.entityTypeMap[baseProp] : `${baseProp}s`; } getReferenceType(prop) { try { const baseProp = prop.replace(/\d/g, ''); if (this.entityTypeMap[baseProp]) return this.entityTypeMap[baseProp]; const pluralProp = `${baseProp}s`; if (this.dataManager._rawData[pluralProp]) return pluralProp; } catch (error) { console.log(prop+" ---- "+error) } return null; } loadReference(ref, refType) { if (!ref?.id) { return ref;} const cacheKey = `${refType}_${ref.id}`; // 4. 统一使用缓存机制 if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } const entities = this.dataManager._rawData[refType] || []; const entity = entities.find(e => e.id === ref.id); if (!entity) { console.warn(`Entity not found: ${refType} with id ${ref.id}`); return ref; } // 5. 使用createProxy确保代理一致性 const prosty= this.createProxy(entity, refType); return prosty } loadReferences(refs, refType) { return refs.map(ref => this.loadReference(ref, refType)); } resolveReferences(entity) { for (const attr in entity) { const refType = this.getReferenceType(attr); if (!refType) continue; if (Array.isArray(entity[attr])) { entity[attr] = entity[attr].map(item => this.dataManager._rawData[refType]?.find(e => e.id === item.id) || item ); } else if (entity[attr]?.id) { entity[attr] = this.dataManager._rawData[refType]?.find(e => e.id === entity[attr].id) || entity[attr]; } } return entity; } clearCache() { this.cache.clear(); } } class MiniProgramDataManager { constructor(baseUrl = '') { this.baseUrl = baseUrl; this.debug = true; this.networkAvailable = false; this.isSyncing = false; this.lastSync = null; this.syncInterval = 5 * 60 * 1000; this.storageKey = 'miniProgramData'; this._rawData = this.createEmptyData(); this.lazyLoader = new LazyLoader(this); this.callbacks = { all: [], bancais: [], dingdans: [], mupis: [], chanpins: [], kucuns: [], chanpin_zujians: [], dingdan_bancais: [], zujians: [], caizhis: [], dingdan_chanpins: [], users: [], jinhuos: [] }; this.initNetwork(); this.loadDataFromStorage(); this.startAutoSync(); } createEmptyData() { return { bancais: [], dingdans: [], mupis: [], chanpins: [], kucuns: [], dingdan_bancais: [], chanpin_zujians: [], zujians: [], caizhis: [], dingdan_chanpins: [], users: [], jinhuos: [], _lastModified: null, _lastSync: null }; } get data() { const handler = { get: (target, prop) => { if (prop.startsWith('_')) return target[prop]; if (Array.isArray(target[prop])) { return target[prop].map(item => this.lazyLoader.createProxy(item, prop.replace(/s$/, '')) ); } return target[prop]; }, set: (target, prop, value) => { target[prop] = value; return true; } }; return new Proxy(this._rawData, handler); } async initialize() { try { await this.syncData(); return true; } catch (error) { if (this._rawData._lastSync) return true; throw error; } } startAutoSync() { this.autoSyncTimer = setInterval(() => { !this.isSyncing && this.syncData(); }, this.syncInterval); } stopAutoSync() { clearInterval(this.autoSyncTimer); } async initNetwork() { try { const { networkType } = await wx.getNetworkType(); this.networkAvailable = networkType !== 'none'; } catch { this.networkAvailable = false; } } async syncData() { if (this.isSyncing) return; this.isSyncing = true; try { const since = this._rawData._lastSync; await this.fetchAll(since); this.lazyLoader.clearCache(); this.saveDataToStorage(); this.triggerCallbacks('refresh', 'all', this.data); } catch (error) { console.error('Sync failed:', error); this.triggerCallbacks('sync_error', 'all', { error }); if (!this._rawData._lastSync) throw error; } finally { this.isSyncing = false; } } async fetchAll(since) { try { const params = since ? { since } : {}; const resolvedData = this.baseUrl ? await this.request('/app/all', 'GET', params) : this.createEmptyData(); Object.keys(this._rawData).forEach(key => { if (key.startsWith('_') || !resolvedData[key]) return; if (since) { resolvedData[key].forEach(newItem => { const index = this._rawData[key].findIndex(item => item.id === newItem.id); index >= 0 ? this._rawData[key][index] = newItem : this._rawData[key].push(newItem); }); } else { this._rawData[key] = resolvedData[key]; } }); this._rawData._lastSync = new Date().toISOString(); this.saveDataToStorage(); return true; } catch (error) { console.error('Fetch error:', error); this.triggerCallbacks('fetch_error', 'all', { error }); throw error; } } async request(url, method, data, retryCount = 3) { return new Promise((resolve, reject) => { const fullUrl = `${this.baseUrl}${url}`; const requestTask = () => { wx.request({ url: fullUrl, method, data, header: { 'Content-Type': 'application/json' }, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data.data); } else { const err = new Error(res.data?.message || 'API error'); retryCount > 1 ? setTimeout(requestTask, 1000, retryCount - 1) : reject(err); } }, fail: (err) => { retryCount > 1 ? setTimeout(requestTask, 1000, retryCount - 1) : reject(new Error(`Network error: ${err.errMsg}`)); } }); }; requestTask(); }); } registerCallback(entity, callback) { this.callbacks[entity]?.push(callback) || this.callbacks.all.push(callback); } unregisterCallback(entity, callback) { const arr = this.callbacks[entity] || this.callbacks.all; const index = arr.indexOf(callback); if (index !== -1) arr.splice(index, 1); } triggerCallbacks(operation, entity, data) { this.callbacks.all.forEach(cb => cb(operation, entity, data)); this.callbacks[entity]?.forEach(cb => cb(operation, data)); } async crudOperation(operation, entity, data) { try { const result = await this.request(`/app/${operation}/${entity}`, 'POST', data); this.updateLocalData(operation, entity, result || data); this.triggerCallbacks(operation, entity, result || data); return result; } catch (error) { this.triggerCallbacks(`${operation}_error`, entity, { data, error }); throw error; } } updateLocalData(operation, entity, data) { const key = `${entity}s`; const collection = this._rawData[key] || []; switch (operation) { case 'add': collection.push(data); break; case 'update': const index = collection.findIndex(item => item.id === data.id); index >= 0 ? collection[index] = data : collection.push(data); break; case 'delete': const deleteIndex = collection.findIndex(item => item.id === data.id); if (deleteIndex >= 0) collection.splice(deleteIndex, 1); break; } this._rawData._lastModified = new Date().toISOString(); this.lazyLoader.clearCache(); this.saveDataToStorage(); } loadDataFromStorage() { try { const storedData = wx.getStorageSync(this.storageKey); if (storedData) this._rawData = storedData; } catch (error) { console.error('Storage load error:', error); } } saveDataToStorage() { try { wx.setStorageSync(this.storageKey, this._rawData); } catch (error) { console.error('Storage save error:', error); wx.showToast({ title: '数据保存失败', icon: 'none' }); } } async addEntity(entity, data) { return this.crudOperation('add', entity, data); } async updateEntity(entity, data) { return this.crudOperation('update', entity, data); } async deleteEntity(entity, id) { return this.crudOperation('delete', entity, { id }); } async transactionalOperation(endpoint, data) { try { await this.request(`/app/Transactional/${endpoint}`, 'POST', data); await this.syncData(); return true; } catch (error) { this.triggerCallbacks('transaction_error', endpoint, { data, error }); throw error; } } } module.exports = MiniProgramDataManager; @startuml ' 基础类与接口 abstract class EntityBasis { + Integer id + Date lastUpdated + Boolean deleted + Date deletedAt + getId() + setId(Integer) } interface EntityBasisId { + getId() + setId(Integer) } abstract class SimpleEntity { + Integer id + String name } ' 实体类定义 class Mupi { + Boolean you + String name + List<Bancai> bancaisForMupi1 + List<Bancai> bancaisForMupi2 } class User { + String name + String andy + String pass + int role + int incumbency } class Chanpin_zujian { + Double one_howmany + Double zujianshu } class WechatUser { + String openid + String sessionKey + String nickname + String avatarUrl + String phoneNumber + Long userId + Date createTime + Integer status } class Kucun { + Integer shuliang } class Dingdan_chanpin { + Integer shuliang } class Information { + Integer Status + String text + Object data } class Zujian { + String name + List<Chanpin_zujian> chanping_zujian } class Dingdan_bancai { + Integer shuliang + Integer currentUserId } class Dingdan { + String number + Date xiadan + Date jiaohuo + List<Dingdan_chanpin> dingdan_chanpin } class Jinhuo { + Integer shuliang + Date date + String text + Integer theTypeOfOperation } class Caizhi { + String name + List<Bancai> bancai } class Bancai { + Double houdu } ' 继承关系 EntityBasis --|> EntityBasisId SimpleEntity --|> EntityBasisId Mupi --|> EntityBasis User --|> EntityBasis Chanpin_zujian --|> EntityBasis WechatUser --|> EntityBasis Kucun --|> EntityBasis Dingdan_chanpin --|> EntityBasis Zujian --|> EntityBasis Dingdan_bancai --|> EntityBasis Dingdan --|> EntityBasis Jinhuo --|> EntityBasis Caizhi --|> EntityBasis Bancai --|> EntityBasis ' 关联关系 ' 一对一 Bancai -- Kucun : 1:1 (kucun) ' 一对多/多对一 Chanpin "1" -- "*" Chanpin_zujian : 包含 (chanpin_zujian) Zujian "1" -- "*" Chanpin_zujian : 关联 (chanping_zujian) Bancai "1" -- "*" Chanpin_zujian : 被使用 (bancai) Dingdan "1" -- "*" Dingdan_chanpin : 包含 (dingdan_chanpin) Chanpin "1" -- "*" Dingdan_chanpin : 被订购 (chanpin) Dingdan "1" -- "*" Dingdan_bancai : 包含 (dingdan) Chanpin "1" -- "*" Dingdan_bancai : 关联 (chanpin) Zujian "1" -- "*" Dingdan_bancai : 关联 (zujian) Bancai "1" -- "*" Dingdan_bancai : 被订购 (bancai) User "1" -- "*" Jinhuo : 操作 (user) Dingdan_bancai "1" -- "*" Jinhuo : 关联 (dingdan_bancai) ' 单向关联 WechatUser -- User : 绑定 (userId) @enduml---------------------------------------pin_zujian ---- RangeError: Maximum call stack size exceeded 2MiniProgramDataManager.js? [sm]:98 dingdan_chanpin ---- RangeError: Maximum call stack size exceeded MiniProgramDataManager.js? [sm]:98 chanpin_zujian ---- RangeError: Maximum call stack size exceeded 2MiniProgramDataManager.js? [sm]:98 dingdan_chanpin ---- RangeError: Maximum call stack size exceeded MiniProgramDataManager.js? [sm]:98 chanpin_zujian ---- RangeError: Maximum call stack size exceeded 2MiniProgramDataManager.js? [sm]:98 dingdan_chanpin ---- RangeError: Maximum call stack size exceeded MiniProgramDataManager.js? [sm]:98 chanpin_zujian ---- RangeError: Maximum call stack size exceeded MiniProgramDataManager.js? [sm]:98 dingdan_chanpin ---- RangeError: Maximum call stack size exceeded MiniProgramDataManager.js? [sm]:98 chanpin_zujian ---- RangeError: Maximum call stack size exceeded MiniProgramDataManager.js? [sm]:98 chanping_zujian ---- RangeError: Maximum call stack size exceeded 2MiniProgramDataManager.js? [sm]:98 dingdan_chanpin ---- RangeError: Maximum call stack size exceeded MiniProgramDataManager.js? [sm]:98 chanpin_zujian ---- RangeError: Maximum call stack size exceeded--------------- getReferenceType(prop) { try { const baseProp = prop.replace(/\d/g, ''); if (this.entityTypeMap[baseProp]) return this.entityTypeMap[baseProp]; const pluralProp = `${baseProp}s`; if (this.dataManager._rawData[pluralProp]) return pluralProp; } catch (error) { console.log(prop+" ---- "+error) }一直触发
最新发布
07-26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值