一、最初
一切从 Javascript 是一门异步编程语言说起,比如这种最简单的:
let n = 0
function f1() {
setTimeout(function () {
n++
}, 1000)
}
f1()
console.log(n)
可能直觉上会觉得最终n=1,但实际上打印出来的是0,因为尽管调用了f1函数,但主进程并不会等待其内部的定时器到时之后再继续往下执行,而是直接就console.log(n)
了,而此时n=0。
二、Callback (回调函数) 方案
为了解决这个问题,人们想出了回调函数,也就是说当一个函数需要一定的时间去运行,那么就等它执行完毕之后再反向调用外部的某个函数,将上例改写如下:
let n = 0
function f1(function_name) {
setTimeout(() => {
n++
function_name(n);
}, 1000);
}
function f2 (ret) {
console.log(ret)
}
f1(f2)
这次打印出来就是n=1了,f1函数多了一个参数function_name,这个参数是告诉f1,当函数运行完毕后,将返回值通过function_name这个函数携带出来,这是一种反向调用,也就是所谓的 “回调函数 (callback function)”。
- 匿名函数
在调用回调函数的时候,可以不指定函数名,将函数体直接嵌到形参里面,还是上面那个例子,可以写成这样:
let n = 0
function f1(function_name) {
setTimeout(() => {
n++
function_name(n);
}, 1000);
}
f1(function (ret) {
console.log(ret)
})
这种写法并不难理解,就是将函数f2直接嵌入了f1的参数列表里。
三、回调地狱
尽管回调函数解决了异步执行返回值的确定性问题,但是如果程序逻辑需要进行多重异步操作,就会导致经典的 “回调地狱”,比如这样:
let n = 0
function f1(function_name) {
setTimeout(() => {
n++
function_name(n);
}, 1000);
}
const f3 = f2 = f1
f1(function (ret) {
f2(function (ret) {
f3(function (ret) {
console.log(ret)
})
})
})
最终结果是n=3,以上演示只写了三层,实际上超过3层的逻辑在实际生活中是很常见的,回调地狱将会让代码的可读性降低,复杂性大大增加。
四、Promise 方案
Promise是ES6中的新特性,其最主要是目的就是要解决“老式”的回调函数所产生的“地狱”问题。将上例改写如下:
let n = 0
function f1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
n++
resolve(n)
}, 1000);
})
}
const f3 = f2 = f1
f1()
.then(() => f2())
.then(() => f3())
.then(
ret => {
console.log(ret)
})
最终结果和回调版本是一样的,依然是 n=3 ,但书写和理解性就比前者容易多了,通过.then()
来形成所谓的 “链式调用“,即在上一个操作执行成功之后,开始下一个的操作。
使用上需要注意的有两点,首先用 return new Promise((resolve, reject) => { ... })
将耗时操作包裹起来,而 resolve (ret)
的意义则是将返回值携带出来,类似 return (ret)
。
PS:上例中没有用到第二个参数 reject ,通过它将抛出一个拒绝执行的信息,比如我们将上例的resolve(n) 改为 reject(n) ,链式调用将会中断, f2、f3将不会被执行。reject 一般与catch 错误捕捉机制协同使用,当然如果没有这个需求的话,reject 可以省略。
-
箭头函数
上例中采用了很多=>
这样的 ”箭头“ 函数,对应老式的function
函数声明方式,以下是对照: -
带名字的函数
不带参数:abc = () => { 函数体 }
等于function abc() { 函数体 }
带参数:abc = (v1, v2) => { 函数体 }
等于function abc(v1, v2) { 函数体 }
-
匿名函数
不带参数:() => { 函数体 }
等于function () { 函数体 }
带参数:(v1, v2) => { 函数体 }
等于function (v1, v2) { 函数体 }
PS: 如果只有一个变量,可以不带括号,比如:
abc = v1 => {
console.log(v1)
}
或者如上例的:
ret => {
console.log(ret)
}
五、async 与 await
通过async 和 await,可以写出更加像 “同步式” 编程语言那样的代码,例如:
let n = 0
function f1() {
return new Promise((resolve) => {
setTimeout(() => {
n++
resolve(n)
}, 1000);
})
}
const f3 = f2 = f1
async function run() {
await f1()
console.log(n)
await f2()
console.log(n)
await f3()
console.log(n)
}
run()
注意,async/wait 这种方案不能直接在主程序的最外层调用,而是必须指定一个 “执行函数”, 并在它前面添加一个 async
关键字,本例中是 async function run()
,而在这个执行函数体内,需要调用的函数前面加一个await
关键字,表示将同步执行该函数,上例中f1、f2、f3前面都添加了await,则表示这三个函数是一个挨着一个按顺序执行的。想象一下,在最初的Javascript例子中,这段代码一定是一次性输出3个0,而不是像现在这样间隔1秒输出:1、2、3。这种体验其实跟同步式编程语言已经非常接近了。
后记
本文对Javascript的异步执行机制,回调函数,Promise机制以及箭头函数等内容只讲了一点皮毛,纯属抛砖引玉。本文的目的也并非深入讨论这些知识点,而是想让完全不熟悉的同学能尽快先 “体验” 一下,并对这些内容产生一些感性认识,需要深入学习的同学可以参考以下链接:
Promise:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arrow_functions
https://juejin.cn/post/7108187709076111367
https://juejin.cn/post/7235177983312216125