avatar


5.异步编程

单线程模式

目前主流的JavaScript环境都是以单线程模式运行的。
JavaScript中的异步,也都是单线程中的异步。

为什么JavaScript环境都是以单线程模式运行的?

在有些资料中,其解释是:因为JavaScript最初的设计,是一门运行在浏览器端的脚本语言,多线程的话,容易引发线程安全问题。

对于这个解释,我个人不认可,因为有很多处理线程安全的方法。

我个人观点,其原因是:JavaScript最初的设计,是一门运行在浏览器端的脚本语言,在浏览器应用中,可能有好几个进程,这些进程又可能有好几个线程,如果对于运行在浏览器端的脚本语言,还允许其多线程的话,容易导致性能问题

执行原理

同步模式

假设存在一段代码如下:

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

function bar() {
console.log('bar task')
}

function foo() {
console.log('foo task')
bar()
}
foo()
console.log('global end')

首先,JavaScript执行引擎会在调用栈中压入一个匿名的调用,我们可以把这个匿名的调用理解为把全部的代码放到了一个匿名函数当中去执行。此时,调用栈(CallStack)如下:

(anonymous)

然后,JavaScript执行引擎逐行执行代码。第一行console.log('global begin')入栈。调用栈(CallStack)如下:

console.log('global begin')
(anonymous)

执行console.log('global begin'),打印"global begin",console.log('global begin')执行完毕,出栈。调用栈(CallStack)如下:

(anonymous)

继续,执行如下代码,调用栈(CallStack)没有任何变化。

1
2
3
4
5
6
7
8
function bar() {
console.log('bar task')
}

function foo() {
console.log('foo task')
bar()
}

执行foo()foo()入栈,调用栈(CallStack)如下:

foo()
(anonymous)

执行foo()中代码。console.log('foo task')入栈,打印"foo task",执行完毕后console.log('foo task')出栈。再执行bar()bar()入栈,调用栈(CallStack)如下:

bar()
foo()
(anonymous)

最后,bar()foo()依次出栈,console.log('global end')入栈又出栈,(anonymous)出栈,执行完毕。

异步模式

JavaScript的异步模式,是基于事件循环和消息队列实现的。

异步模式

如下,是一段有异步模式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log('global begin')

setTimeout(function timer1() {
console.log('timer1 invoke')
}, 1800)

setTimeout(function timer2() {

console.log('timer2 invoke')

setTimeout(function inner() {
console.log('inner invoke')
}, 1000)

}, 1000)

console.log('global end')

首先执行console.log('global begin'),入栈又出栈。然后执行如下代码:

1
2
3
setTimeout(function timer1() {
console.log('timer1 invoke')
}, 1800)

setTimeout入栈,调用栈(CallStack)如下:

setTimeout()
(anonymous)

执行setTimeout,此时,会将倒计时器timer1交由另一个线程(例如WebAPIs)维护;然后setTimeout执行完毕,出栈。调用栈(CallStack)如下:

(anonymous)

继续执行,代码如下:

1
2
3
4
5
6
7
8
9
setTimeout(function timer2() {

console.log('timer2 invoke')

setTimeout(function inner() {
console.log('inner invoke')
}, 1000)

}, 1000)

同样,入栈,执行,将倒计时器timer2交由另一个线程(例如WebAPIs)维护,出栈。
再执行console.log('global end')console.log('global end')入栈,出栈;匿名调用也出栈。此时,调用栈(CallStack)是空的。
然后,EventLoop开始运转,Queue中并没有内容。
WebAPIs维护的两个倒计时器,会在时间到了之后,将timer2()timer1()放入到Queue中,然后再由EventLoop将其压入调用栈,进行执行。

上述过程,就是JavaScript在异步调用的一个原理。

Promise

什么是Promise

概述

Promise,正如字面含义,承诺。

在JavaScript中,Promise对象表示一个异步任务。在一开始,该承诺处于待定状态,我们称之为"Pending";最终如果成功,用"Fulfilled"表示;有可能失败,用"Rejected"表示;如果是"Fulfilled",会有"onFulfilled"的任务被执行;如果是"Rejected",会有"onRejected"的任务被执行。

基本用法

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const promise = new Promise(function (resolve, reject) {

// 试图兑现承诺
// 试图兑现承诺
// 试图兑现承诺

// 达成
resolve(100)

// 失败
// reject(new Error( 'promise rejected'))
})

promise.then(function (value) {
console.log('resolved', value)
}, function (error) {
console.log('rejected', error)
})

console.log('XXX')

运行结果:

1
2
XXX
resolved 100

