JS的异步与程序性能相关问题

2024-01-09 11:32:48

1、现在与将来

1.1、分块的程序

可以把 JavaScript 程序写在单个 .js 文件中,但是这个程序几乎一定是由多个块构成的。这些块中只有一个是现在执行,其余的则会在将来执行。最常见的块单位是函数

从现在到将来的“等待”,最简单的方法(但绝对不是唯一的,甚至也不是最好的!)是使用一个通常称为回调函数的函数

// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", function myCallbackFunction(data){
	console.log( data ); // 耶!这里得到了一些数据!
} );

任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax 响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步机制

异步控制台

在某些条件下,某些浏览器的 console.log(…) 并不会把传入的内容立即输出。出现这种情况的主要原因是,在许多程序(不只是 JavaScript)中,I/O 是非常低速的阻塞部分。所以,(从页面 /UI 的角度来说)浏览器在后台异步处理控制台 I/O 能够提高性能,这时用户甚至可能根本意识不到其发生

1.2、事件循环

直到ES6,JS才真正内建有直接的异步概念。

JavaScript 引擎并不是独立运行的,它运行在宿主环境中,通常就是Web浏览器,实际上,JS现如今已经通过Node.js 这样的工具进入服务器领域,也嵌入到了从机器人到电灯泡等各种各样的设备中。但是,所有这些环境都有一个共同“点”(thread,也指线程。不论真假与否,这都不算一个很精妙的异步笑话),即它们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用 JavaScript 引擎,这种机制被称为事件循环

1.3、并行线程

术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。记住,异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。

并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。

与之相对的是,事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。

在单线程环境中,线程队列中的这些项目是底层运算确实是无所谓的,因为线程本身不会被中断。但如果是在并行系统中,同一个程序中可能有两个不同的线程在运转,这时很可能就会得到不确定的结果。

JavaScript 从不跨线程共享数据

完整运行

var a = 20;
function foo() {
	a = a + 1;
}
function bar() {
	a = a * 2;
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

由于 JavaScript 的单线程特性,foo()(以及 bar())中的代码具有原子性。也就是说,一旦 foo() 开始运行,它的所有代码都会在 bar() 中的任意代码运行之前完成,或者相反。这称为完整运行特性

上,如果 foo()bar() 中的代码更长,完整运行的语义就会更加清晰,比如:
var a = 1;
var b = 2;
function foo() {
	a++;
	b = b * a;
	a = b + 3;
}
function bar() {
	b--;
	a = 8 + b;
	b = a * 2;
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

由于 foo() 不会被 bar() 中断,bar() 也不会被 foo() 中断,所以这个程序只有两个可能的输出,取决于这两个函数哪个先运行——如果存在多线程,且 foo() 和 bar() 中的语句可以交替运行的话,可能输出的数目将会增加不少

块 1 是同步的(现在运行),而块 2 和块 3 是异步的(将来运行),也就是说,它们的运行在时间上是分隔的

// 块 1:
var a = 1;
var b = 2;
// 块 2(foo()):
a++;
b = b * a;
a = b + 3;
// 块 3(bar()):
b--;
a = 8 + b;
b = a * 2;

同一段代码有两个可能输出意味着还是存在不确定性!但是,这种不确定性是在函数(事件)顺序级别上,而不是多线程情况下的语句顺序级别(或者说,表达式运算顺序级别)。换句话说,这一确定性要高于多线程情况在JS的特性中,这种函数顺序的不确定性就是通常所说的竞态条件,foo() 和 bar() 相互竞争,看谁先运行

1.4、并发

两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作“进程”级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对

两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在
独立的处理器或处理器核心上同时运行)。可以把并发看作“进程”级(或者任务级)的
并行,与运算级的并行(不同处理器上的线程)相对

单线程事件循环是并发的一种形式

非交互

两个或多个“进程”在同一个程序内并发地交替运行它们的步骤 / 事件时,如果这些任务彼此不相关,就不一定需要交互

var res = {};
function foo(results) {
	res.foo = results;
}
function bar(results) {
	res.bar = results;
}
// ajax(..)是某个库提供的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

foo() 和 bar() 是两个并发执行的“进程”,按照什么顺序执行是不确定的。但是,我们构建程序的方式使得无论按哪种顺序执行都无所谓,因为它们是独立运行的,不会相互影响

交互

常见的情况是,并发的“进程”需要相互交流,通过作用域或 DOM 间接交互。

1.5、任务

在 ES6 中,有一个新的概念建立在事件循环队列之上,叫作任务队列,带来的最大影响可能是 Promise 的异步特性

我认为对于任务队列最好的理解方式就是,它是挂在事件循环队列的每个 tick 之后的一个队列。在事件循环的每个 tick 中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目(一个任务)

1.6、语句顺序

代码中语句的顺序和 JavaScript 引擎执行语句的顺序并不一定要一致。

1.7、总结

实际上,JS程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。

一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个tick。用户交互、IO 和定时器会向事件队列中加入事件。

任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一个或多个后续事件。

并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)。

