13- 异步编程(上):JS 异步编程都有哪些方案
我们都知道,比较常用的数组方法有 push、pop、slice、map 和 reduce 等。上一讲我带你剖析了 sort 方法以及 V8 源码中关于排序的内容,本讲则会围绕这几个常用方法,并结合 V8 的源代码带你手写这些方法的底层实现。
那么,为了方便你更好地理解本讲的内容,在课程开始前请你先回想一下:
-
reduce 方法里面的参数都是什么作用?
-
push 和 pop 的底层逻辑是什么样的呢?
带着思考,我们开始今天的学习。
push 方法的底层实现
为了更好地实现 push 的底层方法,你可以先去 ECMA 的官网去查一下关于 push 的基本描述(链接:ECMA 数组的 push 标准),我们看下其英文的描述,如下所示。
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
30When the push method is called with zero or more arguments, the following steps are taken: 1. Let O be ? ToObject(this value). 2. Let len be ? LengthOfArrayLike(O). 3. Let argCount be the number of elements in items. 4. If len + argCount > 2^53 - 1, throw a TypeError exception. 5. For each element E of items, do a. Perform ? Set(O, ! ToString(F(len)), E, true). b. Set len to len + 1. 6. Perform ? Set(O, "length", F(len), true). 7. Return F(len).
从上面的描述可以看到边界判断逻辑以及实现的思路,根据这段英文,我们将其转换为容易理解代码,如下所示。
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
42Array.prototype.push = function(...items) { let O = Object(this); let len = this.length >>> 0; let argCount = items.length >>> 0; if (len + argCount > 2 ** 53 - 1) { throw new TypeError("The number of array is over the max value") } for(let i = 0; i < argCount; i++) { O[len + i] = items[i]; } let newLength = len + argCount; O.length = newLength; return newLength; }
从上面的代码可以看出,关键点就在于给数组本身循环添加新的元素 item,然后调整数组的长度 length 为最新的长度,即可完成 push 的底层实现。
其中关于长度的部分需要做无符号位移,无符号位移在很多源码中你都会看到。关于为什么一些变量要进行无符号位移,你可以自己研究一下,比如在 Stack Overflow 中有一些高票的回答,这里就不占用篇幅了。下面我们再看来一下 pop 的实现。
pop 方法的底层实现
同样我们也一起来看下 pop 的底层实现,你也可以先去 ECMA 的官网去查一下关于 pop 的基本描述(链接:ECMA 数组的 pop 标准),我们还是同样看下英文的描述。
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
42When the pop method is called, the following steps are taken: 1. Let O be ? ToObject(this value). 2. Let len be ? LengthOfArrayLike(O). 3. If len = 0, then Perform ? Set(O, "length", +0F, true). Return undefined. 4. Else, Assert: len > 0. Let newLen be F(len - 1). Let index be ! ToString(newLen). Let element be ? Get(O, index). Perform ? DeletePropertyOrThrow(O, index). Perform ? Set(O, "length", newLen, true). Return element.
从上面的描述可以看到边界判断逻辑以及实现的思路,根据上面的英文,我们同样将其转换为可以理解的代码,如下所示。
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
39Array.prototype.pop = function() { let O = Object(this); let len = this.length >>> 0; if (len === 0) { O.length = 0; return undefined; } len --; let value = O[len]; delete O[len]; O.length = len; return value; }
其核心思路还是在于删掉数组自身的最后一个元素,index 就是数组的 len 长度,然后更新最新的长度,最后返回的元素的值,即可达到想要的效果。另外就是在当长度为 0 的时候,如果执行 pop 操作,返回的是 undefined,需要做一下特殊处理。
看完了 pop 的实现,我们再来看一下 map 方法的底层逻辑。
map 方法的底层实现
同样你可以去 ECMA 的官网去查一下关于 map 的基本描述(链接:ECMA 数组的 map 标准),请看英文的表述。
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
45When the map method is called with one or two arguments, the following steps are taken: 1. Let O be ? ToObject(this value). 2. Let len be ? LengthOfArrayLike(O). 3. If IsCallable(callbackfn) is false, throw a TypeError exception. 4. Let A be ? ArraySpeciesCreate(O, len). 5. Let k be 0. 6. Repeat, while k < len, a. Let Pk be ! ToString(F(k)). b. Let kPresent be ? HasProperty(O, Pk). c. If kPresent is true, then Let kValue be ? Get(O, Pk). Let mappedValue be ? Call(callbackfn, thisArg, « kValue, F(k), O »). Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue). d. Set k to k + 1. 7. Return A.
同样的,根据上面的英文,我们将其转换为可理解的代码,如下所示。
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
52
53
54
55
56
57
58
59
60Array.prototype.map = function(callbackFn, thisArg) { if (this === null || this === undefined) { throw new TypeError("Cannot read property 'map' of null"); } if (Object.prototype.toString.call(callbackfn) != "[object Function]") { throw new TypeError(callbackfn + ' is not a function') } let O = Object(this); let T = thisArg; let len = O.length >>> 0; let A = new Array(len); for(let k = 0; k < len; k++) { if (k in O) { let kValue = O[k]; let mappedValue = callbackfn.call(T, KValue, k, O); A[k] = mappedValue; } } return A; }
有了上面实现 push 和 pop 的基础思路,map 的实现也不会太难了,基本就是再多加一些判断,循环遍历实现 map 的思路,将处理过后的 mappedValue 赋给一个新定义的数组 A,最后返回这个新数组 A,并不改变原数组的值。
我们在 “07 | 数组原理(上):帮你梳理眼花缭乱的数组 API” 中也介绍过数据的方法分类,遍历类型的方法最后返回的都是一个新数组,并不改变原有数组的值,这点你需要牢记。
最后我们来看看 reduce 的实现。
reduce 方法的底层实现
ECMA 官网关于 reduce 的基本描述(链接:ECMA 数组的 pop 标准),如下所示。
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78When the reduce method is called with one or two arguments, the following steps are taken: 1. Let O be ? ToObject(this value). 2. Let len be ? LengthOfArrayLike(O). 3. If IsCallable(callbackfn) is false, throw a TypeError exception. 4. If len = 0 and initialValue is not present, throw a TypeError exception. 5. Let k be 0. 6. Let accumulator be undefined. 7. If initialValue is present, then Set accumulator to initialValue. 8. Else, Let kPresent be false. Repeat, while kPresent is false and k < len, Let Pk be ! ToString(F(k)). Set kPresent to ? HasProperty(O, Pk). If kPresent is true, then Set accumulator to ? Get(O, Pk). Set k to k + 1. If kPresent is false, throw a TypeError exception. 9. Repeat, while k < len, Let Pk be ! ToString(F(k)). Let kPresent be ? HasProperty(O, Pk). If kPresent is true, then Let kValue be ? Get(O, Pk). Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, F(k), O »). Set k to k + 1. 10. Return accumulator.
还是将其转换为我们自己的代码,如下所示。
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84Array.prototype.reduce = function(callbackfn, initialValue) { if (this === null || this === undefined) { throw new TypeError("Cannot read property 'reduce' of null"); } if (Object.prototype.toString.call(callbackfn) != "[object Function]") { throw new TypeError(callbackfn + ' is not a function') } let O = Object(this); let len = O.length >>> 0; let k = 0; let accumulator = initialValue; if (accumulator === undefined) { for(; k < len ; k++) { if (k in O) { accumulator = O[k]; k++; break; } } throw new Error('Each element of the array is empty'); } for(;k < len; k++) { if (k in O) { accumulator = callbackfn.call(undefined, accumulator, O[k], O); } } return accumulator; }
根据上面的代码及注释,有几个关键点你需要重点关注:
-
初始值默认值不传的特殊处理;
-
累加器以及 callbackfn 的处理逻辑。
这两个关键问题处理好,其他的地方和上面几个方法实现的思路是基本类似的,你要学会举一反三。
总结
到这里,本讲的内容就先告一段落了。这一讲内容虽少,但却是你必须要掌握的内容。
这一讲中,我把 JS 的 push 、pop、map、reduce 的底层方法挨个带你实现了一遍,希望你能对此形成一套自己的思路。我所提供的实现代码,虽然不能完全和 V8 源码中实现的代码媲美,但是在正常的使用中,你如果自己能实现到这个程度,基本也可以满足要求了。
讲到这里,我再贴一下 V8 数组关于各种方法的实现源码地址,如下表所示。
数组方法 | V8 源码地址 |
---|---|
pop | V8 源码 pop 的实现 |
push | V8 源码 push 的实现 |
map | V8 源码 map 的实现 |
slice | V8 源码 slice 的实现 |
filter | V8 源码 filter 的实现 |
... | ... |
关于本讲内容没有提到的代码及方法,你可以根据自己的兴趣,尝试着实现其中的某个方法。
同时也希望你能够多思考日常工作中都有哪些经常用到的 JS 方法,并且去研究其底层源代码的实现逻辑,找机会自己实现一遍,来整体提升你的 JavaScript 的编程能力和对底层的理解能力。
下一讲我们将会进入一个全新的模块——JS 的异步编程篇,期待你能从中学习到更多的东西。每天进步一点点,加油!
14-异步编程(中):如何深入理解异步编程的核心 Promise
上一讲,我们聊了关于 JS 异步编程的发展历程以及异步编程的几种方式,那么从这一讲开始,就要深入学习了,今天要和你说的就是异步编程的核心 Promise。
其实在 ES6 标准出现之前,社区就最早提出了 Promise 的方案,后随着 ES6 将其加入进去,才统一了其用法,并提供了原生的 Promise 对象。Promise 也是日常前端开发使用比较多的编程方式,因此希望通过这一讲的学习,你能够对 Promise 异步编程的思路有更深刻的理解。
按照惯例,我先给你抛出几个问题:
-
Promise 内部究竟有几种状态?
-
Promise 是怎么解决回调地狱问题的?
现在请你带着思考,跟我一起回顾 Promise 的相关内容吧。
Promise 的基本情况
如果一定要解释 Promise 到底是什么,简单来说它就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。我们来简单看一下 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
47
48
49
50
51function read(url) { return new Promise((resolve, reject) => { fs.readFile(url, 'utf8', (err, data) => { if(err) reject(err); resolve(data); }); }); } read(A).then(data => { return read(B); }).then(data => { return read(C); }).then(data => { return read(D); }).catch(reason => { console.log(reason); });
结合上面的代码,我们一起来分析一下 Promise 内部的状态流转情况,Promise 对象在被创建出来时是待定的状态,它让你能够把异步操作返回最终的成功值或者失败原因,和相应的处理程序关联起来。
一般 Promise 在执行过程中,必然会处于以下几种状态之一。
-
待定(pending):初始状态,既没有被完成,也没有被拒绝。
-
已完成(fulfilled):操作成功完成。
-
已拒绝(rejected):操作失败。
待定状态的 Promise 对象执行的话,最后要么会通过一个值完成,要么会通过一个原因被拒绝。当其中一种情况发生时,我们用 Promise 的 then 方法排列起来的相关处理程序就会被调用。因为最后 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是一个 Promise, 所以它们可以继续被链式调用。
关于 Promise 的状态流转情况,有一点值得注意的是,内部状态改变之后不可逆,你需要在编程过程中加以注意。文字描述比较晦涩,我们直接通过一张图就能很清晰地看出 Promise 内部状态流转的情况,如下所示(图片来源于网络)。
从上图可以看出,我们最开始创建一个新的 Promise 返回给 p1 ,然后开始执行,状态是 pending,当执行 resolve 之后状态就切换为 fulfilled,执行 reject 之后就变为 rejected 的状态。
关于 Promise 的状态切换如果你想深入研究,可以学习一下 “有限状态机” 这个知识点。日常中比较常见的状态机有很多,比如马路上的红绿灯。
那么,Promise 的基本情况先介绍到这里,我们再一起来分析下,Promise 如何解决回调地狱的问题。
Promise 如何解决回调地狱
首先,请你再回想一下什么是回调地狱,回调地狱有两个主要的问题:
-
多层嵌套的问题;
-
每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。
这两种问题在 “回调函数时代” 尤为突出,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
38
39
40
41
42let readFilePromise = filename => { return new Promise((resolve, reject) => { fs.readFile(filename, (err, data) => { if (err) { reject(err) } else { resolve(data) } }) }) } readFilePromise('1.json').then(data => { return readFilePromise('2.json') });
从上面的代码中可以看到,回调函数不是直接声明的,而是通过后面的 then 方法传入的,即延迟传入,这就是回调函数延迟绑定。接下来我们针对上面的代码做一下微调,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12let x = readFilePromise('1.json').then(data => { return readFilePromise('2.json') }); x.then()
我们根据 then 中回调函数的传入值创建不同类型的 Promise,然后把返回的 Promise 穿透到外层,以供后续的调用。这里的 x 指的就是内部返回的 Promise,然后在 x 后面可以依次完成链式调用。这便是返回值穿透的效果,这两种技术一起作用便可以将深层的嵌套回调写成下面的形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21readFilePromise('1.json').then(data => { return readFilePromise('2.json'); }).then(data => { return readFilePromise('3.json'); }).then(data => { return readFilePromise('4.json'); });
这样就显得清爽了许多,更重要的是,它更符合人的线性思维模式,开发体验也更好,两种技术结合产生了链式调用的效果。
这样解决了多层嵌套的问题,那另外一个问题,即每次任务执行结束后分别处理成功和失败的情况怎么解决的呢?Promise 采用了错误冒泡的方式。其实很容易理解,我们来看看效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24readFilePromise('1.json').then(data => { return readFilePromise('2.json'); }).then(data => { return readFilePromise('3.json'); }).then(data => { return readFilePromise('4.json'); }).catch(err => { })
这样前面产生的错误会一直向后传递,被 catch 接收到,就不用频繁地检查错误了。从上面的这些代码中可以看到,Promise 解决效果也比较明显:实现链式调用,解决多层嵌套问题;实现错误冒泡后一站式处理,解决每次任务中判断错误、增加代码混乱度的问题。
接下来我们再看看 Promise 提供了哪些静态的方法。
Promise 的静态方法
我会从语法、参数以及方法的代码几个方面来分别介绍 all、allSettled、any、race 这四种方法。
all 方法
语法: Promise.all(iterable)
参数: 一个可迭代对象,如 Array。
描述: 此方法对于汇总多个 promise 的结果很有用,在 ES6 中可以将多个 Promise.all 异步请求并行操作,返回结果一般有下面两种情况。
-
当所有结果成功返回时按照请求顺序返回成功。
-
当其中有一个失败方法时,则进入失败方法。
我们来看下业务的场景,对于下面这个业务场景页面的加载,将多个请求合并到一起,用 all 来实现可能效果会更好,请看代码片段。
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90function getBannerList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('轮播数据') },300) }) } function getStoreList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('店铺数据') },500) }) } function getCategoryList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('分类数据') },700) }) } function initLoad(){ Promise.all([getBannerList(),getStoreList(),getCategoryList()]) .then(res=>{ console.log(res) }).catch(err=>{ console.log(err) }) } initLoad()
从上面代码中可以看出,在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面需要同时发出请求进行页面渲染,这样用 Promise.all 来实现,看起来更清晰、一目了然。
下面我们再来看另一种方法。
allSettled 方法
Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,返回一个新的 Promise。唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功。
我们来看一下用 allSettled 实现的一段代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const resolved = Promise.resolve(2); const rejected = Promise.reject(-1); const allSettledPromise = Promise.allSettled([resolved, rejected]); allSettledPromise.then(function (results) { console.log(results); });
从上面代码中可以看到,Promise.allSettled 最后返回的是一个数组,记录传进来的参数中每个 Promise 的返回值,这就是和 all 方法不太一样的地方。你也可以根据 all 方法提供的业务场景的代码进行改造,其实也能知道多个请求发出去之后,Promise 最后返回的是每个参数的最终状态。
接下来看一下 any 这个方法。
any 方法
语法: Promise.any(iterable)
参数: iterable 可迭代的对象,例如 Array。
描述: any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fulfilled 状态,最后 any 返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态。
还是对上面 allSettled 这段代码进行改造,我们来看下改造完的代码和执行结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const resolved = Promise.resolve(2); const rejected = Promise.reject(-1); const allSettledPromise = Promise.any([resolved, rejected]); allSettledPromise.then(function (results) { console.log(results); });
从改造后的代码中可以看出,只要其中一个 Promise 变成 fulfilled 状态,那么 any 最后就返回这个 Promise。由于上面 resolved 这个 Promise 已经是 resolve 的了,故最后返回结果为 2。
我们最后来看一下 race 方法。
race 方法
语法: Promise.race(iterable)
参数: iterable 可迭代的对象,例如 Array。
描述: race 方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数。
我们来看一下这个业务场景,对于图片的加载,特别适合用 race 方法来解决,将图片请求和超时判断放到一起,用 race 来实现图片的超时判断。请看代码片段。
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
52
53
54
55
56
57
58
59
60
61
62
63function requestImg(){ var p = new Promise(function(resolve, reject){ var img = new Image(); img.onload = function(){ resolve(img); } img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png'; }); return p; } function timeout(){ var p = new Promise(function(resolve, reject){ setTimeout(function(){ reject('图片请求超时'); }, 5000); }); return p; } Promise.race([requestImg(), timeout()]) .then(function(results){ console.log(results); }) .catch(function(reason){ console.log(reason); });
从上面的代码中可以看出,采用 Promise 的方式来判断图片是否加载成功,也是针对 Promise.race 方法的一个比较好的业务场景。
综上,这四种方法的参数传递形式基本是一致的,但是最后每个方法实现的功能还是略微有些差异的,这一点你需要留意。
总结
好了,这一讲内容就介绍到这了。这两讲,我将 Promise 的异步编程方式带你学习了一遍,希望你能对此形成更深刻的认知。关于如何自己实现一个符合规范的 Promise,在后面的进阶课程中我会带你一步步去实现,这两讲也是为后面的实践打下基础,因此希望你能好好掌握。
我最后整理了一下 Promise 的几个方法,你可以根据下面的表格再次复习。
在后续的课程中,我还会继续对 JS 异步编程的知识点进行更详细的剖析,你要及时发现自身的不足,有针对性地学习薄弱的知识。
new Promise(function(resolve, reject){
var img = new Image();
1
2
3
4
5img.onload = function(){ resolve(img); } img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png';
});
return p;
}
function timeout(){
var p = new Promise(function(resolve, reject){
1
2setTimeout(function(){ reject('图片请求超时'); }, 5000);
});
return p;
}
Promise.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17从上面的代码中可以看出,采用 Promise 的方式来判断图片是否加载成功,也是针对 Promise.race 方法的一个比较好的业务场景。 综上,这四种方法的参数传递形式基本是一致的,但是最后每个方法实现的功能还是略微有些差异的,这一点你需要留意。 ### 总结 好了,这一讲内容就介绍到这了。这两讲,我将 Promise 的异步编程方式带你学习了一遍,希望你能对此形成更深刻的认知。关于如何自己实现一个符合规范的 Promise,在后面的进阶课程中我会带你一步步去实现,这两讲也是为后面的实践打下基础,因此希望你能好好掌握。 我最后整理了一下 Promise 的几个方法,你可以根据下面的表格再次复习。 [外链图片转存中...(img-QMZPvd0x-1657765548312)] 在后续的课程中,我还会继续对 JS 异步编程的知识点进行更详细的剖析,你要及时发现自身的不足,有针对性地学习薄弱的知识。 下一讲,我们来聊聊 Generator 和 async/await,这些语法糖也是你需要掌握的内容。我们到时见。
最后
以上就是无限花卷最近收集整理的关于JavaScript核心原理精讲第四章 异步编程的全部内容,更多相关JavaScript核心原理精讲第四章内容请搜索靠谱客的其他文章。
发表评论 取消回复