在上一篇文章中,我们深入了解 Javascript 非同步的核心,到底在背景做了哪些事,才构成了我们实际看到的画面。
今天让我们来了解一些常见的非同步操作,callback、Promise,他们的演化脉络,以及各自的优缺点吧!
中文可以叫做「回呼函式」,基本上就是一个普通的函式,但因为被应用在「非同步结束後才呼叫」,所以多得到一个 callback function 的称号。
有没有觉得 callback 这个词特别眼熟?昨天在讨论非同步核心时,有提到一个:
Callback Queue:用来存放从 Web api 过来,准备要进入 Call Stack 的指令
没错,就是你想的那样!
Callback Queue 就是用来让 callback function 排队等待的地方。
callback function 最常见的就是 DOM 事件绑定:
elem.addEventListener('click', callback);
或者计时器:
setTimeout(callback, 1000);
这个东西很常会跟我们在 Day 8 提到的高阶函式 HoF(Higher-order function) 搞混。
简单来说,把一个 function A,传进另外一个 function B 当参数,等到 B 的事情做完,他会去呼叫并执行那个 A。
我们会称呼 function A 为 callback function(因为被带入),而 function B 为 HoF(因为有函式参数带入)。
所以上面两个例子中,addEventListener
、setTimeout
就是 HoF,而 callback
就是 callback function。
面对不知道要等多久的事情,与其站在那边等,不如直接给它一个 function,告诉它事情做完就来执行 function,我就可以去做其他事情了。
生活化来说,就像订网购一样,我在网站上下订商品(执行 function B),但不可能下订之後就立刻准备好,所以我也同时告诉它,包裹准备好之後,帮我送到我家(执行 function A)。
值得注意的是,我完成下订後,就可以去浇我的花、追我的剧,不用特地等包裹准备好才跟对方说地址,我可以同时去做我其他事情,包裹就会自动送到了。
写成简单的程序码就像是这样:
const doOrder = () => {
console.log('传送交易资料到主机预计 2 秒...');
setTimeout(() => {
console.log('传送完成,开始配送预计 10 秒...');
setTimeout(() => {
console.log('包裹已送达!');
}, 10000);
}, 2000);
};
doOrder();
console.log('订单送出去了,可以去浇花、追剧罗!');
执行结果
传送交易资料到主机预计 2 秒...
订单送出去了,可以去浇花、追剧罗!
// (2秒空档)
传送完成,开始配送预计 10 秒...
// (10秒空档)
包裹已送达!
在 setTimeout
的第一个参数,就是我们的 callback function,我们没有被动地等 setTimeout
的秒数跑完,而是主动告诉它下一步是什麽,然後就可以利用时间去做其他事。
可以说,callback function 是一个转被动为主动的姿态啊!
听起来很棒,对於需要等待的事情,我不用等,直接告诉它下一步要做什麽,这样我就可以去逍遥了。
但如果今天有一件事情,我告诉它下一步,还要再告诉它下一步的下一步,以及下一步的下一步的下一步...
我下一步你的下一步!
我 callback 的时候 callback!
延续上一个例子,我需要在下订单之前,先查询我的钱包是否有余额,那就会变这样:
const doOrder = () => {
console.log('查询钱包预计 3 秒...');
setTimeout(() => {
console.log('查询完成,传送交易资料到主机预计 2 秒...');
setTimeout(() => {
console.log('传送完成,开始配送预计 10 秒...');
setTimeout(() => {
console.log('包裹已送达!');
}, 10000);
}, 2000);
}, 3000);
};
doOrder();
console.log('订单送出去了,可以去浇花、追剧罗!');
执行结果
查询钱包预计 3 秒...
订单送出去了,可以去浇花、追剧罗!
传送交易资料到主机预计 2 秒...
// (2秒空档)
传送完成,开始配送预计 10 秒...
// (10秒空档)
包裹已送达!
是不是离波动拳愈来愈近了呢?
没错,callback 本身是很好的方式,但在处理一连串巢状 callback 时,视觉上容易出错(这个右括号是哪一层的右括号?),虽然可以用 linter 解决,但要做错误处理(error handling)也会更为复杂,每一层的 code 又会增加,最後变成一大坨难以维护的「程序码群」。
於是 Promise 诞生了,拯救在地狱浮沉的 callback 们。
Promise 已经是 ES6 的标准了,可以想像是一种特别的物件,这种物件是用状态机的方式,来表达任务执行的状态,主要有三种状态:
pending
fulfilled
rejected
一个 Promise 刚建立的时候是 pending
,接下来有两种可能:
resolve
并回传 result,转变成fulfilled
状态,reject
并回传 error,转变成 rejected
的状态。生活化来说,Promise 就像是在商店街跟店家点餐时,店家不可能一点餐就立刻出餐,但也不好意思让客人一直站在旁边等,所以店家会给你这个:
取餐叫号机,就像是 Promise,你刚拿到时,餐点还没好,所以是 pending
,当餐点做好时,就会震动闪光(?)转成 fulfilled
状态,告诉你该做下一步了。(不过取餐叫号机好像很少有 rejected
状态?也许是你的餐做失败了,请回来重新点餐...)
我们可以用 .then()
的方式拿到他的 result,.catch()
抓到他的error:
const doSomethingReturnPromise = () => {
// 用 setTimeout 来模拟一个非同步的 Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
// 一秒後回传资料
resolve({ data: 123 });
}, 1000);
})
};
doSomethingReturnPromise()
.then(function (result) {
// ...
})
.catch(function (err) {
// ...
});
可以看到其实 Promise 也运用了一些 callback 进来,但最大的不同是,他的 then()
跟 catch()
都可以回传一个 Promise (可以想像成,每个阶段工作人员都会给你一个叫号机),因此可以「串接」下去:
doSomethingReturnPromise()
.then(function (result) {
// ...
// return 另一个 promise
})
.then(function (result) {
// ...
// return 另一个 promise
})
.then(function (result) {
// ...
})
.catch(function (err) {
// ...
});
在 callback hell 遇到的问题,多层的缩排瞬间就被拉成一层,只要每个 .then()
都回传 Promise,就可以确保 A 完成了做 B,B完成了做 C,你要串几个都很容易扩充(promise chain)。
而且错误处理的部分也简单了许多,中间不管有几个 .then()
,只要有任何一个 Promise 转为 rejected
,就会进入 .catch()
,不用再一个个处理 error,只要处理一次即可。
针对刚刚的例子,试着写写看比较贴近真实一点的版本。比如我要先取得钱包余额,确定如果还有余额,才可以进行下单动作。
const doQueryWallet = () => {
// 用 setTimeout 模拟从资料库 IO
return new Promise((resolve, reject) => {
setTimeout(() => {
// 一秒後回传资料
resolve({ balance: 100 });
}, 1000);
})
};
const doOrder = () => {
// 用 setTimeout 模拟资料库 IO
return new Promise((resolve, reject) => {
setTimeout(() => {
// 一秒後回传资料
resolve({ status: 'ok' });
}, 1000);
})
};
doQueryWallet()
.then(function (result) {
if (result.balance > 0) {
return doOrder();
} else {
console.log('余额不足');
// 往下回传 pending 的 Promise (类似中断 Promise)
return new Promise((resolve, reject) => {})
}
})
.then(function (result) {
if (result.status === 'ok') {
console.log('下单成功');
} else {
console.log('下单失败');
}
})
.catch(function (err) {
console.error(err);
});
ES6 的 fetch
语法是最容易接触到的 Promise 语法:
fetch('https://api.jokes.one/jod')
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(myJson);
})
.catch(function(error) {
console.log(error.message);
});
(上面这一段真的可以复制贴到 console 执行,每日笑话lol)
非同步系列的演化脉络,虽然大家可能早就用 async
/await
飞来飞去了,但就像我们学习历史一样,监古知今。
重点就在於「解决了什麽问题」。
当我们更清楚这些新技术的诞生,不是为了出现而横空出世,而是为了解决实际 coding 会遇到,可读性、可维护性的问题,才会对於手上正在用的工具更加清晰。
有感於许多踏入前端领域的人,已经是框架时代的起飞时期,这些人可能不知道 jQuery 有划时代的意义,只觉得是过时的产物,这样盲目追求的人,容易不清楚手上工具的优势与劣势,简单的展示页面也动辄 Angular、React 飞来飞去,其实是很危险的。
希望这几天的「非同步讲古入门」,能够点出每个工具在那个时代的意义。
一次次的等候
在愈来愈深的洞穴
渐渐迷失了归途
>>: [Day 20] 2D 批次渲染 (二) - BURN OUT
Understanding Quantum Teleportation 1.Creates an e...
永茂猪头饭 地点:台南市盐水区朝琴路48号(盐水信义路与朝琴路的交叉口) 时间:早上 第一次到盐水时...
运算子算是比较繁杂的部分,需要多些耐心来理解与记忆,没办法用一个简明的观念来一以贯之。 算术运算子...
Gird是一种二维的布局方式,相较flex来说grid还多控制了列~ example : <d...
今天要来跟各位一起解析 QnA Maker Bot,以下简称 QA Bot。 今天是参考 官方范例程...