通常需要对这些并发执行的“进程”(有别于操作系统中的进程概念)进行某种形式的交互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身分割为更小的块,以便其他“进程”插入进来。

2、回调

回调函数包裹或者说封装了程序的延续(continuation)

我们的大脑可以看作类似于单线程运行的事件循环队列,就像 JavaScript 引擎那样

2.1、嵌套回调与链式回调

嵌套回调

listen("click", function handler(evt) {
    setTimeout(function request() {
        ajax("http://some.url.1", function response(text) {
            if (text == "hello") {
                handler();
            }
            else if (text == "world") {
                request();
            }
        });
    }, 500);
});

这里我们得到了三个函数嵌套在一起构成的链,其中每个函数代表异步序列(任务,“进程”)中的一个步骤。这种代码常常被称为回调地狱,有时也被称为毁灭金字塔

2.2、总结

回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。

第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码是坏代码,会导致坏 bug。

我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。

第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。这种控制转移导致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。

可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会被发现。

我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可以复用,且没有重复代码的开销。

我们需要比回调更好的机制。到目前为止,回调提供了很好的服务,但是未来的 JavaScript需要更高级、功能更强大的异步模式。本书接下来的几章会深入探讨这些新型技术。

3、Promise

通过回调表达程序异步和管理并发的两个主要缺陷:缺乏顺序性和可信任性。

我们不把自己程序的continuation 传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么,这种范式就称为 Promise

Promise举例

设想一下这样一个场景:我走到快餐店的柜台,点了一个芝士汉堡。我交给收银员 1.47 美元。通过下订单并付款,我已经发出了一个对某个值(就是那个汉堡)的请求。我已经启动了一次交易。
但是,通常我不能马上就得到这个汉堡。收银员会交给我某个东西来代替汉堡:一张带有订单号的收据。订单号就是一个 IOU(I owe you,我欠你的)承诺(promise),保证了最终我会得到我的汉堡。
所以我得好好保留我的收据和订单号。我知道这代表了我未来的汉堡,所以不需要担心,只是现在我还是很饿!
在等待的过程中,我可以做点其他的事情,比如给朋友发个短信:“嗨,要来和我一起吃午饭吗?我正要吃芝士汉堡。”
我已经在想着未来的芝士汉堡了,尽管现在我还没有拿到手。我的大脑之所以可以这么做,是因为它已经把订单号当作芝士汉堡的占位符了。从本质上讲,这个占位符使得这个值不再依赖时间。这是一个未来值。
终于,我听到服务员在喊“订单 113”,然后愉快地拿着收据走到柜台,把收据交给收银员,换来了我的芝士汉堡。换句话说,一旦我需要的值准备好了,我就用我的承诺值(value-promise)换取这个值本身。
但是,还可能有另一种结果。他们叫到了我的订单号,但当我过去拿芝士汉堡的时候,收银员满是歉意地告诉我:“不好意思,芝士汉堡卖完了。”除了作为顾客对这种情况感到愤怒之外,我们还可以看到未来值的一个重要特性:它可能成功,也可能失败。
每次点芝士汉堡,我都知道最终要么得到一个芝士汉堡,要么得到一个汉堡包售罄的坏消息,那我就得找点别的当午饭了。

