《你不知道的javascript 》读书笔记—JS 异步


前言

​ js异步编程主要是处理程序将来执行和现在执行的关系。js块中分为将来和现在,也就是程序中现在执行的部分会按照代码逻辑和顺序立即执行,将来的部分就是在将来某个时候响应某个事件(定时器、ajax响应、鼠标点击等),程序中就引入了异步机制。js是单线程的,不支持多线程,那为何会有异步产生的?那就要说道说道js的宿主环境(浏览器),浏览器是支持多线程的。

​ 浏览器是事件驱动的,浏览器中很多行为是异步的,回创建事件并放入到执行队列中,js引擎是单线程处理它的的任务队列,当异步事件发生时,会将其放入执行队列里面。

​ setTimeout也是一样,当调用的时候,js引擎会启动定时器timer,大约xxms以后执行xxx,当定时器时间到,就把该事件放到主事件队列等待处理(浏览器不忙的时候才会真正执行)。

​ ajax异步,也是浏览器新开一个线程,当请求状态发生变更,事件回调函数就会放入到事件队列处理。

知识点

  • 浏览器中的控制台也存在异步,是控制台IO。
  • 事件循环,就是宿主调用js引擎按照顺序执行事件。
  • 并行线程实现异步,里面讲述了js是单线程的原因之一。
  • 并发,对一些异步事件处理,进行协调和优化,防止竞态出现。
  • 语句顺序和代码执行顺序有差异。

事件循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// eventLoop是一个用作队列的数组
// (先进, 先出)
var eventLoop = [ ];
var event;
// “永远”执行
while (true) {
// 一次tick
if (eventLoop.length > 0) {
// 拿到队列中的下一个事件
event = eventLoop.shift();
// 现在, 执行下一个事件
try {
event();
}catch (err) {
reportError(err);
}
}
}

异步的并发应用场景

协作:

  • 当处理大量的结果集列表,可以将当前的任务进行批处理或者多步骤处理

  • 举例:考虑一个需要遍历很长的结果列表进行值转换的 Ajax响应处理函数 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var 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
    23
    var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a, b;
function foo(x) {
a = x * 2;
if (a && b) {
baz();
}
}
function bar(y) {
b = y * 2;
if (a && b) {
baz();
}
}
function baz() {
console.log( a + b );
}
// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//解释setTimeout(function(){.....},0)的执行
console.log( "A" );
setTimeout( function(){
console.log( "B" );
}, 0 );
// 理论上的"任务API"
schedule( function(){
console.log( "C" );
schedule( function(){
console.log( "D" );
} );
} );

// 打印出来 A C D B

//定时器setTimeout(function(){.....},0),是等当前代码执行完之后才执行,
//任务处理是在当前事件循环 tick 结尾处, 且定时器触发是为了调度下一个事件循环 tick(如果可用的话! ) 。

**回调 **

回调是异步处理必然发生的事情,回调函数是将程序将来执行的部分代码交给某个第三方(你的代码跟这个第三方没有必然的联系)去处理,从而导致回调函数的不确定性,导致存在潜在的风险:

  • 调用回调过早
  • 调用回调过晚
  • 调用回调的次数太少或太多
  • 回调异常(吞掉可能出现的错误或异常 )
  • 回调地狱(嵌套回调与链式回调 )

回调的场景:

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
//回调过早
function timeoutify(fn,delay) {
var intv = setTimeout( function(){
intv = null;
fn( new Error( "Timeout!" ) );
}, delay );

return function() { // 还没有超时?
if (intv) {
clearTimeout( intv );
fn.apply( this, arguments );
} };
}
//回调次数太多
var tracked = false;
analytics.trackPurchase( purchaseData, function(){
if (!tracked) {
tracked = true;
chargeCreditCard();
displayThankyouPage();
}
});
//回调异常
function addNumbers(x,y) {
// 确保输入为数字
if (typeof x != "number" || typeof y != "number") {
throw Error( "Bad parameters" );
}
// 如果到达这里, 可以通过+安全的进行数字相加
return x + y;
}
addNumbers( 21, 21 ); // 42
addNumbers( 21, "21" ); // Error: "Bad parameters"

Promise

获取promise决议,决议的值是不变的。调用then()函数注册两个函数,成功和失败,返回promise对象,称为promise链式调用。永远在将来执行,promise 归一保证了行为的一致性 ,不管是现在还是将来,都看做未来值来处理。Promise语法上限制了自己,虽说有了then的思路,可以用thenable 鸭子类型检测 Promise。

识别 Promise(或者行为类似于 Promise 的东西) 就是定义某种称为 thenable 的东西, 将其定义为任何具有 then(..) 方
法的对象和函数。 我们认为, 任何这样的值就是 Promise 一致的 thenable。 这个then()方法的鉴别Promise——具有 then 方法的鸭子类型(类型检查)。

then(..) 函数的一个对象或函数值完成一个 Promise,它会自动被识别为 thenable, 并被按照特定的规则处理被认定为Promise决议。

1
2
3
4
5
6
7
//带有then方法的对象,在检测Promise时,会被识别为Promise对象
var o = { then: function(){} };
// 让v [[Prototype]]-link到o
var v = Object.create( o );
v.someStuff = "cool";
v.otherStuff = "not so cool";
v.hasOwnProperty( "then" ); // false

信任问题

  • 调用过早(一个任务有时同步完成, 有时异步完成, 这可能会导致竞态条件.)

    该情况不会在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
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
//应用场景:消息传递
//假定工具ajax( {url}, {callback} )存在
// Promise-aware ajax
function request(url) {
return new Promise( function(resolve,reject){
// ajax(..)回调应该是我们这个promise的resolve(..)函数
ajax( url, resolve );
} );
}

// 步骤1:
request( "http://some.url.1/" )
// 步骤2:
.then( function(response1){
foo.bar(); // undefined, 出错!
// 永远不会到达这里
return request( "http://some.url.2/?v=" + response1 );
} )
// 步骤3:
.then(
function fulfilled(response2){
// 永远不会到达这里
},
// 捕捉错误的拒绝处理函数
function rejected(err){
console.log( err );
// 来自foo.bar()的错误TypeError
return 42;
}
)
// 步骤4:
.then( function(msg){
console.log( msg ); // 42
} );
//在第二步出错,在第三步的Promise的rejected函数捕获到异常,异常返回42,在第四步的Promise调用默认拒绝函数,返回42

错误处理

异步处理的方式:

  • 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
    11
    var 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
    9
    1.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
    19
    var 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 国际许可协议。

本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。