浅谈 js 异步

刚开始学 nodejs 的时候,被它的异步搞蒙了。并不像我们写 C 或者 Java 一样将前一步执行完才进行下一步。

举个例子:

1
2
3
4
5
6
7
8
// demo1
console.log('begin')

setTimeout(() => {
console.log('1s')
}, 1000)

console.log('end')
1
2
3
begin
end
1s

不管运行多少次,结果都是一样的。

我预期的结果是下面这样的

1
2
3
begin
1s
end

那代码就要这么写

1
2
3
4
5
6
7
// demo2
console.log('begin')

setTimeout(() => {
console.log('1s')
console.log('end')
}, 1000)

为什么要这么写呢?因为setTimeout的第一个参数是一个异步回调函数

等等,异步?难道还有同步的回调函数吗?

有,我们看下异步和同步的两种回调函数

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
// demo3
const fs = require('fs')

let result1 = 'result1'
let result2 = 'result1'

// 异步读文件的函数
function readFile() {
fs.readFile('./msyql.js', () => {
result1 = 'readFile'
})
return result1
}

// 同步函数
function sync() {
result2 = 'change'
return result2
}

function func(cb) {
return cb()
}

func(readFile)
func(sync)
console.log(result1) // result1
console.log(result2) // change

可能已经发现一个问题了。同步回调在这里好像并没有正在存在的必要,直接在调用前一个同步方法后面调用下一个同步方法就可以了。同步回调有助于我们更好的编写和阅读代码,这里先不提。那么剩下的是异步回调函数的作用。

异步回调函数是干嘛用的

异步回调函数是 nodejs 现实异步的方式,那么只有这种方式吗?当然不是,可以用events实现发布订阅的模式实现异步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// demo4
const EventEmitter = new require('events').EventEmitter
const events = new EventEmitter()
console.log('begin')
events.on('done', (data) => {
console.log(data)
})
setTimeout(() => {
console.log('1s')
events.emit('done', 'timeout')
console.log('end')
}, 1000)
// begin
// 1s
// timeout
// end

但是 events 的形式,可能会因为 event name 过多,导致编码困难,并没有回调函数的方式灵活。

但是回调函数也有缺点,就是嵌套严重,也就是我们常说的回调地狱(不知道回调地狱的,自行搜索)。

有没有更优雅的方式使用 JavaScript 的异步呢?像写同步代码一样简单的方式?答案是有的。

Promise + async await

Promise 是为了解决回调地狱的一个解决方案。把原来的回调嵌套的方式使用链式的方式来执行。视觉上比回调函数好那么一点,但是本质其实还是回调函数,而且链式调用里面也是存在回调函数的。

我们把 demo2 改一下,使用 Promise 写,结果还是和 demo2 一样的。

1
2
3
4
5
6
7
8
9
10
11
12
// demo5
console.log('begin')

new Promise((resolve) => {
setTimeout(() => {
resolve('1s')
}, 1000)
}).then((res) => {
console.log(res)
}).then(() => {
console.log('end')
})

当你把最后的 then 里面的 log 写到 Promise 的下面的时候,发现end会比1s先打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// demo6
console.log('begin')

new Promise((resolve) => {
setTimeout(() => {
resolve('1s')
}, 1000)
}).then((res) => {
console.log(res)
})
console.log('end')
// begin
// end
// 1s

所以还是和异步回调一样的,只是把 callback 写成 then 罢了。那有没有更好的方法呢?

有,async await

async await其实是 Promise 的语法糖。语法糖,就是用更优雅的写法来写之前看起来相对比较丑的语法的方式。就像药丸很苦,我们医生为了骗小孩吃下去,就在药丸外边涂上一层糖衣,小孩子以为是糖果就会吃药。

我们简单举个语法糖的例子,箭头函数与函数表达式。这两种方式除了this,基本上一致的。

1
2
3
4
5
6
7
const def = function () {
console.log('default')
}

const arrow = () => {
console.log('arrow')
}

我们用async await来改写一下 demo5

1
2
3
4
5
6
7
8
9
10
11
12
13
// demo7
console.log('begin')

const run = async()=>{
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve('1s')
}, 1000)
})
console.log(res)
console.log('end')
}
run()

这样看起来就顺眼多了。当然有个要注意的地方,await必须写在async修饰的方法里面。


浅谈 js 异步
https://bubao.github.io/posts/8e5102f6.html
作者
一念
发布于
2019年12月17日
许可协议