Promise值

就像芝士汉堡订单一样,Promise 的决议结果可能是拒绝而不是完成。拒绝值和完成的 Promise 不一样:完成值总是编程给出的,而拒绝值,通常称为拒绝原因(rejection reason),可能是程序逻辑直接设置的,也可能是从运行异常隐式得出的值

add( fetchX(), fetchY() )
    .then(
    // 完成处理函数
    function(sum) {
        console.log( sum );
    },
    // 拒绝处理函数
    function(err) {
        console.error( err ); // 烦!
    }
);

一旦 Promise 决议,它就永远保持在这个状态。此时它就成为了不变值(immutable value),可以根据需求多次查看。

Promise 是一种封装和组合未来值的易于复用的机制。

3.1、事件

单独的 Promise 展示了未来值的特性。但是,也可以从另外一个角度看待Promise 的决议:一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的 this-then-that

function foo(x) {
    // 开始做点可能耗时的工作
    // 构造一个listener事件通知处理对象来返回
    return listener;
}
var evt = foo(42);
evt.on("completion", function () {
	// 可以进行下一步了!
});
evt.on("failure", function (err) {
	// 啊,foo(..)中出错了
});

Promise事件

function foo(x) {
    // 可是做一些可能耗时的工作
    // 构造并返回一个promise
    return new Promise( function(resolve,reject){
		// 最终调用resolve(..)或者reject(..)
		// 这是这个promise的决议回调
	});
}
var p = foo( 42 );

Promise 决议并不一定要像前面将 Promise 作为未来值查看时一样会涉及发送消息。它也可以只作为一种流程控制信号

function bar() {
	// foo(..)肯定已经完成,所以执行bar(..)的任务
}
function oopsBar() {
	// 啊,foo(..)中出错了,所以bar(..)没有运行
}
// 对于baz()和oopsBaz()也是一样
var p = foo( 42 );
p.then( bar, oopsBar );
p.then( baz, oopsBaz );

3.2、具有then方法的鸭子类型

在 Promise 领域,一个重要的细节是如何确定某个值是不是真正的 Promise。或者更直接地说,它是不是一个行为方式类似于 Promise 的值?

既然 Promise 是通过 new Promise(…) 语法创建的,那你可能就认为可以通过 p instanceofPromise 来检查。但遗憾的是,这并不足以作为检查方法,原因有许多

其中最主要的是,Promise 值可能是从其他浏览器窗口(iframe 等)接收到的。这个浏览器窗口自己的 Promise 可能和当前窗口 /frame 的不同,因此这样的检查无法识别 Promise实例。

还有,库或框架可能会选择实现自己的 Promise,而不是使用原生 ES6 Promise 实现

因此,识别 Promise(或者行为类似于 Promise 的东西)就是定义某种称为 thenable 的东西,将其定义为任何具有 then(…) 方法的对象和函数。我们认为,任何这样的值就是Promise 一致的 thenable。

根据一个值的形态(具有哪些属性)对这个值的类型做出一些假定。这种类型检查(type check)一般用术语鸭子类型(duck typing)来表示

if (
    p !== null &&
    (
        typeof p === "object" ||
        typeof p === "function"
    ) &&
    typeof p.then === "function"
) {
    // 假定这是一个thenable!
}
else {
    // 不是thenable
}

3.3、Promise信任问题

把一个回调传入工具 foo(…) 时可能出现如下问题:

  1. 调用回调过早;
  2. 调用回调过晚(或不被调用);
  3. 调用回调次数过少或过多;
  4. 未能传递所需的环境和参数;
  5. 吞掉可能出现的错误和异常;
3.3.1、调用过早

在这类问题中,一个任务有时同步完成,有时异步完成,这可能会导致竞态条件。