解释说明:

  • Promise的构造函数,接收另一个函数作为参数,而所接收的函数,就是作为试图兑现承诺的函数。
  • 试图兑现承诺的函数,接收两个参数,分别是resolve函数和reject函数。
    resolve函数将promise对象的状态修改为fulfilled,一般我们将异步任务的操作结果通过resolve传递出去,在上文的例子中,我们传入100。
    reject函数将promise对象的状态修改为rejected,一般传递的是一个错误的对象,用来表示承诺失败的原因。
  • promise对象被创建完成后,我们可以调用这个对象的then方法分别去指定onFulfilled和onRejected回调函数。
    第一个参数就是onFulfilled的回调函数,第二个参数是onRejected的回调函数。

上述执行结果,还有一个特点,先打印XXX,再打印resolved 100
因为,即使Promise当中没有任何的异步操作,then方法当中所指定的回调函数,依然进入到队列当中排队,必须要等待这里同步代码全部执行完了才会执行。

使用案例

用Promise封装AJAX请求的例子,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

ajax('https://kakawanyifan.com/').then(function (res) {
console.log(res)
}, function (err) {
console.log(err)
})

链式调用

then方法的返回

Promise对象的then方法,会返回一个全新的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
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

let ajax_1 = ajax('https://kakawanyifan.com/')

let ajax_2 = ajax_1.then(function (res) {
console.log(res)
}, function (err) {
console.log(err)
})

console.log(ajax_2)
console.log(ajax_1 === ajax_2)

运行结果:

1
2
Promise
false

then方法中回调函数的返回

特别的,我们还可以在then方法的回调函数中指定返回。示例代码:

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
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

ajax('https://kakawanyifan.com/').then(function (res) {
// console.log(res)
return '123'
}, function (err) {
// console.log(err)
return '456'
}).then(function (res) {
console.log(res)
}, function (err) {
console.log(err)
})

运行结果:

1
2
Promise
123

解释说明:前面then方法中回调函数的返回值会作为后面then方法回调的参数。

then方法中回调函数返回promise对象

如果在then方法回调函数中返回的是一个promise对象的话,后面的then方法是为所返回的这个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
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

ajax('https://kakawanyifan.com/').then(function (res) {
// console.log(res)
return ajax('https://kakawanyifan.com/')
}, function (err) {
// console.log(err)
return ajax('https://kakawanyifan.com/')
}).then(function (res) {
console.log(res)
}, function (err) {
console.log(err)
})

运行结果:

1
<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Kaka Wan Yifan</title>【部分运行结果略】

解释说明:如果在then方法回调函数中返回的是一个promise对象的话,后面的then方法是为所返回的这个promise对象去注册了对应的回调。

小结

  1. Promise对象的then方法会返回一个全新的Promise对象。
  2. 后面的then方法就是在上一个then返回的Promise注册回调。
  3. 前面then方法中回调函数的返回值会作为后面then方法回调的参数。
  4. 如果在then方法回调函数中返回的是一个promise对象的话,后面的then方法是为所返回的这个promise对象去注册了对应的回调。

异常处理

抛出异常

如果Promise的结果是失败,则会调用then方法中的onRejected回调函数。
如果在Promise执行的过程当中出现了异常,也会调用then方法中的onRejected回调函数。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
function ajax(url) {
return new Promise(function (resolve, reject) {
throw new Error()
})
}

ajax('https://kakawanyifan.com/').then(function (res) {
console.log(res)
}, function (err) {
console.log('catch')
console.log(err)
})

运行结果:

1
2
3
4
5
6
catch
Error
at <anonymous>:3:15
at new Promise (<anonymous>)
at ajax (<anonymous>:2:12)
at <anonymous>:7:1

catch方法

我们也可以用Catch方法处理异常。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
function ajax(url) {
return new Promise(function (resolve, reject) {
throw new Error()
})
}

ajax('https://kakawanyifan.com/').then(function (res) {
console.log(res)
}).catch(function (err) {
console.log('catch')
console.log(err)
})

运行结果:

1
2
3
4
5
6
catch
Error
at <anonymous>:3:15
at new Promise (<anonymous>)
at ajax (<anonymous>:2:12)
at <anonymous>:7:1

上文我们讨论过,then方法返回一个Promise对象。而这里的catch,实在为返回的Promise对象注册监听方法;并不是给ajax('https://kakawanyifan.com/')的Promise对象注册监听方法。

建议

上述两种方法,最常见的实现,是第二种。
这种方法,对于Promise链条上任何一个异常都会被一直向后传递,直至被捕获。也就是说,这种方式是给整个Promise链条注册的失败回调,相对来讲要更通用一些。

并行执行

上文的操作都是通过Promise去串联执行多个异步任务,也就是一个任务结束过后再去开启下一个任务。
如果我们想并行执行多个异步任务呢?

Promise.all

