起源

日常工作中我们想要获取一个函数的返回值有很多种方式,常见的有:

  • return,通过return返回我们想要获取的值,这种是最常见的
  • callback,通过传入一个callback来获取对应的值

上面两种方式均可以获取函数的返回值,其中return多半是对应同步操作,也就是我们会等待函数调用完成之后才会进行下一步操作,如果我们不希望程序被阻塞在调用的过程中,就可以使用回调函数来执行。

示例

下面通过几个例子来进行说明:

  • 示例1:return 如上所示我们直接通过return来返回函数的调用结果,不过很显然,这里是阻塞的,也就是说compute不完成计算就不会进行下一步的操作。
  • 示例2:回调 这里我们在函数的最后传入了一个回调函数,此时compute并不会再返回什么数据,而是将计算结果返回给回调函数,由回调函数进行进一步的处理,这得益于函数式编程我们可以将函数作为参数传递给其他函数,可能这显得并不是那么重要,因为仅仅是换了一个地方来处理函数返回的数据,不过模式的改变带来的好处是解耦合。

上面我们可以看到函数的执行都是同步的,这是因为这些程序的执行只需要cpu的参与,对于需要经由网络、IO、外部时钟参与的通常是需要等待的,由于js是单线程执行的,因此等待意味着阻塞,对于耗时较长的任务这种阻塞会使程序出现卡顿,为了优化这个问题,js提供了promise来解决这个问题,promise和java的future很类似,翻译过来叫做承诺,也就是将来要做某件事,而做这个动作就是通过回调函数来实现的,接下来我们通过代码演示一下promise的使用:

示例1:promise callback: 上面的代码中我们可以看到程序的执行效果,程序并不会阻塞在compute这里,而是会直接跳过这里,然后通过回调函数继续执行。

示例2:promise then: 这里我们演示了promise then的写法,首先我们要获取一个promise,promise的生成代表了一个耗时的,非cpu的运算,其接受两个回调函数,分别是resolve、reject,分别代表了运行无问题正常返回和异常。

上面这个示例略微有点硬,因为我们是自己直接创建并返回了一个promise,更多的情况下我们自己是不需要创建promise的,都是其他函数返回的promise,然后我们通过回调函数来处理返回的值,我们通过setTimeout来演示一下这种情况,setTimeout的执行逻辑是当程序遇到setTimeout代码段的时候,会将其中的执行逻辑交给定时器外设,程序此时并不会阻塞,会跳过这一段代码继续往下执行,等到外设执行完了就会回调该函数继续执行原来跳过的代码,因此我们可以通过setTimeout来模拟promise,具体如下: 这里很明显能看到一个问题,就是为了保障promise的顺序执行,我们不得不在回调函数中继续调用promise,这样循环嵌套,最终会让代码看的臃肿不堪。我们上面演示的promise-then的方式就解决了这个问题。而且上面的代码还并不包含异常的处理逻辑。

对于示例1里面的callback hell我们可以提供promise版本的优雅地写法:对于一个黑盒函数来说,我们要将其变成promise的唯一的途径就是包一层,哈哈,包一层。。。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 黑盒函数
function compute(a, b, cb) {
setTimeout(()=>{
c = a+b;
cb(c)
}, a * 100)
}
// 将回调逻辑连同匿名函数一起包装了,不过回调的执行代码放在了之后的then的代码块里面
// function cbl(res) {
// console.log(`res: ${res}`)
// }

function promiseCompute(a, b) {
return new Promise((resolve, reject) => {
compute(a, b, (res) => {
// 这里忽略异常检测逻辑 reject
resolve(res);
});
})
}

function main() {
console.log("start")
promiseCompute(1,2).then((r) => {
console.log(`res: ${r}`)
// then后不再是promise了,因此这里如果想要顺序执行,
// 就在后面不断地返回promise并不断地再次调用then方法
return promiseCompute(2,3)
}).then((r) => {
console.log(`res: ${r}`)
}).catch((e) => {
// 异常统一处理逻辑
console.log(e)
})
console.log("end")
}
main();

上面代码有点长,因此这里把代码贴出来演示一下,具体逻辑已经在注释里面说明的比较详细了。

示例3:async、await语法糖: 上面我们看到promise比较经典的用法,就是通过then-catch的方式来执行的,不过js给我们提供了语法糖,让程序看起来更像是串行的执行的,修改之后的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 黑盒函数
function compute(a, b, cb) {
setTimeout(()=>{
c = a+b;
cb(c)
}, a * 100)
}
// 将回调逻辑连同匿名函数一起包装了,不过回调的执行代码放在了之后的then的代码块里面
// function cbl(res) {
// console.log(`res: ${res}`)
// }

function promiseCompute(a, b) {
return new Promise((resolve, reject) => {
compute(a, b, (res) => {
// 这里忽略异常检测逻辑 reject
resolve(res);
});
})
}

async function main() {
console.log("start")
try {
const r1 = await promiseCompute(1,2)
console.log(`res1 : ${r1}`)
const r2 = await promiseCompute(2,3)
console.log(`res2 : ${r2}`)
} catch (error) {
console.log(error)
}
console.log("end")
}
main();

上述代码执行结果如下: 可以看到程序完全变成了同步的代码,包括最后的end,这是由于我们的await的逻辑等同于将其后的代码包装到then里面了(一直包含到程序的最后,并且await函数返回的结果是对应函数中resolve、reject的结果,这看起来有点不可思议,毕竟try是有作用于的,不过结果确实是这个样子)。如果想要将上述执行结果start、end和中间的代码看起来不是同步的话,我们只需要再包一层就可以了,提炼出新的函数,将异步逻辑放到新的函数中就完事了,这里就不再演示了。


除了上述串行执行外,可能还会涉及到一种新的逻辑,就是我们希望几个promise能够并发执行,完事之后我们等待所有的并发结果都执行出来了才继续下一步,这个有点类似于java里面的栅栏了,这个其实也是可以实现的,具体实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function main() {
console.log("start")
try {
const p1 = promiseCompute(1,2)
const p2 = promiseCompute(2,3)

const [r1, r2] = await Promise.all([p1, p2])
console.log(`res1 : ${r1}`)
console.log(`res2 : ${r2}`)
} catch (error) {
console.log(error)
}
console.log("end")
}

小结

参考网上学习 视频