根据定义,Promise 就不必担心这种问题,因为即使是立即完成的 Promise(类似于 newPromise(function(resolve){ resolve(42); }))也无法被同步观察到。也就是说,对一个 Promise 调用 then(…) 的时候,即使这个 Promise 已经决议,提供给then(…) 的回调也总会被异步调用

3.3.2、调用过晚

和前面一点类似,Promise 创建对象调用 resolve(…) 或 reject(…) 时,这个 Promise 的then(…) 注册的观察回调就会被自动调度。

同步查看是不可能的,所以一个同步任务链无法以这种方式运行来实现按照预期有效延迟另一个回调的发生。也就是说,一个 Promise 决议后,这个 Promise 上所有的通过then(…) 注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。

p.then(function () {
    p.then(function () {
        console.log("C");
    });
    console.log("A");
});
p.then(function () {
    console.log("B");
});
// A B C

这里,“C” 无法打断或抢占 “B”,这是因为 Promise 的运作方式。

Promise调度技巧

但是,还有很重要的一点需要指出,有很多调度的细微差别。在这种情况下,两个独立Promise 上链接的回调的相对顺序无法可靠预测。如果两个 promise p1 和 p2 都已经决议,那么 p1.then(…); p2.then(…) 应该最终会先调用p1 的回调,然后是 p2 的那些。但还有一些微妙的场景可能不是这样的,比如以下代码

var p3 = new Promise( function(resolve,reject){
	resolve( "B" );
} );
var p1 = new Promise( function(resolve,reject){
	resolve( p3 );
} );
p2 = new Promise( function(resolve,reject){
	resolve( "A" );
} );
p1.then( function(v){
	console.log( v );
} );
p2.then( function(v){
	console.log( v );
} );
// A B

p1 不是用立即值而是用另一个 promise p3 决议,后者本身决议为值 “B”。规定的行为是把 p3 展开到 p1,但是是异步地展开。所以,在异步任务队列中,p1 的回调排在 p2 的回调之后

3.3.3、回调未调用

没有任何东西(甚至 JavaScript 错误)能阻止 Promise 向你通知它的决议(如果它决议了的话)。如果你对一个 Promise 注册了一个完成回调和一个拒绝回调,那么 Promise在决议时总是会调用其中的一个。

但是,如果 Promise 本身永远不被决议呢?即使这样,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来了解是哪种情况
    }
);
3.3.4、调用次数过少或过多

Promise 的定义方式使得它只能被决议一次。如果出于某种原因,Promise 创建代码试图调用 resolve(…) 或 reject(…) 多次,或者试图两者都调用,那么这个 Promise 将只会接受第一次决议,并默默地忽略任何后续调用。

由于 Promise 只能被决议一次,所以任何通过 then(…) 注册的(每个)回调就只会被调用一次。

如果你把同一个回调注册了不止一次(比如 p.then(f); p.then(f);),那它被调用的次数就会和注册次数相同。

3.3.5、未能传递参数 / 环境值

Promise 至多只能有一个决议值(完成或拒绝)

如果你没有用任何值显式决议,那么这个值就是 undefined,这是 JavaScript 常见的处理方式。但不管这个值是什么,无论当前或未来,它都会被传给所有注册的(且适当的完成或拒绝)回调。

还有一点需要清楚:如果使用多个参数调用 resovle(…) 或者 reject(…),第一个参数之后的所有参数都会被默默忽略。因为这是对 Promise 机制的无效使用。

如果要传递多个值,你就必须要把它们封装在单个值中传递,比如通过一个数组或对象。

对环境来说,JavaScript 中的函数总是保持其定义所在的作用域的闭包,所以它们当然可以继续访问你提供的环境状态。

3.3.6、吞掉错误或异常

基本上,这部分是上个要点的再次说明。如果拒绝一个 Promise 并给出一个理由(也就是一个出错消息),这个值就会被传给拒绝回调。

如果在 Promise 的创建过程中或在查看其决议结果过程中的任何时间点上出现了一个 JavaScript 异常错误,比如一个TypeError或ReferenceError,那这个异常就会被捕捉,并且会使这个 Promise 被拒绝。