Promise.all(),将多个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
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}


let pa = Promise.all([
ajax('https://kakawanyifan.com/10000'),
ajax('https://kakawanyifan.com/20000')
])

pa.then(function (res) {
console.log(res)
}, function (err) {
console.log('catch')
console.log(err)
})

运行结果:

1
2
Promise 
(2) [`<!DOCTYPE html><html lang="zh-CN" data-theme="ligh…heme: 'default',\n })\n})\n}\x3C/script></body></html>`, `<!DOCTYPE html><html lang="zh-CN" data-theme="ligh…heme: 'default',\n })\n})\n}\x3C/script></body></html>`]

如运行结果所示,得到的结果是一个数组,数组中包含着每一个异步任务执行过后的结果。

需要注意的是,只有所有任务都成功结束了,才会成功结束,如果说其中有任何一个任务失败了,就会以失败结束。

Promise.race

Promise.race(),同样可以把多个Promise对象组合为一个全新的Promise对象。
Promise.all()不同的是,Promise.all()是等待所有的任务结束过后才会结束,而Promise.race()是跟着我们所有任务当中第一个完成的任务一起结束,也就是说,只要有任何一个任务完成了,所返回的新的Promise对象也就会完成。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
let request_promise = ajax('https://kakawanyifan.com/')
let timeout_promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('timeout')), 5000)
})


Promise.race([request_promise, timeout_promise]).then(value => {
console.log(value)
}).catch(error => {
console.log(error)
})

Generator

生成器函数

语法格式

在语法上,生成器函数比普通函数多了一个星号。

我们调用生成器函数,并不会立即去执行函数,而是得到一个生成器对象。直到我们手动调用这个对象的next方法,这个函数的函数体才会开始执行。

示例代码:

1
2
3
4
5
function * foo() {
console.log('start')
}
let generator = foo()
generator.next()

运行结果:

1
2
start
{value: undefined, done: true}

yield

我们可以在函数内部,随时使用yield向外去返回一个值,然后在next方法获取返回的值。

在这个返回的对象当中,还有一个done属性,用来表示这个生成器是否已经全部执行完毕。

示例代码:

1
2
3
4
5
6
7
8
function * foo() {
console.log('start')
yield 'foo-yield'
}
let generator = foo()
let result = generator.next()

console.log(result)

运行结果:

1
2
start
{value: 'foo-yield', done: false}

而且,yield不会像return语句一样立即去结束这个函数的执行,只会暂停我们这个生成器函数的执行,直到我们外界下一次去调用我们生成器对象的next方法时,会继续从yield的这个位置往下直行。

示例代码:

1
2
3
4
5
6
7
8
9
10
function * foo() {
console.log('start')
yield 'foo-yield'
}
let generator = foo()
let result = generator.next()

console.log(result)

generator.next()

运行结果:

1
2
3
start
{value: 'foo-yield', done: false}
{value: undefined, done: true}

我们可以在调用生成器对象的next方法时传入了一个参数;所传入的这个参数会作为yield的这个语句的返回值。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
function * foo() {
console.log('start')
let res = yield 'foo-yield'
console.log(res)
}
let generator = foo()
let result = generator.next()

console.log(result)

generator.next('12345')

运行结果:

1
2
3
4
start
{value: 'foo-yield', done: false}
12345
{value: undefined, done: true}

throw

如果我们在外部手动调用生成器对象的throw方法,可以对生成器内部抛出一个异常,内部可以通过try-catch去捕获异常。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function * foo() {
console.log('start')

try {
let res = yield 'foo-yield'
console.log(res)
} catch (error) {
console.log(error)
}
}
let generator = foo()
let result = generator.next()

console.log(result)

// generator.next('12345')

generator.throw(new Error('ERROR'))

运行结果:

1
2
3
4
5
start
{value: 'foo-yield', done: false}
Error: ERROR
at <anonymous>:18:17
{value: undefined, done: true}

生成器管理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
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

function * gFunc (url){
let res = yield ajax(url)
console.log(res)
}

let g = gFunc('https://kakawanyifan.com/')
let result = g.next()

result.value.then(data => {
g.next(data)
})

运行结果:

1
<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Kaka Wan Yifan</title>【部分运行结果略】

解释说明:

  • gFunc是生成器函数
    • 使用yield返回一个ajax调用,即返回一个Promise对象。
    • 使用res接收yield的返回,并打印。
  • g = gFunc('https://kakawanyifan.com/'),调用这个生成器函数,去得到一个生成器对象,再调用这个对象的next方法。
  • next方法返回对象的value,就是yield所返回的Promise对象。
    所以,我们通过then的方式去指定这个promise的回调。
  • 在Promise的回调中,我们拿到Promise的执行结果,再调用一次next传递进去。

