《你不知道的javascript 》读书笔记—JS 异步
前言
js异步编程主要是处理程序将来执行和现在执行的关系。js块中分为将来和现在,也就是程序中现在执行的部分会按照代码逻辑和顺序立即执行,将来的部分就是在将来某个时候响应某个事件(定时器、ajax响应、鼠标点击等),程序中就引入了异步机制。js是单线程的,不支持多线程,那为何会有异步产生的?那就要说道说道js的宿主环境(浏览器),浏览器是支持多线程的。
浏览器是事件驱动的,浏览器中很多行为是异步的,回创建事件并放入到执行队列中,js引擎是单线程处理它的的任务队列,当异步事件发生时,会将其放入执行队列里面。
setTimeout也是一样,当调用的时候,js引擎会启动定时器timer,大约xxms以后执行xxx,当定时器时间到,就把该事件放到主事件队列等待处理(浏览器不忙的时候才会真正执行)。
ajax异步,也是浏览器新开一个线程,当请求状态发生变更,事件回调函数就会放入到事件队列处理。
知识点
- 浏览器中的控制台也存在异步,是控制台IO。
- 事件循环,就是宿主调用js引擎按照顺序执行事件。
- 并行线程实现异步,里面讲述了js是单线程的原因之一。
- 并发,对一些异步事件处理,进行协调和优化,防止竞态出现。
- 语句顺序和代码执行顺序有差异。
事件循环
1 | // eventLoop是一个用作队列的数组 |
异步的并发应用场景
协作:
当处理大量的结果集列表,可以将当前的任务进行批处理或者多步骤处理
举例:考虑一个需要遍历很长的结果列表进行值转换的 Ajax响应处理函数 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14var res = [];
// response(..)从Ajax调用中取得结果数组
function response(data) {
// 添加到已有的res数组
res = res.concat(
// 创建一个新的变换数组把所有data值加倍
data.map( function(val){
return val * 2;
} )
);
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );当数据量过大,页面上的其他的代码就不能执行,就会出现页面白屏,卡死的状态
用并发协作处理异步,进行批处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23var res = [];
// response(..)从Ajax调用中取得结果数组
function response(data) {
// 一次处理1000个
var chunk = data.splice( 0, 1000 );
// 添加到已有的res组
res = res.concat(
// 创建一个新的数组把chunk中所有值加倍
chunk.map( function(val){
return val * 2;
} )
);
// 还有剩下的需要处理吗?
if (data.length > 0) {
// 异步调度下一次批处理
setTimeout( function(){
response( data );
}, 0 );
}
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
交互和非交互:
- 场景:定义的变量的值共享
1 | var a, b; |
任务
1 | //解释setTimeout(function(){.....},0)的执行 |
**回调 **
回调是异步处理必然发生的事情,回调函数是将程序将来执行的部分代码交给某个第三方(你的代码跟这个第三方没有必然的联系)去处理,从而导致回调函数的不确定性,导致存在潜在的风险:
- 调用回调过早
- 调用回调过晚
- 调用回调的次数太少或太多
- 回调异常(吞掉可能出现的错误或异常 )
- 回调地狱(嵌套回调与链式回调 )
回调的场景:
1 | //回调过早 |
Promise
获取promise决议,决议的值是不变的。调用then()函数注册两个函数,成功和失败,返回promise对象,称为promise链式调用。永远在将来执行,promise 归一保证了行为的一致性 ,不管是现在还是将来,都看做未来值来处理。Promise语法上限制了自己,虽说有了then的思路,可以用thenable 鸭子类型检测 Promise。
识别 Promise(或者行为类似于 Promise 的东西) 就是定义某种称为 thenable 的东西, 将其定义为任何具有 then(..) 方
法的对象和函数。 我们认为, 任何这样的值就是 Promise 一致的 thenable。 这个then()方法的鉴别Promise——具有 then 方法的鸭子类型(类型检查)。
then(..) 函数的一个对象或函数值完成一个 Promise,它会自动被识别为 thenable, 并被按照特定的规则处理被认定为Promise决议。
1 | //带有then方法的对象,在检测Promise时,会被识别为Promise对象 |
信任问题
调用过早(一个任务有时同步完成, 有时异步完成, 这可能会导致竞态条件.)
该情况不会在Promise里面发生,即便是立即完成的 Promise(类似于 new Promise(function(resolve){
resolve(42); }) ) 也无法被同步观察到。 一个Promise决议了,提供的then(….)的回调也总会被异步调用调用太晚
1
2
3
4
5
6
7
8
9
10
11//一旦Promise决议,就会立即执行then(...)注册的回调,回调中的任意一个都无法影响或延误对其他回调的调用。
p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
} );
// A B C未调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//Promise未决议,将会被挂起
// 用于超时一个Promise的工具
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Timeout!" );
}, delay );
} );
}
// 设置foo()超时
Promise.race( [
foo(), // 试着开始foo()
timeoutPromise( 3000 ) // 给它3秒钟
] )
.then(
function(){
// foo(..)及时完成!
},
function(err){
// 或者foo()被拒绝, 或者只是没能按时完成
// 查看err来了解是哪种情况
}
);
调用多次
Promise调用then方法注册的回调就只会调用一次。出现多次的情况,是由开发者自己控制的。
吞掉异常或者错误
如果Promise在创建过程中出错,then就会调用reject函数;如果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//Promise在创建过程中出错,rejected捕捉异常
var p = new Promise( function(resolve,reject){
foo.bar(); // foo未定义, 所以会出错!
resolve( 42 ); // 永远不会到达这里 :(
} );
p.then(
function fulfilled(){
// 永远不会到达这里 :(
},
function rejected(err){
// err将会是一个TypeError异常对象来自foo.bar()这一行
}
);
//Promise是在完成后查看结果时出错,表面上是吞掉异常
//p.then(..) 调用本身返回了另外一个 promise, 正是这个 promise 将会因 TypeError 异常而被拒绝。
//这里为什么没调用rejected()函数???
//p的决议已经是42了,如果查看 p 的决议,因为出错就将p变成一个拒绝。
//那么就违背了 Promise 一旦决议就不可再变的原则
var p = new Promise( function(resolve,reject){
resolve( 42 );
} );
p.then(
function fulfilled(msg){
foo.bar();
console.log( msg ); // 永远不会到达这里 :(
},
function rejected(err){
// 永远也不会到达这里 :(
});
链式流
流程:
- 调用Promise的then()函数会自动创建一个新的Promise并返回。
- 在完成或拒绝处理函数内部, 如果返回一个值或抛出一个异常, 新返回的(可链接的) Promise 就相应地决议。
- 如果完成或拒绝处理函数返回一个 Promise, 它将会被展开, 这样一来, 不管它的决议值是什么, 都会成为当前
then(..) 返回的链接 Promise 的决议值。
1 | //应用场景:消息传递 |
错误处理
异步处理的方式:
error-first回调方式(异步处理对错误处理的流行方式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//error回调风格
function foo(cb) {
setTimeout( function(){
try {
var x = baz.bar();
cb( null, x ); // 成功!
}catch (err) {
cb( err );
}
}, 100 );
}
foo( function(err,val){
if (err) {
console.error( err ); // 烦 :(
}else {
console.log( val );
}
} );
//只有baz.bar()调用失败时抛出错误被try-catch捕获到,输出错误信息。同时要注意调用多层error-first风格的try-catch会导致回调地狱
split-back回调方式(Promise的分离方式)
1
2
3
4
5
6
7
8
9
10
11var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 数字没有string函数, 所以会抛出错误
console.log( msg.toLowerCase() );
},
function rejected(err){
// 永远不会到达这里
}
);
//参数msg没有toLowerCase()的方法,会抛出异常;但是p已经被填充为42,同时Promise一旦决议,不可改变;那么这个错误就会被吞掉。处理错误函数rejected()是为Promise对象p准备的。
模式
Promise.all([…])
1
2
3
4
5
6
7
8
9
10
11
12
13//应用场景:同时发送两个Ajax请求,再发送第三个Ajax请求
// request(..)是一个Promise-aware Ajax工具
// 就像我们在本章前面定义的一样
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.all( [p1,p2] ).then( function(msgs){
// 这里, p1和p2完成并把它们的消息传入
return request(
"http://some.url.3/?v=" + msgs.join(",")
);
} ).then( function(msg){
console.log( msg );
} );Promise.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//Promise超时模式
//为foo设定超时
function foo(){
return new Promise((resolve,reject) => {
setTimeout(function(){
resolve(42);
},5000)
})
}
function timeoutPromise(delay){
return new Promise((resolve,reject) => {
setTimeout(function(){
reject('timeout!');
},delay)
})
}
//为foo设定超时
Promise.race( [
foo(), // 启动foo()
timeoutPromise( 3000 ), // 给它3秒钟
] ).then(
function(){
// foo(..)按时完成!
console.log('foo() finish!!!');
},
function(err){
// 要么foo()被拒绝, 要么只是没能够按时完成,
// 因此要查看err了解具体原因
console.log(err);
}
);
all([….])和race([….])的变体
1
2
3
4
5
6
7
8
91.none([ .. ])
这个模式类似于 all([ .. ]) , 不过完成和拒绝的情况互换了。 所有的 Promise 都要被拒绝, 即拒绝转化为完成值,
反之亦然。
2.any([ .. ])
这个模式与 all([ .. ]) 类似, 但是会忽略拒绝, 所以只需要完成一个而不是全部。
3.first([ .. ])
这个模式类似于与 any([ .. ]) 的竞争, 即只要第一个 Promise 完成, 它就会忽略后续的任何拒绝和完成。
4.last([ .. ])
这个模式类似于 first([ .. ]) , 但却是只有最后一个完成胜出。
并发迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19var p1 = Promise.resolve( 21 );
var p2 = Promise.resolve( 42 );
var p3 = Promise.reject( "Oops" );
// 把列表中的值加倍, 即使是在Promise中
Promise.map( [p1,p2,p3], function(pr,done){
// 保证这一条本身是一个Promise
Promise.resolve( pr ).then(
// 提取值作为v
function(v){
// map完成的v到新值
done( v * 2 );
},
// 或者map到promise拒绝消息
done
);
} )
.then( function(vals){
console.log( vals ); // [42,84,"Oops"]
} );
局限性
顺序错误处理
如果在then(…)中的错误做了隐式处理,那么到最后无法得到错误信息。
单一值
执行过程返回单个数据,获取多个数据需要嵌套,也可使用es6的解构mag = [a,b,c]。
单决议
Promise 只能被决议一次(完成或拒绝),对事件的重复调用,Promise只能决议一次,后面再去调用还是原值(例如按钮)。
惯性
如果项目中使用Promise,项目中有不同风格的回调,导致将不同风格的回调转换成Promise的回调风格。(书中提到Promise.wrap())
无法取消的Promise
创建了Promise,但是在决议的过程中一直悬而未决,导致Promise没办法决议;也没办法取消该任务的进程。
小结
对于es6的异步理解很多,对Promise里面的api用法和原理理解有很大进步。如果有兴趣可以看这篇博客你可能不知道的 Promise
本文由 Abert 创作,采用 知识共享署名 4.0 国际许可协议。
本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。