var p = new Promise(function (resolve, reject) {
    foo.bar(); // foo未定义,所以会出错!
    resolve(42); // 永远不会到达这里 :(
});
p.then(
    function fulfilled() {
        // 永远不会到达这里 :(
    },
    function rejected(err) {
        // err将会是一个TypeError异常对象来自foo.bar()这一行
    }
);

foo.bar() 中发生的 JavaScript 异常导致了 Promise 拒绝,你可以捕捉并对其作出响应。

Promise 甚至把 JavaScript 异常也变成了异步行为,进而极大降低了竞态条件出现的可能。

但是,如果 Promise 完成后在查看结果时(then(…) 注册的回调中)出现了 JavaScript 异常错误会怎样呢?

var p = new Promise(function (resolve, reject) {
    resolve(42);
});
p.then(
    function fulfilled(msg) {
        foo.bar();
        console.log(msg); // 永远不会到达这里 :(
    },
    function rejected(err) {
        // 永远也不会到达这里 :(
    }
);

等一下,这看起来像是 foo.bar() 产生的异常真的被吞掉了。别担心,实际上并不是这样。但是这里有一个深藏的问题,就是我们没有侦听到它。p.then(…) 调用本身返回了另外一个 promise,正是这个 promise 将会因 TypeError 异常而被拒绝。

3.3.7、Promise可信任吗

Promise 并没有完全摆脱回调。它们只是改变了传递回调的位置。我们并不是把回调传递给 foo(…),而是从 foo(…) 得到某个东西(外观上看是一个真正的Promise),然后把回调传给这个东西。

如果向 Promise.resolve(…) 传递一个非 Promise、非 thenable 的立即值,就会得到一个用这个值填充的 promise。

var p1 = new Promise( function(resolve,reject){
	resolve( 42 );
} );
var p2 = Promise.resolve( 42 );
// 如果向 Promise.resolve(..) 传递一个真正的 Promise,就只会返回同一个 promise
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2 // true

更重要的是,如果向 Promise.resolve(…) 传递了一个非 Promise 的 thenable 值,前者就会试图展开这个值,而且展开过程会持续到提取出一个具体的非类 Promise 的最终值

Promise.resolve(…) 可以接受任何 thenable,将其解封为它的非 thenable 值。从 Promise.resolve(…) 得到的是一个真正的 Promise,是一个可以信任的值。如果你传入的已经是真正的 Promise,那么你得到的就是它本身,所以通过 Promise.resolve(…) 过滤来获得可信任性完全没有坏处。

3.3.8、建立信任

可以用 JavaScript 编写异步代码而无需信任

Promise 这种模式通过可信任的语义把回调作为参数传递,使得这种行为更可靠更合理。通过把回调的控制反转反转回来,我们把控制权放在了一个可信任的系统(Promise)中,这种系统的设计目的就是为了使异步编码更清晰。

3.4、链式流

我们可以把多个 Promise 连接到一起以表示一系列异步步骤,这种方式可以实现的关键在于以下两个 Promise 固有行为特性:

  1. 每次你对 Promise 调用 then(…),它都会创建并返回一个新的 Promise,我们可以将其链接起来
  2. 不管从 then(…) 调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被链接 Promise(第一点中的)的完成

如果我们向封装的 promise 引入异步,一切都仍然会同样工作

var p = Promise.resolve(21);
p.then(function (v) {
  console.log(v); // 21
  // 创建一个promise并返回
  return new Promise(function (resolve, reject) {
    // 引入异步!
    setTimeout(function () {
      // 用值42填充
      resolve(v * 2);
    }, 1000);
  });
}).then(function (v) {
  // 在前一步中的100ms延迟之后运行
  console.log(v); // 42
});

来简单总结一下使链式流程控制可行的 Promise 固有特性:

  1. 调用 Promise 的 then(…) 会自动创建一个新的 Promise 从调用返回。
  2. 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise 就相应地决议。
  3. 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前 then(…) 返回的链接 Promise 的决议值

3.5、决议、完成、拒绝