如此,在生成器函数内部,我们就彻底消灭了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
38
39
40
41
42
43
44
45
46
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

function * gFunc() {
try {
let users = yield ajax('/api/users.json')
console.log(users)

let posts = yield ajax('/api/posts.json')
console.log(posts)

let urls = yield ajax('/api/urls11.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}

function handleResult(result) {

// 生成器函数结束
if (result.done) return

result.value.then(data => {
handleResult(g.next(data))
}, error => {
g.throw(error)
})
}


let g = gFunc()
handleResult(g.next())

还可以把handleResult部分,也再进行封装,示例代码:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

function * gFunc() {
try {
let users = yield ajax('/api/users.json')
console.log(users)

let posts = yield ajax('/api/posts.json')
console.log(posts)

let urls = yield ajax('/api/urls11.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}

function co(generator){

function handleResult(result) {

// 生成器函数结束
if (result.done) return

result.value.then(data => {
handleResult(g.next(data))
}, error => {
g.throw(error)
})
}


let g = generator()
handleResult(g.next())
}

co(gFunc)

Async

什么是Async

在ECMAScript2017(ES8)的标准当中,新增了一个叫做Async的函数,同样提供了更好的异步编程体验,其实也是基于生成器函数。

简单体验

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Async 函数定义:
async function foo() {}

// Async 函数表达式:
const foo = async function () {};

// Async 方法定义:
let obj = {
async foo() {}
}

// Async 箭头函数:
const foo = async () => {};

原理:Promise对象

async函数返回的是一个封装的Promise对象。示例代码:

1
2
3
4
5
6
async function asyncFunc() {
return 123;
}

asyncFunc()
.then(x => console.log(x));

运行结果:

1
2
123
Promise {<fulfilled>: undefined}

如果在函数中抛出异常,则会reject Promise。示例代码:

1
2
3
4
5
6
async function asyncFunc() {
throw new Error('Problem!');
}

asyncFunc()
.catch(err => console.log(err));

运行结果:

1
2
3
Error: Problem!
at asyncFunc (<anonymous>:2:11)
at <anonymous>:5:1

await

上面的例子中我们在async函数使用的是同步的代码,如果想要在async中执行异步代码,则可以使用await,注意await只能在async中使用。await后面接的是一个Promise。如果Promise完成了,那么await被赋值的结果就是Promise的值。如果Promise被reject了,那么await将会抛出异常。

示例代码:

1
2
3
4
async function asyncFunc() {
let result = await otherAsyncFunc();
console.log(result);
}

我们可以顺序处理异步执行的结果。示例代码:

1
2
3
4
5
6
async function asyncFunc() {
let result1 = await otherAsyncFunc1();
console.log(result1);
let result2 = await otherAsyncFunc2();
console.log(result2);
}

也可以并行执行异步结果。示例代码:

1
2
3
4
5
6
7
async function asyncFunc() {
const [result1, result2] = await Promise.all([
otherAsyncFunc1(),
otherAsyncFunc2(),
]);
console.log(result1, result2);
}

处理异常。示例代码:

1
2
3
4
5
6
7
async function asyncFunc() {
try {
await otherAsyncFunc();
} catch (err) {
console.error(err);
}
}

案例

最后,上文的例子,以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
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
hr.responseType = 'json'
xhr.onload = function () {
if (this.status = 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

async function gFunc() {
try {
let users = await ajax('/api/users.json')
console.log(users)

let posts = await ajax('/api/posts.json')
console.log(posts)

let urls = await ajax('/api/urls.json')
console.log(urls)
} catch (error) {
console.log(error)
}
}


let promise = gFunc()
promise.then(() => {
console.log('all completed')
})

注意

如果async中返回的不是Promise,那么将会被封装成为Promise。如果已经是Promise对象的话,则不会被再次封装。示例代码:

1
2
3
4
5
async function asyncFunc() {
return Promise.resolve(123);
}
asyncFunc()
.then(x => console.log(x))

如果返回一个rejected的Promise对象,则和抛出异常一样的结果。示例代码:

1
2
3
4
5
async function asyncFunc() {
return Promise.reject(new Error('ERROR!'));
}
asyncFunc()
.catch(err => console.error(err));

如果我们只想触发异步方法,并不想等待其执行完毕,那么不使用await。示例代码:

1
2
3
4
5
6
7
8
9
async function asyncFunc() {
const writer = openFile('someFile.txt');
// don’t wait
writer.write('hello');
// don’t wait
writer.write('world');
// wait for file to close
await writer.close();
}

最后,await关键字,只能够出现在async函数内部,不能直接在外部。

(据说以后会有await单独出现在外部,但至少截止2024年7月,还没有。)

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/12005
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板