文章目录
- 早期做法
- 回调函数
- 另一段探究代码可以表明问题
- 实现电子钟表(秒表)功能
- 改进做法
- promise
- 使用高阶函数风格的好处
- 从0开始理解promise
- link0
- link1
- 链式调用
- 创建Promise
- promise 经验法则
- link2
- 执行顺序
- link3
- 消息队列和函数栈
- 添加消息到队列
- async/await
- link4
早期做法
回调函数
在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数
),是指通过参数将函数传递到其它代码的,某一块可执行代码的引用
(或封装该代码的对象本身)。这一设计允许了底层代码调用在高层定义的子程序。
对于以回调函数对象会参数的高阶函数,只需要将回调函数对象参数角色当作普通的参数来理解,不过该中参数具有一定的功能,他的功能和函数一样.
- 回调函数可以简化代码,让代码更紧凑.
- 减少一些中间变量的创建(中间值的保存和判断)
- 但是一般来说,高阶函数和回调函数可以拆分为用普通函数来实现
例如下面的回调用法,借助回调,我们可以避免一些判断逻辑的同时,也能够实现串行化任务.
同时,代码的复用程度也不错.
function delayedExecute(str, callback = null) {
setTimeout(() => {
console.log(str);
callback && callback();
}, 1000)
}
delayedExecute('p1 callback', () => {
delayedExecute('p2 callback', () => {
delayedExecute('p3 callback', () => {
delayedExecute('p4 callback');
});
});
});
运行结果
// p1 callback(1 秒后)
// p2 callback(2 秒后)
// p3 callback(3 秒后)
// p4 callback(4 秒后)
另一段探究代码可以表明问题
/* */
function go() {
return new Promise(
(resolve, reject) => {
// ()=>console.log(Date.now())
console.log("before run the setTimeout...");
console.log(Date())
// 期约解决之后(或者状态敲定之后,后续的than所添加的执行逻辑(回调)方可进入异步消息队列)
setTimeout(resolve, 1000)
console.log("after the setTimeout task be send to the async task queue.");
console.log(Date());
console.log("\n");
/* default return resolved */
// resolve()
}
)
}
function test_go() {
go()
.then(go)
.then(go)
}
// 后续的then()中添加的操作会等待前面的操作期约返回结果
/* 等价于 */
/*
go()
.then(()=>go())
.then(()=>go())
*/
test_go()
运行结果表明,js中,setTimeout()将异步任务进行排队的事件几乎瞬间完成,(体现在该语句前后两个打印事件的语句(几乎是同时发生的)(当然,您可以通过将setTimeout的毫秒参数调大或者将事件打印改为事件戳,可以更能说明问题.
但是这种写法还是有缺点,回调函数对象嵌套的太深了.
实现电子钟表(秒表)功能
/* implement of simple clock: */
function goo() {
// return
p = new Promise(
(resolve) => {
// ()=>console.log(Date.now())
console.log(Date())
setTimeout(resolve, 1000)
}
)
p.then(goo)
// return p
}
goo()
您可以修改setTimeout()的毫秒参数来提高计时精度.(越低越精确)
改进做法
早期没有引入Promise的时候,想要串行化异步调用往往会陷入回调地狱的问题
promise
链式调用方案:
function delayedResolve(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
});
}
delayedResolve('p1 executor')
.then(() => delayedResolve('p2 executor'))
.then(() => delayedResolve('p3 executor'))
.then(() => delayedResolve('p4 executor'))
结果
// p1 executor(0 秒后)
// p2 executor(1 秒后)
// p3 executor(2 秒后)
// p4 executor(3 秒后)
使用高阶函数风格的好处
嗯,我们先将这个被作为参数的函数(或函数对象的引用)记为pf(parameter_functoin); 将以pf为参数的高阶函数记为hf(higherOrder_fucntion)
笼统的讲:
回调函数:传递给高阶函数的函数
高阶函数:接收函数(回调函数)为参数或返回值为函数的函数
一个典型的例子是:
//定义三个函数(将作为回调函数传给高阶函数calculator)
function add(num1, num2) {
return num1 + num2;
}
function sub(num1, num2) {
return num1 - num2;
}
function multiply(num1, num2) {
return num1 * num2;
}
/* origin version calculator: */
// function calculator(num1, num2, operator) {
// return operator(num1, num2);
// }
/* the more robust calculator*/
function calculator(num1, num2, operator) {
// Check inputs are number or not - Common logic
if (isNaN(num1) || isNaN(num2)) {
console.log("\terror:input(s) are not a number:");
return;
}
return operator(num1, num2);
}
ret1 = calculator(10, 5, add);
console.log("result1:" + ret1)
ret2 = calculator(10, 5, sub);
console.log("result2:" + ret2)
ret3 = calculator(10, "NotNumberArgument", multiply);
console.log( "result3:" + ret3)
可以看到,通过高阶函数接收回调函数的形式,我们为一类函数的执行做了统一的类型判断
而且,operator 还是软编码
从0开始理解promise
link0
优雅的异步处理 - 学习 Web 开发 | MDN (mozilla.org) 这是位于js教程中的一篇内容
Promise术语回顾
在上面的部分中有很多要介绍的内容,所以让我们快速回过头来给你一个简短的指南,你可以将它添加到书签中,以便将来更新你的记忆。你还应该再次阅读上述部分,以确保这些概念坚持下去。
- 创建promise时,它既不是成功也不是失败状态。这个状态叫作pending(待定)。
- 当promise返回时,称为resolved(已解决).
- 一个成功resolved的promise称为fullfilled(实现)。它返回一个值,可以通过将
.then()
块链接到promise链的末尾
来访问该值
。<span> </span>.then()
块中的执行程序函数将包含promise的返回值。 - 一个不成功resolved的promise被称为rejected(拒绝)了。它返回一个原因(reason),一条错误消息,说明为什么拒绝promise。可以通过将
.catch()
块链接到promise链的末尾来访问此原因。
link1
MDN reference link:该页面介绍了promise最常用的部分 一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知的值。* Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。
- 如同字面意思一样,它不可能还只是个壳子(期约),但是我们的后续操作(依赖于异步操作的结果的后续操作)可以建立在这个可能尚未被确定值的期约上
- 正因为我们使用期约的时候往往是我们不知道期约所代表的结果(异步操作的执行结果的成败),所以我们往往要准备两套方案(两个回回调函数)来分别处理promise的两种不同的稳态结果.
- 更直白的,我们可以将promise理解为一种对结果的设想,并将这种设想具象化(实例化),然后我们对这个设想执行一些handler的绑定;待到promise状态确定下来后,就可以按情况执行其中一种的后续处理方案
- 早期没有promise(异步执行的结果没有结果替身,就只好嵌套着递归来实现后续处理
- 到了绑定部分,handler(resolved/rejected)函数对象本身要作为参数传递给then();同时,handler本也是函数的角色,所以往往也接受参数,这个参数正是期约(promise)状态稳定下来后所带出的结果(异步逻辑成功执行的结果或者执行失败的异常结果res[ult])
let myFirstPromise = new Promise(function(resolve, reject){
//当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...)
//在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法.
setTimeout(function(){
resolve("成功!"); //代码正常执行!
}, 2000);
});
//在这里为myFirstPromise绑定resolve回调的具体逻辑(通过then()行数执行相应的绑定
myFirstPromise.then(function(successMessage){
//successMessage的值是上面调用resolve(...)方法传入的值.
//successMessage参数不一定非要是字符串类型,这里只是举个例子
console.log("Yay! " + successMessage);
});
链式调用
创建Promise
const wait = (ms) => {
new Promise(
(resolve) => {setTimeout(resolve, ms)}
)
};
//wait:指向函数对象
//ms为毫秒数(参数)
//resolve 作为handler(回调函数)传递给setTimeout()
//函数对象(resolve) => {setTimeout(resolve, ms)}作为Promise构造函数的执行器参数
/*调用wait*/
//wait(1000)将创建一个Promise(以函数执行器对象(resolve) => {setTimeout(resolve, ms)})为参数;
执行器在1000ms(也可能更久)后将会执行resolve所指的函数
由通过then传递具体的处理逻辑给resolve形参);
wait(10000).then(
() => saySomething("10 seconds")
).catch(failureCallback);
通常,Promise 的构造器接收一个执行函数(executor),我们可以在这个执行函数里手动地 resolve 和 reject 一个 Promise。
既然 setTimeout 并不会真的执行失败,那么我们可以在这种情况下忽略 reject。
promise 经验法则
一个好的经验法则是总是返回或终止 Promise 链,并且一旦你得到一个新的 Promise,返回它
。
doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));
link2
Promise more detail
过早地处理被拒绝的 promise 会对之后 promise 的链式调用造成影响
执行顺序
link3
并发模型与事件循环
事件队列
像promise这样的异步操作被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续JavaScript代码的运行。排队操作将尽快完成
(仍然由主线程负责排队),然后将结果返回到JavaScript环境
。
可视化描述(运行时概念)
stack中存放函数栈帧
一个函数调用就会向栈中压入一个帧(尤其时嵌套调用,递归调用)
消息队列和函数栈
在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。
被处理的消息
会被移出队列,并作为输入*参数*
来调用与之关联的系列函数
。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
函数的处理会一直进行到执行栈再次为空为止;
然后事件循环将会处理队列中的下一个消息(如果还有的话)。
添加消息到队列
添加消息
在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。
async/await
link4
async/await