对于术语决议(resolve)、完成(fulfill)和拒绝(reject),在更深入学习 Promise 之前,我们还有一些模糊之处需要澄清。先来研究一下构造器 Promise(…):

var p = new Promise( function(X,Y){
	// X()用于完成
	// Y()用于拒绝
} );

第一个通常用于标识 Promise 已经完成,第二个总是用于标识 Promise 被拒绝。追根究底,这只是你的用户代码和标识符名称,对引擎而言没有意义。

事实上,第二个参数名称很容易决定。几乎所有的文献都将其命名为 reject(…),因为这就是它真实的(也是唯一的!)工作。

但是,第一个参数就有一些模糊了,Promise 文献通常将其称为 resolve(…)。

3.6、错误处理

错误处理最自然的形式就是同步的 try…catch 结构。遗憾的是,它只能是同步的,无法用于异步代码模式

浏览器有一个特有的功能是我们的代码所没有的:它们可以跟踪并了解所有对象被丢弃以及被垃圾回收的时机。所以,浏览器可以追踪 Promise 对象。如果在它被垃圾回收的时候其中有拒绝,浏览器就能够确保这是一个真正的未捕获错误,进而可以确定应该将其报告到开发者终端。

3.7、Promise模式

1、Promise.all(…)

在异步序列中(Promise 链),任意时刻都只能有一个异步任务正在执行——步骤 2 只能在步骤 1 之后,步骤 3 只能在步骤 2 之后。但是,如果想要同时执行两个或更多步骤(也就是“并行执行”),要怎么实现呢?

在经典的编程术语中,门(gate)是这样一种机制要等待两个或更多并行 / 并发的任务都完成才能继续。它们的完成顺序并不重要,但是必须都要完成,门才能打开并让流程控制继续。

Promise.all() 是 JavaScript 中的一个方法,它接收一个可迭代的对象(例如数组)作为参数,并返回一个新的 Promise。这个 Promise 在传入的所有 Promise 都成功解决后才会解决,并且返回一个包含所有 Promise 解决值的数组。如果传入的任何一个 Promise 被拒绝,则整个 Promise.all() 会立即被拒绝,并返回被拒绝的 Promise。

const promise1 = Promise.resolve(10);
const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, 'Hello');
});
const promise3 = fetch('https://api.example.com/data');

Promise.all([promise1, promise2, promise3])
  .then(values => {
    console.log(values);
    // 在所有 Promise 都解决后,打印数组结果
  })
  .catch(error => {
    console.error(error);
    // 如果有任何一个 Promise 被拒绝,打印错误信息
  });
2、Promise.race(…)

尽管 Promise.all([ … ]) 协调多个并发 Promise 的运行,并假定所有 Promise 都需要完成,但有时候你会想只响应“第一个跨过终点线的 Promise”,而抛弃其他 Promise。这种模式传统上称为门闩,但在 Promise 中称为竞态

Promise.race([ … ]) 也接受单个数组参数。这个数组由一个或多个 Promise、thenable 或立即值组成。

var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.race( [p1,p2] )
.then( function(msg){
// p1或者p2将赢得这场竞赛
return request(
"http://some.url.3/?v=" + msg
);
} )
.then( function(msg){
console.log( msg );
} );

因为只有一个 promise 能够取胜,所以完成值是单个消息,而不是像对 Promise.all([ … ])那样的是一个数组。

超时竞赛

Promise.race( [
	foo(), // 启动foo()
	timeoutPromise( 3000 ) // 给它3秒钟
] )
.then(
	function(){
		// foo(..)按时完成!
	},
    function(err){
    	// 要么foo()被拒绝,要么只是没能够按时完成,
    	// 因此要查看err了解具体原因
    }
);

finally

一个关键问题是:“那些被丢弃或忽略的 promise 会发生什么呢?”我们并不是从性能的角度提出这个问题的——通常最终它们会被垃圾回收——而是从行为的角度(副作用等)

有些开发者提出,Promise 需要一个 finally(…) 回调注册,这个回调在 Promise 决议后总是会被调用,并且允许你执行任何必要的清理工作。

