在JavaScript开发中,回调函数(callback)是最基础的异步编程模式之一。简单来说,回调函数就是一个在特定条件满足时被调用的函数。当我们需要确保某些操作按照特定顺序执行时,回调函数就能派上用场。
回调函数的本质是将函数作为参数传递给另一个函数,并在后者完成特定操作后执行。这种模式特别适合处理异步操作,比如网络请求、文件读取或定时任务等。
javascript复制function fetchData(callback) {
// 模拟异步操作
setTimeout(() => {
const data = { id: 1, name: '示例数据' };
callback(null, data); // 操作成功,调用回调
}, 1000);
}
// 使用回调函数
fetchData((err, result) => {
if (err) {
console.error('出错:', err);
return;
}
console.log('获取到的数据:', result);
});
在这个例子中,fetchData函数接收一个回调函数作为参数,在异步操作完成后调用这个回调。这种模式确保了我们在数据准备好之后才进行后续处理。
JavaScript是单线程语言,意味着它一次只能执行一个任务。为了避免阻塞主线程,很多操作(如网络请求)都是异步执行的。回调函数提供了一种机制,让我们能够在异步操作完成后得到通知并处理结果。
在门店入驻的示例中,我们需要先查询编码是否可用,然后根据查询结果决定是否跳转页面。如果不使用回调函数,代码可能会在查询完成前就执行跳转逻辑,导致错误。
让我们深入分析门店入驻的业务流程:
这个流程中,第二步的接口查询是异步操作,我们需要等待它完成才能进行第三步。这正是回调函数的典型应用场景。
原始代码中的回调实现如下:
javascript复制query(shop, callback) {
if(shop) {
let that = this;
codeQuery({code:shop}).then(res => {
this.shop_id = res.data.info.shop_id
console.log(this.shop_id,'接口返---')
callback && callback(null, this.shop_id);
}).catch(err => {
uni.showToast({ title: err, icon: 'none' });
callback && callback(err);
});
} else {
const errMsg = '该二维码无红包编码';
uni.showToast({ title: errMsg, icon: "none" });
callback && callback(errMsg);
}
}
这段代码有几个关键点值得注意:
callback && callback()确保回调函数存在才调用优点:
缺点:
虽然回调函数能解决问题,但随着项目复杂度增加,我们需要更优雅的解决方案。以下是两种常见的替代方案。
Promise是ES6引入的异步编程解决方案,它提供了更清晰的链式调用语法。
javascript复制// 改造query方法为Promise版本
query(shop) {
return new Promise((resolve, reject) => {
if(!shop) {
const errMsg = '该二维码无红包编码';
uni.showToast({ title: errMsg, icon: "none" });
return reject(errMsg);
}
codeQuery({code:shop})
.then(res => {
this.shop_id = res.data.info.shop_id;
console.log(this.shop_id,'接口返---');
resolve(this.shop_id);
})
.catch(err => {
uni.showToast({ title: err, icon: 'none' });
reject(err);
});
});
}
// 使用方式
this.query(shop)
.then(shopId => {
if(shopId == 0) {
uni.navigateTo({
url: url +'?shop=' + shop+ '&codeDisabled=true'
});
} else {
uni.showToast({ title: '该二维码已绑定商户', icon: "none", duration: 3000 });
}
})
.catch(err => {
console.error('查询失败:', err);
});
Promise方案的优势在于:
async/await是ES2017引入的语法糖,让异步代码看起来像同步代码一样直观。
javascript复制// 改造query方法为async/await兼容版本
async query(shop) {
if(!shop) {
const errMsg = '该二维码无红包编码';
uni.showToast({ title: errMsg, icon: "none" });
throw new Error(errMsg);
}
try {
const res = await codeQuery({code:shop});
this.shop_id = res.data.info.shop_id;
console.log(this.shop_id,'接口返---');
return this.shop_id;
} catch(err) {
uni.showToast({ title: err, icon: 'none' });
throw err;
}
}
// 使用方式
async handleSettled(type) {
if(type == 1) {
//#ifdef H5
try {
const res = await this.$wechat.wechatEvevt('scanQRCode', {
needResult: 1,
scanType: ["qrCode"],
});
let result = res.resultStr;
let url = '/pages/xiaonuo/settled/index';
let shop = 'I9JIFFVU0H';
if(result) {
const shopId = await this.query(shop);
if(shopId == 0) {
uni.navigateTo({
url: url +'?shop=' + shop+ '&codeDisabled=true'
});
} else {
uni.showToast({ title: '该二维码已绑定商户', icon: "none", duration: 3000 });
}
}
} catch(err) {
console.error('处理失败:', err);
}
//#endif
}
// 其他逻辑...
}
async/await的优势:
| 特性 | 回调函数 | Promise | async/await |
|---|---|---|---|
| 代码可读性 | 差(嵌套深) | 较好(链式调用) | 最好(类似同步代码) |
| 错误处理 | 手动处理每个回调 | .catch统一处理 | try/catch块处理 |
| 浏览器兼容性 | 全部支持 | ES6+ | ES2017+ |
| 学习曲线 | 低 | 中 | 中高 |
| 适用场景 | 简单异步操作 | 复杂异步流程 | 现代前端项目 |
在实际开发中,我通常会这样选择:
在回调函数中,this的指向常常会出乎意料。例如:
javascript复制const obj = {
name: '示例',
doSomething: function(callback) {
callback();
},
callback: function() {
console.log(this.name); // 可能不是预期的obj
}
};
obj.doSomething(obj.callback); // 输出可能是undefined
解决方案:
javascript复制// 解决方案1:箭头函数
obj.doSomething(() => obj.callback());
// 解决方案2:bind
obj.doSomething(obj.callback.bind(obj));
// 解决方案3:保存引用
const that = this;
callback(function() {
that.doSomething();
});
无论使用哪种异步模式,良好的错误处理都至关重要:
javascript复制// 不好的做法
query(shop, (err, result) => {
if(err) {
console.error(err);
return;
}
// 处理result
});
// 更好的做法
function handleError(err) {
console.error('操作失败:', err);
uni.showToast({ title: '操作失败', icon: 'none' });
}
query(shop, (err, result) => {
if(err) return handleError(err);
// 处理result
});
javascript复制// 并行执行示例
async function fetchAllData() {
try {
const [user, orders] = await Promise.all([
fetchUser(),
fetchOrders()
]);
// 处理数据...
} catch(err) {
handleError(err);
}
}
// 超时机制示例
function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('操作超时')), timeout)
)
]);
}
随着前端生态的发展,出现了更多处理异步编程的工具和模式:
对于复杂的事件流处理,可以考虑使用RxJS的Observable:
javascript复制import { from } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
from(codeQuery({code: shop})).pipe(
map(res => res.data.info.shop_id),
catchError(err => {
uni.showToast({ title: err, icon: 'none' });
throw err;
})
).subscribe(shopId => {
if(shopId == 0) {
uni.navigateTo({
url: url +'?shop=' + shop+ '&codeDisabled=true'
});
} else {
uni.showToast({ title: '该二维码已绑定商户', icon: "none", duration: 3000 });
}
});
虽然async/await已经很大程度上取代了Generator函数,但了解它仍有价值:
javascript复制function* queryGenerator(shop) {
try {
const res = yield codeQuery({code: shop});
return res.data.info.shop_id;
} catch(err) {
uni.showToast({ title: err, icon: 'none' });
throw err;
}
}
// 使用co库执行
co(function* () {
const shopId = yield queryGenerator(shop);
if(shopId == 0) {
uni.navigateTo({
url: url +'?shop=' + shop+ '&codeDisabled=true'
});
}
});
对于CPU密集型任务,可以使用Web Workers避免阻塞主线程:
javascript复制// worker.js
self.onmessage = function(e) {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// 主线程
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
console.log('计算结果:', e.data);
};
worker.postMessage(inputData);
理解JavaScript异步编程的演进历程有助于我们做出更好的技术选型:
在实际项目中,我建议:
测试异步代码有其特殊性,以下是几种测试方式的示例:
javascript复制// query.js
module.exports = function query(shop, callback) {
// 实现...
};
// query.test.js
test('query should return shop_id', done => {
query('VALID_SHOP', (err, shopId) => {
expect(err).toBeNull();
expect(shopId).toBeGreaterThanOrEqual(0);
done(); // 告诉Jest测试完成
});
});
javascript复制// query.js
module.exports = function query(shop) {
return new Promise((resolve, reject) => {
// 实现...
});
};
// query.test.js
test('query should resolve with shop_id', () => {
return query('VALID_SHOP').then(shopId => {
expect(shopId).toBeGreaterThanOrEqual(0);
});
});
// 或者使用async/await语法
test('query should resolve with shop_id', async () => {
const shopId = await query('VALID_SHOP');
expect(shopId).toBeGreaterThanOrEqual(0);
});
javascript复制// handleSettled.js
module.exports = async function handleSettled(type) {
// 实现...
};
// handleSettled.test.js
test('should navigate when shop_id is 0', async () => {
const mockNavigate = jest.fn();
// 设置测试环境...
await handleSettled(1);
expect(mockNavigate).toHaveBeenCalled();
});
调试异步代码比同步代码更具挑战性,以下是一些实用技巧:
javascript复制async function complexOperation() {
console.log('开始操作'); // 1
const result1 = await step1();
console.log('第一步完成', result1); // 3
const result2 = await step2(result1);
console.log('第二步完成', result2); // 5
return finalize(result1, result2);
}
console.log('调用开始'); // 2
complexOperation().then(result => {
console.log('最终结果', result); // 6
});
console.log('调用结束'); // 4
理解这个日志输出的顺序对调试异步代码至关重要。
根据多年项目经验,我总结了以下异步编程的最佳实践:
javascript复制/**
* 查询门店编码是否可用
* @param {string} shop - 门店编码
* @returns {Promise<number>} 返回shop_id,0表示可用
* @throws {Error} 当编码无效或查询失败时抛出错误
*/
async function queryShop(shop) {
// 实现...
}
在团队协作中,制定并遵守统一的异步编程规范可以显著提高代码质量和可维护性。