JavaScript 的秘密生活:Promise
Source: Dev.to
Timothy 站在黑板前,手开始抽筋。他已经写了十分钟的代码,但实际上卡在了角落——字面意思。他的代码写得太靠右,以至于字母被压在木框边上。
“我只想做三件简单的事,”Timothy 抱怨道。“登录,获取用户的数据,然后获取他们的帖子。但代码看起来像通向无处的楼梯。”
他后退一步,展示他的怪物:
login(user, function(token) {
getUser(token, function(profile) {
getPosts(profile.id, function(posts) {
console.log("Finally got posts:", posts);
}, function(err) {
console.error("Error getting posts");
});
}, function(err) {
console.error("Error getting user");
});
}, function(err) {
console.error("Error logging in");
});
Margaret 走过来,手里拿着一块新的橡皮。“啊,”她说。“灾难金字塔。”
“这太难读了,”Timothy 说。“我在括号里迷路了。错误处理简直是噩梦。”
Margaret 把整段三角形代码擦掉。“那是因为你依赖了 callbacks。你把库的控制权交给别人,然后指望他们回调你。”
她在黑板上画了一个单一、整洁的框。
“是时候学习 the Promise 了。”
IOU(Promise 对象)
“Promise,” Margaret 解释道,“是一个对象。它不是值本身,而是一个占位符——一个欠条。”
Pending(待定): “我正在处理。”
Fulfilled(已完成): “这是你的数据。”
Rejected(已拒绝): “出错了。”
“与其把函数 传入
login,login函数会把一个 Promise 对象 返回 给你。你可以持有它、传递它,并给它附加指令。”
const loginPromise = login(user);
loginPromise.then(function(token) {
// This runs only when the promise is fulfilled
return getUser(token);
});
“更好,” Timothy 承认道。“但如果我有三个步骤,我不是仍然要嵌套它们吗?”
“不,” Margaret 说。“因为 Promise 返回……另一个 Promise。你可以平铺链式调用。”
login(user)
.then(token => getUser(token))
.then(profile => getPosts(profile.id))
.then(posts => console.log("Finally got posts:", posts))
.catch(err => console.error("Something went wrong:", err));
Timothy 跟踪那行代码说:“它是平的。并且只有一个 .catch() 在最后吗?”
“是的,” Margaret 点头。“不必在每一层都处理错误,而是统一在底部捕获。金字塔结构消失了。”
暂停(Async / Await)
Timothy 看着链条。“它更简洁,”他同意道。“但它仍然看起来…不同。它不像普通的自上而下的编程。”
“你说得对,”Margaret 微笑着说。“这条链仍然有点‘语法化’。如果你想要看起来真正像人的代码,需要 async / await。”
她再次擦掉了白板。
“还记得 event loop 吗?还记得我们说过不能阻塞栈吗?”
“记得,”Timothy 背诵道。“如果我暂停栈,浏览器就会卡死。”
“正确。但
await让你可以暂停 函数 而不阻塞 栈。”
async function showUserPosts() {
try {
const token = await login(user);
const profile = await getUser(token);
const posts = await getPosts(profile.id);
console.log(posts);
} catch (error) {
console.error("Something went wrong:", error);
}
}
Timothy 注视着它。“第 3 行:await login(user)。代码就在这里停下来?它在等网络吗?”
“是 函数 暂停,”Margaret 纠正道。“当引擎看到 await 时,它会挂起这个函数并实际上让位。它把控制权返回给浏览器的其余部分。”
她快速画了一个栈的示意图。
“当这个函数被挂起时,浏览器保持活跃——处理点击和渲染样式。栈是空闲的。”
“数据返回时会怎样?”
“函数的其余部分——续体——会被放入 microtask queue(VIP 队列)。只要栈为空,你的函数就会重新跳回去,在第 4 行继续执行,手里已经拿到数据。”
结论
Timothy 看着板上的三个版本:
- The Pyramid (callbacks)
- The Chain (promises)
- The Story (async/await)
“他们都在做同样的事,” Timothy 意识到。
“是的,” Margaret 说。“但最后一个才是实情。它让你写出看起来是同步的代码——一步一步,只有一个 try/catch 块——却以异步的方式运行。”
她把粉笔递给了他。
“你不再需要嵌套你的逻辑了,Timothy。你只需要等它就行。”