它看起来可能类似于:

var p = Promise.resolve( 42 );
p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );

3.8、Promise API

1、new Promise(…)构造器

有启示性的构造器 Promise(…) 必须和 new 一起使用,并且必须提供一个函数回调。这个函数接受两个函数回调,用以支持 promise 的决议。通常我们把这两个函数称为 resolve(…) 和 reject(…):

var p = new Promise( function(resolve,reject){
// resolve(..)用于决议/完成这个promise
// reject(..)用于拒绝这个promise
} );
2、Promise.resolve(…)和Promise.reject(…)

创 建 一 个 已 被 拒 绝 的 Promise 的 快 捷 方 式 是 使 用 Promise.reject(…), 所 以 以 下 两 个promise 是等价的:

var p1 = new Promise( function(resolve,reject){
	reject( "Oops" );
} );
var p2 = Promise.reject( "Oops" );

Promise.resolve(…) 常用于创建一个已完成的 Promise,使用方式与 Promise.reject(…)类似。但是,Promise.resolve(…) 也会展开 thenable 值(前面已多次介绍)。在这种情况下,返回的 Promise 采用传入的这个 thenable 的最终决议值,可能是完成,也可能是拒绝:

var fulfilledTh = {
	then: function(cb) { cb( 42 ); }
};
var rejectedTh = {
	then: function(cb,errCb) {
		errCb( "Oops" );
	}
};
var p1 = Promise.resolve( fulfilledTh );
var p2 = Promise.resolve( rejectedTh );
// p1是完成的promise
// p2是拒绝的promise

还要记住,如果传入的是真正的 Promise,Promise.resolve(…) 什么都不会做,只会直接把这个值返回。所以,对你不了解属性的值调用 Promise.resolve(…),如果它恰好是一个真正的 Promise,是不会有额外的开销的。

3、then(…)和catch(…)

每个 Promise 实例(不是 Promise API 命名空间)都有 then(…) 和 catch(…) 方法,通过这两个方法可以为这个 Promise 注册完成和拒绝处理函数。Promise 决议之后,立即会调用这两个处理函数之一,但不会两个都调用,而且总是异步调用

then(…) 接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。

then(…) 和 catch(…) 也会创建并返回一个新的promise,这个promise 可以用于实现Promise 链式流程控制。如果完成或拒绝回调中抛出异常,返回的 promise 是被拒绝的。如果任意一个回调返回非 Promise、非 thenable 的立即值,这个值会被用作返回 promise 的完成值。

4、Promise.all(…)和Promise.race(…)

Promise.all([ … ]) 和 Promise.race([ … ]) 都会创建一个 Promise 作为它们的返回值。这个 promise 的决议完全由传入的 promise 数组控制。

对 Promise.all([ … ]) 来说,只有传入的所有 promise 都完成,返回 promise 才能完成。如果有任何 promise 被拒绝,返回的主 promise 就立即会被拒绝(抛弃任何其他 promise 的结果)。如果完成的话,你会得到一个数组,其中包含传入的所有 promise 的完成值。对于拒绝的情况,你只会得到第一个拒绝 promise 的拒绝理由值。这种模式传统上被称为门:所有人都到齐了才开门。

对 Promise.race([ … ]) 来说,只有第一个决议的 promise(完成或拒绝)取胜,并且其决议结果成为返回 promise 的决议。这种模式传统上称为门闩:第一个到达者打开门闩通过。

var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( "Hello World" );
var p3 = Promise.reject( "Oops" );
Promise.race( [p1,p2,p3] )
.then( function(msg){
console.log( msg ); // 42
} );
Promise.all( [p1,p2,p3] )
.catch( function(err){
console.error( err ); // "Oops"
} );
Promise.all( [p1,p2] )
.then( function(msgs){
console.log( msgs ); // [42,"Hello World"]
} );

若向 Promise.all([ … ]) 传入空数组,它会立即完成,但 Promise.race([ … ]) 会挂住,且永远不会决议。

文章来源:https://blog.csdn.net/qq_60754128/article/details/135418872
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。