JavaScript异步编程(Promise教程和async、await原理)

2 篇文章 0 订阅
2 篇文章 0 订阅

异步

同步:是指一个进程在执行某个请求的时候,若这个请求没有执行完成,那么这个进程将会一直等待下去,直到这个请求执行完毕,才会继续执行下面的请求。
异步:是指一个进程在执行某个请求的时候,如果这个请求没有执行完毕,进程不会等待,而是继续执行下面的请求。
javascript程序写在许多个.js文件中,但是这些程序几乎一定是由多个块构成的。这些块中只有一个是现在执行,其余的则会在将来执行。因为javascript引擎是单线程的。
javascript虽然可以实现阻塞,但是一般不会这样做,因为这会锁定浏览器UI(按钮、菜单、滚动条等),并阻塞所有的用户交互。因为javascript引擎是单线程的。

异步控制台

console.*方法族并不是正式JavaScript的一部分,而是由宿主环境添加到JavaScript中的。
因此,不同浏览器中console.*方法可能会有不一样的调用结果。比如:某些浏览器的console.log()并不会把传入的内容立即输出。这是因为,在现实中I/O是非常低速的阻塞部分。所以,(从页面/UI的角度来说)浏览器在后台异步处理控制台I/O能够提高性能,这时可能根本想不到意外发生的原因。如果遇到这种情况最好是在JS调试器中使用断点,而不要依赖控制台输出。次优的方案是把对象序列化到一个字符串中,以强制执行一次“快照”,比如通过JSON.stringify()。

事件循环

JS引擎本身所做的只不过是在谁有需要的时候,执行某程序中的代码块,并没有时间的概念。JS引擎并不是独立运行的,它运行在宿主环境中,这些宿主环境都提供了一种机制来处理程序中多个块的执行方法,当需要执行某个块的时候把这个块放入待执行的队列中,交给JS引擎去执行,JS引擎只管执行队列中的任务就可以。这种机制被称为事件循环。
举例来说:如果你的JS程序发送一个Ajax请求,从服务器获取一些数据,同时你还在一个函数(通常是回调函数)中设置好响应代码,然后JS引擎会通知宿主环境:“我现在需要服务器返回的数据,你一旦完成刚才的Ajax请求,收到了这个数据,就请调用这个回调函数。”
然后浏览器会设置监听来自网络的响应,拿到服务器数据后,会把回调函数插入到事件循环队列中,等待JS引擎按顺序执行队列中的任务,以此实现对这个回调的调度执行。
JS引擎靠的是一个“永远”执行的while循环来遍历事件循环队列中的任务,获取任务后直接执行,执行完成后又取出一个新任务。循环的每一轮称为一个tick。对每个tick而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。
一定要清楚,setTimeout()并没有把你的回调函数挂在事件循环队列中。它所做的是在js引擎外面(宿主环境)设定一个定时器。当定时器到时后,宿主环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的tick会摘下并执行这个回调。如果这个时间事件循环队列中已经有20个项目了会怎样?你的回调就会等待,它得排在其他项目后面(通常没有抢占式的方式支持直接将其排到队首)。这也解释了为什么setTimeout()定时器的精度可能不高。
直到ES6,JavaScript才真正内建有直接的异步概念。为什么这么说呢?是因为现在ES6从本质上改变了在哪里管理事件循环。ES6精确指定了事件循环的工作细节,这意味着在技术上将其纳入JS引擎的势力范围,而不是只由宿主环境来管理。这个改变的主要原因是ES6中Promise的引入,这项技术要求对事件循环队列的调度运行能够直接进行精细控制。

并行线程

并行是关于能够同时发生的事情,异步是关于现在和将来的时间间隙。
如果JS是多线程的话,那异步和并行将会变成很复杂的东西,但是JS引擎是单线程,事件循环队列中的每一个任务只能有一个在JS引擎中执行,并且必须把这一个任务执行完毕后才能执行下一个任务。所以单从这一点看JS引擎并不能像多线程一样并行执行任务,即从这点上看,JS中无并行。
虽然每个任务会被JS引擎逐个执行,但是我们不能知道每个任务的执行顺序。如果共享同一数据的多个javascript事件进入循环事件队列中,我们将无法预测结果,比如:

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)

对于上面的代码,var a = 1;与var b = 2;两条语句是同步的(现在运行),而foo()与bar()是异步的(将来运行),也就是说,它们的运行在时间上是分隔开的。我们不能确定foo()与bar()在什么时候执行,如果foo()运行在bar()之前,a的结果是11,而如果bar()运行在foo()之前的话,a的结果就是183。
同一段代码有两个可能输出意味着还是存在不确定性!但是,这种不确定性是在函数(事件)顺序级别上,而不是多线程情况下的语句顺序级别(或者说,表达式运算顺序级别)。换句话说,这一确定性要高于多线程。
在JS的特性中,这种函数顺序的不确定性就是通常所说的竞态条件,foo()与bar()相互竞争同一资源,看谁先运行。具体来说,因为无法预测a与b的最终结果,所以才是竞态条件。

并发

并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。
并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。
单CPU的并发是这样的:多个事件(多个线程/进程)在不同的时间片交替在同一个CPU上运行
多CPU的并行是这样的:4个事件(4个线程/进程)分别在4个CPU上同时运行
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
从上面关于并行与并发的介绍来看,因为JS是用单线程处理多个事件,一次只能处理一个事件,事件与事件之间是异步的(不同时间段执行的),所以JS中的事件执行应该算是并发的一种。
举例来说:我们想在前端展示一个随时可能更新的列表,随着用户向下滚动列表而逐渐加载更多内容。要正确地实现这一特性,需要(至少)两个独立的任务同时运行(这里的同时并不是真正意义上的同时,而是在同一时间段内,并不需要在同一时刻,即并发的一种)。
第一个任务在用户向下滚动页面触发onscroll事件时,响应这些事件(先预留好位置,即UI渲染,然后发起Ajax请求要求更新其中的内容)。第二个任务是:接收Ajax的响应(把内容展示到对应的位置,即UI渲染)
我们模拟上述任务一种可能的情况:

onscroll,UI渲染,请求1
onscroll,UI渲染,请求2
响应1,UI渲染
onscroll,UI渲染,请求3
响应2,UI渲染
响应3,UI渲染
onscroll,UI渲染,请求4
onscroll,UI渲染,请求5
onscroll,UI渲染,请求6
响应4,UI渲染
onscroll,UI渲染,请求7
响应6,UI渲染
响应5,UI渲染
响应7,UI渲染

注意响应6和响应5返回乱序了,原因是:响应5来的比响应6慢。

交互

上面例子中的这种乱序可能会影响到结果,比如UI渲染错位置:6的结果渲染到5的位置。
这个时候可以通过判断Ajax返回值的ID来和前端位置进行对应(这得前端和后端进行共同确认返回值)。这种比较简单,就不写伪代码了。
但有些场景下,前端和后端可能会不做这种协调,这就总是会导致一些错误,如下:

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

在这个例子中,无论foo()和bar()哪个先被触发,总会使baz()过早运行(a或者b仍然处于未定义状态)。
解决这个问题的方法如下:

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)

如上包裹baz()调用的条件判断if(a && b)一般被称为门,虽然不确定a和b到达的顺序,但是会等它们两个都到再把门打开。
另一种可能遇到的并发交互条件有时称为竞态,但是更精确的叫法是门闩,它的特性可以描述为:“只有第一名取胜”。在这里,不确定性是可以接受的,因为它明确指出了这一点可以接受:需要“竞争”到终点,且只有唯一的胜利者。
例如下面代码:

var a;
function foo(x){
  if(!a){
    a = x * 2;
    baz();
  }
}
function bar(y){
  if(!a){
    a = x / 2;
    baz();
  }
}
function baz(){
  console.log(a);
}
//ajax()是某个库中提供的某个Ajax函数
ajax("http://some.url.1",foo)
ajax("http://some.url.2",bar)

条件判断if(!a)使得foo()和bar()只有第一个可以通过,第二个及其之后任何调用都会被忽略。也就是说:只执行最开始的一次,且不确定执行的是哪一个函数。

协作

还有一种并发合作方式,称为并发协作。这里的重点不再是通过共享作用域中的值进行交互(尽管可以这么做),这里的目标是把一个可能会长期运行的任务(或者函数)拆分为多个步骤或者多个批次,使得其他并发任务有机会将自己的运算插入到事件循环队列中与该长期任务交替运行。
举例来说:如果有一个需要遍历很长的结果列表进行值转换的Ajax响应处理函数,如下所示

var res=[];
//response()是用来从Ajax调用中取得结果数组
function response(data){
  res = res.concat(
    data.map(function(val){
      return val*2;
    })
  );
}
function foo(x){
  if(!a){
    a = x * 2;
    baz();
  }
}
//ajax()是某个库中提供的某个Ajax函数
ajax("http://some.url.1",response)
ajax("http://some.url.2",foo)

如果“http://some.url.1”接口先取得结果,那么整个列表会立即映射到res中。如果列表中只有几千条数据或更少,就没什么事。但是如果有1000万条数据的话,就可能运行相当长一段时间了,这时,页面的其他代码将不能运行,包括其他response()的调用或者UI刷新,甚至滚动、输入、按钮点击都没有反应。
所以要分批处理这些返回结果,如下:

var res=[];
//response()是用来从Ajax调用中取得结果数组
function response(data){
  //一次处理1000个
  var chunk=data.splice(0,1000)
  //添加到已有的res组
  res = res.concat(
    chunk.map(function(val){
      return val*2;
    })
  );
  //还有剩下的要处理吗?
  if(data.length>0){
    //异步调度下一次批处理
    setTimeout(function(){
      response(data);
    },0)
  }
}
function foo(x){
  if(!a){
    a = x * 2;
    baz();
  }
}
//ajax()是某个库中提供的某个Ajax函数
ajax("http://some.url.1",response)
ajax("http://some.url.2",foo)

但是,需要注意一点:setTimeout(…,0)并不直接把函数插入到事件循环队列。定时器会在有机会的时候插入事件。举例来说:两个连续的setTimeout(…,0)调用时,不能保证会严格按照调用顺序处理,所以各种情况都有可能出现,比如定时器漂移,在这种情况下,这些事件的顺序就不可预测。在Node.js中,类似的方法是process.nextTick()。这些方法虽然方便且性能也不错,但并没有直接的方法可以适应所有环境来确保异步事件的顺序。

任务

ES6中,有一个新的概念建立在事件循环队列之上,叫做任务队列。在这里有一个概念就行,之后会在讨论Promise的异步特性时,说清楚事件队列和任务队列是怎么协调的。
所以,现在最好的理解方式是:任务队列是挂在事件循环队列的每个tick之后的一个队列。(这里忘记tick是什么的同学,可以去前面事件循环中查看tick定义)换句话说:每个tick执行完成后,不会继续执行下一个tick,而是先执行任务队列中的任务,直到任务队列中的任务做完后,才继续执行下一个tick。

回调

写了却不调用,给别人调用的函数就是回调函数。比如写一个函数 A,传给另一个函数 B 调用,那么函数 A 就是回调函数。
比如写了一个函数foo,给ajax异步调用:

//foo是回调函数,交给第三方ajax()函数去执行
ajax("http://some.url.2",foo)

回调不仅仅只有异步回调,还有同步回调,只是在这里我们主要讨论异步回调。
同步回调是阻塞的,而异步回调不是阻塞的
比如array.forEach()方法就是同步回调:

var arr = [1, 3, 5, 13, 2];

function foo(item, index) {
  console.log(`数组第${index + 1}个元素是${item}`);
}
//foo是回调函数,交给第三方arr.forEach()函数去执行
arr.forEach(foo)

array.forEach()方法在面向对象语法中的this部分中的显式绑定中的API调用的“上下文”中有介绍,在面向过程语法中循环部分也有介绍。

回调BUG及其处理方法

回调是JS中最基础的异步模式,回调包括了Ajax请求与响应、setTimeout()调用,如果大量使用这些回调会使代码变得更加难以理解、追踪、调试和维护。
回调嵌套太多层的话(回调地狱),可能会导致无限循环BUG,或者无法预测执行顺序而导致的bug,这会很难维护,使代码的可读性差、扩展性差,需要开发者自己注意。
有时候ajax()不是你编写的代码,也不在你的直接控制下。多数情况下,它是某个第三方提供的工具。我们把这称为控制反转(inversion of control,即IOC),也就是把自己程序一部分的执行控制交给某个第三方。
在这里可以这样说:回调就是控制反转
但是,我们不能完全信任第三方的操作是安全的,所以需要在自己的代码中加上一些校验,以阻止一些无法预料的问题。
比如要这样定义一个加法运算函数:

function addNumbers(x,y){
  //首先确保输入的是数字
  x=Number(x);
  y=Number(y);
  //然后安全的对数字进行相加
  return x+y;
}
//在有校验的情况下调用回调函数
//ajax()是某个库中提供的某个Ajax函数
ajax("http://some.url.1",addNumbers)

有一些API为了更优雅地处理错误,提供了分离回调(传入两个函数,一个函数用于成功通知,一个函数用于出错通知):

function success(data){
  console.log(data);
}
function failure(err){
  console.error(err);
}
//ajax()是某个库中提供的某个Ajax函数
ajax("http://some.url.1",success,failure);

还有一种常见的回调模式叫做“error-first风格”(有时也称为“Node风格”,因为几乎所有Node.js API都采用这种风格),其中回调的第一个参数保留用作错误对象(如果有的话)。如果成功了,这个参数就会被清空/置假(后续的参数就是成功数据)。不过,如果产生了错误结果,那么第一个参数就会被置起/置真(通常就不会再传递其他结果):

function response(data){
  if (err){
    console.error(err);
  }else{
    console.log(data);
  }
}

//ajax()是某个库中提供的某个Ajax函数
ajax("http://some.url.1",response);

如果回调函数一直在运行,没有输出也没有停止,找不到bug,无限循环怎么办。可以构造一个设置超时的工具来取消事件。例如:

//fn待执行函数,dalay超时时间
function timeoutify(fn,dalay){...}//这里不展示细节

//本处使用“error-first风格”定义结果处理函数
function response(data){
  if (err){
    console.error(err);
  }else{
    console.log(data);
  }
}
//ajax()是某个库中提供的某个Ajax函数
ajax("http://some.url.1",timeoutify(response,500));

如果在某个关键任务完成之前调用回调怎么办?一个有效避免这种情况的方法是:用前一个任务去回调后一个任务,这样就可以预测回调任务的执行顺序了。

Promise

未来值

先假设一个场景:我们需要计算x+y,但是x和y的值现在还没准备好,我们希望等待到它们都准备好时再进行相加。现在我们看看回调是如何实现这个功能的:

// getX(fn)与getY(fn)是一个ajax请求获取值的函数(异步函数),
// 或者是一个单纯的获取值的函数(同步函数),总之获取到值后给fn进行处理
function add(getX,getY){
  var x, y;
  getX(function(x_value){
    x=x_value;
    //两个都准备好了吗?
    if(y!=undefined){
      console.log(x+y);//开始求和并输出
    }
  });
  getY(function(y_value){
    y=y_value;
    //两个都准备好了吗?
    if(x!=undefined){
      console.log(x+y);//开始求和并输出
    }
  });
}
// fetchX(fn)与fetchY(fn)是同步或者异步函数,就是上面说的getX(fn)与getY(fn)
add(fetchX,fetchY);

先介绍一个东西:promise.then(onCompleted, onRejected);
意思是:就是当.then()前的方法执行完后再执行then()内部的程序,这样就避免了,数据没获取到等的问题。其中第一个参数onCompleted是待执行的函数,第二个参数onRejected是错误处理函数
现在将使用Promise函数来表达这个例子:

function add(xPromise, yPromise) {
  // Promise.all([..])接受一个promise数组并返回一个新的promise,
  // 这个新promise等待数组中的所有promise决议
  return Promise.all([xPromise, yPromise])//这个promise决议后,我们取得收到的x与y值,并加在一起
    .then(function (values) {
      //values是来自于之前完成的promise的消息数组
      return values[0] + values[1];
    });
}
// 这里的fetchX()和fetchY()是直接函数调用,返回两个Promise对象,分别是xPromise, yPromise(可能决议也可能还在等待),传入add函数中
add(fetchX(), fetchY())
  //我们得到一个这两个数组的和的promise
  //现在链式调用then()来等待返回promise的结果
  .then(function (sum) {
    console.log(sum);//这更简单
  });

上面的代码中有两层Promise。
fetchX()和fetchY()是直接调用的而不是传入一个函数,fetchX()和fetchY()的返回值(promise对象)被传给add()。这些promise代表的底层值的可用时间可能是现在或将来,但不管怎样,promise归一保证了行为的一致性。我们可以按照不依赖时间的方式追踪值x和y。它们是未来值。
第二层是add()(通过Promise.all([]))创建并返回的Promise。我们通过调用.then()等待这个promise。add()运算完成后,未来值sum就准备好了,可以打印出来。我们把等待未来值X和Y的逻辑隐藏在了add()内部
在add()内部,Promise.all([])调用创建了一个promise(这个promise等待xPromise和yPromise的值就位)。链式调用.then()创建了另外一个promise。这个promise由 return values[0] + values[1];立即执行得到运算结果。因此,链add()调用终止处的调用then()——在代码结尾处——实际上操作的是返回的第二个promise,而不是由Promise.all([])创建的第一个promise。还有,尽管第二个then()后面没有链接任何东西,但它实际上也创建了一个新的promise,如果想要观察或者使用它的话就可以看到。
就像在KFC买汉堡一样,先给你订单再给你汉堡,没有汉堡也会拒绝你。Promise的结果也可能是拒绝而不是完成。拒绝值和完成的Promise不一样:完成值总是编程给出的,而拒绝值,通常被称为拒绝原因,可能是程序逻辑直接设置的,也可能是从运行异常隐式得出的值。比如:

function add(xPromise, yPromise) {
  // Promise.all([..])接受一个promise数组并返回一个新的promise,
  // 这个新promise等待数组中的所有promise完成
  return Promise.all([xPromise, yPromise])//这个promise完成后,我们取得收到的x与y值,并加在一起
    .then(function (values) {
      //values是来自于之前完成的promise的消息数组
      return values[0] + values[1];
    });
}
// 这里的fetchX()和fetchY()是直接函数调用,返回两个Promise对象,分别是xPromise, yPromise,传入add函数中
add(fetchX(),fetchY())
  .then(
	  //完成处理函数
	  function(sum){
	    console.log(sum);
	  },
	  //拒绝处理函数
	  function(err){
	    console.error(err);//输出异常
	  }
	);

如果在获取X或Y的过程中出错,或者在加法过程中出错,add()返回的值就是一个被拒绝的promise,这个被拒绝的promise会自动传递给.then()的第二个错误处理回调函数。
Promise的值一旦决定,便永远不会改变了。

用Promise来完成一连串的事件

Promise可以当为一个未来值,也可以当作是一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的this-then-that。
假设某个场景要调用一个函数ajax()执行某个任务。我们不关心它的任何细节。只知道它可能现在立即完成,也可能需要一段时间完成。我们要知道ajax()什么时候完成,这时候就可以用回调,即:ajax()完成后,JS环境把某个函数放入事件循环队列,等待JS调用。(这里我们知道ajax()回调的工作流程,就不举例了)
还可以用观察者模式解决这个问题:侦听某个通知,你可能就会想到JS中常用的事件机制,即ajax()返回一个事件对象,并用多个函数去监控这个事件对象,当事件完成时,事件对象会发起通知,然后这些监控该事件对象的函数会收到通知,根据通知做不同的反应。例如下面的伪代码:

//下面的都是伪代码
function ajax(x){
  //开始执行耗时异步任务
  //构造一个listener事件对象,用来存储异步任务结果,是个未来值
  //返回这个listener事件对象
  return listener;
}
//用一个变量evt引用这个事件对象
var evt=ajax(42);

//让代码中其他函数订阅该事件对象
evt.on("completion",function foo(){
  //这个函数订阅了evt事件对象,
  //当事件对象中的任务完成时,会把任务的最终结果通知给这个函数(就是调用这个函数啦)
  //这个函数查看结果,如果结果是"completion"(成功啦),则执行这个函数中的内容,如果不是,则什么都不做。
})
//让代码中其他函数订阅该事件对象
evt.on("failure",function get_error(err){
  //这个函数订阅了evt事件对象,
  //当事件对象中的任务完成时,会把任务的最终结果通知给这个函数(就是调用这个函数啦)
  //这个函数查看结果,如果结果是"failure"(出错啦),则执行这个函数中的内容,如果不是,则什么都不做。
})
//从上可以看出,evt很像一个中介,把任务成功或失败的信息广播给订阅了evt的人,好让这些人做下一步行动

与回调进行对比,这么做的好处是:用回调的话,一个ajax()执行的任务,只能由一个函数来处理,这一个函数要处理很多东西,导致代码耦合度很高。而观察者模式可以把一个ajax()生成的事件对象分给代码中多个独立的部分观察,每个观察者都是独立的互不影响,即:可以把代码功能拆分开。
在介绍完观察者模式代替回调后,我们可以发现回调和观察者模式都能实现异步调用,同时可以控制事件发生的顺序,但是观察者模式可读性更好,耦合性更低。到这里,大家应该可以猜到了,观察者模式中的事件对象evt就是Promise对象的一个模拟。
实际上:我们通过then()方法在Promise对象上进行注册,注册一个“fulfillment事件”和/或“rejection事件”。
下面我们用Promise来进一步完善上面的观察者模式伪代码:

// 下面的内容还是伪代码
function ajax(x){
   //构造并返回一个promise
   return new Promise(function(resolve,reject){
     //开始先做一些可能耗时的工作
     //最后会调用resolve()或者reject()
   });
 }
function bar(){
  //ajax()成功完成后才执行的函数
}
function oopsBar(){
  //ajax()出错了才执行的函数
}
function baz(){
  //ajax()成功完成后才执行的函数
}
function oopsBaz(){
  //ajax()出错了才执行的函数
}
var p = ajax(42);
p.then(bar,oopsBar);//给Promise对象注册一个bar函数
p.then(baz,oopsBaz);//给Promise对象注册一个bar函数

new Promise(function(){})模式通常称为revealing constructor。传入的函数会立刻执行(这是一个同步回调例子,而不是异步回调,它不会有延迟)。
这里并没有把promise传递给bar()和baz(),而是使用promise控制bar()和baz()何时执行,这就是一种控制反转。
我们可以看看把promise传递给bar()和baz()的处理方式,如下伪代码:

// 下面的内容还是伪代码
function ajax(x) {
  //开始先做一些可能耗时的工作
  //构造并返回一个promise
  return new Promise(function (resolve, reject) {
    //开始先做一些可能耗时的工作
    //最后会调用resolve()或者reject()
  });
}
function bar(fooPromise) {
  fooPromise.then(
    function () {
      //ajax()成功完成后才执行的函数
    }, function () {
      //ajax()出错了才执行的函数
    }
  );
}
function baz(fooPromise) {
  fooPromise.then(
    function () {
      //ajax()成功完成后才执行的函数
    }, function () {
      //ajax()出错了才执行的函数
    }
  );
}

var p = ajax(42);
bar(p);
baz(p);

不论是把promise传递给bar()和baz(),还是使用promise控制bar()和baz()何时执行,都可以获得同样的结果,只是两种方法适用于不同的情况,大家只需在适合的时候使用对应方法即可。
另外,promise p这个对象可以多次调用then()方法,因为promise一旦完成,其值是不会发生改变的,可以按需查看promise的值。即:new Promise(function(){})创建promise对象后,即开始执行其中的函数,而且只执行一次,后面再也不会调用这个函数,而创建的promise对象只是用来查看函数调用结果的东西而已。

Promise对比普通回调的优点

回顾一下之前回调导致的一些不信任问题,因为回调是把函数交给第三方库去执行,所以叫回调,但第三方库什么时候调用这个回调函数,调用几次这个回调函数,我们是不知道的:

1、调用回调过早

//你知道下面这个到底输出0还是1吗?这是无法预测的!因为ajax是第三方的库,你不知道它什么时候调用
//即:你不可能保证第三方库一定是异步调用回调的,它万一在某种情况下会同步调用回调怎么办?
function result(data){
  console.log(a);
}
var a=0;
ajax("https://pre.url",result);//如果ajax在a++之前执行了result,则输出0,否则输出1
a++;

如上代码所示,一个事件有时是同步完成的,有时候是异步完成的,这可能会导致竞态条件。
而使用promise则不需要担心这个问题,因为我们可以用promise控制代码执行顺序,同时promise中的then()方法一定是异步的
New Promise(function (resolve, reject) {});这段语句是同步调用的,New Promise中的函数会立即执行,Promise中的函数的两个参数分别是resolve和reject,它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
参考前面的异步的任务章节,我们可以发现,promise是基于任务的(参考前面的异步的任务章节),所以即使一个promise对象中的任务已经完成,提供给then()方法的回调函数也总是异步调用的。

2、调用回调过晚(包含:执行顺序预测)

console.log("A");
setTimeout(function(){
  console.log("B");
},0);//这是个异步回调
// 现在假设schedule(function(){})是同步回调,即:阻塞的
schedule(function(){
  console.log("C");
	schedule(function(){
    console.log("D");
  });
});
//这里会发现,即使我们希望setTimeout中的函数立即执行,
// 即:setTimeout等待时间为0,它也会在最后执行
//最后输出的结果是:A,C,D,B
//显然setTimeout中的函数调用太晚了!!
//如果schedule是一个无限循环,你会发现setTimeout中的函数不会被调用,参考前面的异步的任务章节

// 还有一种情况与上面的相区别:
ajax1("http://url1",baz);
ajax2("http://url2",foo);
// 假设一个场景:我希望ajax1()和ajax2()都是异步回调,
// 但是我希望ajax1()先执行,然后再执行ajax2()
// 实际情况却是:ajax1()一直在等待后端给的数据,但一直收不到数据,卡了一分钟。
// 而ajax2()却在一秒内收到了后端的数据,这会导致ajax2()中的回调函数直接进入事件循环队列
// 最终ajax1()在ajax2()之后执行。这并不是我们想要的结果。

实际情况中,我们使用第三方的函数去回调一些东西都有可能会出现这种无法预测执行顺序的问题,因为我们无法知道第三方库是同步回调还是异步回调。假设刚才的schedule(function(){})是异步回调,那输出顺序会变为:A,B,C,D。解决这个问题的一个好方法其实就是把一切回调都变成异步的,拒绝同步回调。Promise机制就是这么做的。
和前面的 调用回调过早 类似,一个Promise创建的对象调用resolve()或者reject()时,这个Promise的then()注册的回调函数就会被自动执行,不会出现调用太晚的情况。这些回调任务一定会在上一个任务完成时立即执行。
也就是说,一个Promise决议后,这个Promise上所有通过then()注册的回调函数都会在下一个异步时机点上依次被调用。一个Promise上的任意一个回调都不会出现影响或延误其他Promise上回调的调用,他们会按主函数调用的顺序执行。
比如:

p=new Promise(function (resolve, reject) {
  resolve()
})
p.then(function A(){
  p.then(function C(){
    console.log("C");
  })
  console.log("A");
})
p.then(function B(){
  console.log("B");
})
for(let i=0;i<10;i++){
  p.then(()=>{
    console.log(i);
  })
}
//输出顺序为:A B 0,1,2,3,4,5,6,7,8,9 C
//解析:p.then只是注册一个函数进promise中等待异步调用,具体注册
//第一步:函数A注册到了promise中,同时返回一个新的Promise
//第二步:函数B注册到了promise中,同时返回一个新的Promise
// 第三步:一个循环中的所有箭头函数注册到了promise中,同时返回一个新的Promise
// 第四步:按照刚才的注册顺序,依次执行刚才promise中注册的函数。
// 第五步:函数A被执行,把函数C注册到了promise中,同时返回一个新的Promise
// 第六步:函数B被执行,箭头函数被执行,最后函数C被执行
Promise的执行顺序预测

这是一个重要的Promise特点:
两个不同的Promise对象上注册的回调函数执行的相对顺序无法正确预测。比如:

 var p3 = new Promise(function (resolve, reject) {
  resolve("B")
})
 var p1 = new Promise(function (resolve, reject) {
  resolve(p3)
})//这里p1依赖了p3
 var p2 = new Promise(function (resolve, reject) {
  resolve("A")
})
p1.then(function B(v){
  console.log(v);
})
p2.then(function A(v){
  console.log(v);
})
//实际输出:A,B

主观上,我们认为p1先注册了函数B(),直觉上就认为程序会先输出B,后输出A。实则不然,实际上先输出A,后输出B,原因是引擎会自动把p3的结果展开到p1,但是这个展开是异步的宏任务,具体原因之后会讲,这里我们只需要知道:不要去预测两个不同的Promise对象上注册的回调函数执行的相对顺序。
但也可以有正确预测的例子,比如:

let p1=Promise.resolve("hh")
let p2=Promise.resolve()

p1.then(function(){
  console.log("promise1");
  setTimeout(()=>{console.log("setTimeout1被执行")},0)
})
console.log("llz")
setTimeout(()=>{
  console.log("setTimeout2被执行")
  p2.then(function(){
    console.log("promise2");
  })
})

//输出内容的顺序:
// llz
// promise1
// setTimeout2被执行
// promise2
// setTimeout1被执行

3、回调未调用(promise.race([…]))

这个问题很常见,比如:Ajax请求的后台程序卡住了,导致前端没有接到信号,也就无法执行后续的回调。或者第三方库Ajax的内部出现了JavaScript语法错误,导致整个Ajax请求失败,从而没有执行后续的回调函数。
Promise有几个解决这个问题的方法,首先,Promise的决议不会被任何方式阻止(甚至是JavaScript错误,比如:如果Promise在完成决议之前其中的代码就出现JavaScript语法错误,那么这个Promise会将决议结果变为拒绝)。如果用then()方法给Promise注册了一个完成回调和一个拒绝回调,那么Promise决议完成时总是会调用完成回调与拒绝其中的一个回调函数。即:只要Promise决议完成,则后续的回调一定会被调用。
但是如果Promise永远不会被决议怎么办?比如:

let p1=new Promise((resolve, reject)=>{
  resolve('第一步 成功啦');
})
let p2=p1.then((data)=>{
  //显示第一步的值
  console.log(data);//输出:第一步 成功啦
  console.log("第二步开始执行");
  //返回一个永远不会被决议的Promise对象,或者返回一个永远不会执行参数的thenable(后面鸭子类型有说明和例子)
  return new Promise(()=>{})
},(err)=>{
  console.log(err,"第一步 失败啦");
});
//后面的代码将永远不会执行,看起来就像Promise中断了一样,导致回调未被调用
p2.then((data)=>{
  //下面的代码不会执行
  console.log(data);
  console.log("第三步开始执行");
  return "第三步成功"
},(err)=>{
  //下面的代码不会执行
  console.log(err,"第二步 失败啦");
})

我们可以看到上面的例子中,显然有回调未执行的情况。Promise对此也提供了解决方案:promise.race

promise.race([…])

promise.race([…])是一种叫竞态的高级抽象机制,即:用一个定时回调任务与原始任务进行竞争,谁先完成就返回谁的结果:

//自定义一个超时工具,delay为规定的超时时间,单位毫秒
function timeoutPromise(delay){
  return new Promise(function(resolve,reject){
    setTimeout(function(){
      reject("Timeout!");
    },delay);
  });
}
function ajax(){
  return new Promise((resolve,reject)=>{
    //调用一个ajax请求
    ajax(function(data){
      //饭回data结果
      resolve(data);
    });
  })
}

//把原始任务和超时任务放到一个数组中,然后把数组放入一个Promise.race中
//超时设置为3秒
let p1=Promise.race([ajaxPromise(),timeoutPromise(3000)])
// 输出原始任务和超时任务的返回值,谁先执行完饭回谁
p1.then(function(data)=>{
  //假如ajax没有超过3秒就完成了,则执行该函数
  console.log(data)
},err=>{
  //假如ajax超过3秒也没有完成,则执行该函数
  console.log(err)
});

可以看到,当promise.race()和超时回调一起使用时,可以保证至少有一个输出信号,防止回调被挂住。
注意:还要考虑一点,如果超时了,返回超时错误后,ajax请求却还在执行中。这会导致一个问题,就是说这个超时的ajax请求并不会停止,它会继续执行任务,这可能会导致一些新的问题,比如:前端显示任务错误,后端却正在处理任务,只是慢了点,所以promise.race()要分情况使用。

4、调用回调次数过少或过多

调用过少,即:比如刚才的不被调用,有其他任务在无限循环,导致回调函数没机会加入到下一个事件循环队列中。或者:第三方库出bug,比如:ajax()请求在执行到一半时,系统坏掉了,导致丢失这一次请求,之后也不会再执行这一个回调函数。
调用过多,即:第三方库出bug,比如:ajax()中发现请求超时了,然后重复发送请求,导致调用回调时也执行了多次(即:第三方库出bug了,我们却不知道)
Promise的定义方式使得一个Promise只能被决议一次。如果出于某种原因,Promise的创建代码中试图调用多次resolve()或reject(),或者试图两者都调用,那么有效的调用有且仅有第一个,后续调用将被忽略。
比如:

var p =new Promise(function(resolve,reject){
    resolve(44);
    reject("timeout");
  });
  p.then((value)=>{
    console.log(value);//打印44
  });
  (err)=>{
    console.log(err);
  }

5、未能传递所需的环境和参数

第三方回调可能会忽略一些你想传递的值,而Promise则强行规定你只能传递一个值
首先,如果resolve()或reject()函数中没有值,则这个Promise最终结果值就是undefined。比如:

let a= new Promise(function(resolve, reject){
  //做一些异步操作
  setTimeout(function(){
    console.log('执行完成Promise');
    resolve();
  }, 2000);
})
console.log(a);//输出:Promise { <pending> }
a.then((data)=>{
  console.log(data)//输出:undefined
},(err)=>{
  console.log(err)
})

其次,Promise强制规定:如果resolve()或reject()函数中有多个参数,第一个参数之后的所有参数都会被忽略。比如:

let a= new Promise(function(resolve, reject){
  //做一些异步操作
  setTimeout(function(){
    console.log('执行完成Promise');
    resolve("哈哈哈","第二个参数");
  }, 2000);
})
console.log(a);//输出:Promise { <pending> }
a.then((data)=>{
  console.log(data)//输出:哈哈哈
},(err)=>{
  console.log(err)
})

如果需要传递多个值,则必须把它们封装到一个对象中,比如数组或对象。

6、吞掉可能出现的错误和异常

第三方库可能会吞掉一些错误或者异常,而这个过程是不会告诉你的,而Promise的总是会把异常和错误传递下去。因为Promise有一个拒绝机制,一旦发现结果是错误的,或者在执行中发现了JS语法或JS异常,下一个then会把这个错误值或异常值传递给“拒绝回调函数”。比如:

var p=new Promise((resolve, reject)=>{
  foo.bar();//foo未定义,所以这里会抛出错误,下面的语句自然不会执行。
  console.log("执行中");
  resolve('结果值');
})
p.then(
  function (data){
    console.log(data)//这里不可能运行
  },
  function rejected(err){
    console.log("出错了");//输出:出错了
    console.log(err);//输出:ReferenceError: foo is not defined
  }
)

但如果then()注册的回调中出现JS异常错误会怎样?比如:

var p=new Promise((resolve, reject)=>{
  resolve('结果值');
})
p.then(
  function (data){
    console.log("开始运行");//输出:开始运行
    foo.bar();//foo未定义,所以这里会抛出错误,下面的语句自然不会执行。
    console.log(data)//这里不可能运行
  },
  function rejected(err){
  	console.log("发生错误");
    console.log(err);//这里不可能运行
  }
)

你会发现上面代码中的错误好像被吞掉了,实际上不是这样的,这是因为p.then()本身又返回了一个Promise。这个Promise的结果取决于p.then()中函数返回的值,如果p.then()中的回调函数出现错误或者JS异常,则会把p.then()返回的Promise的结果值变为拒绝值,从而在下一个p.then()中捕获,比如:

var p=new Promise((resolve, reject)=>{
  resolve('结果值');
})
let a=p.then(
  function (data){
    console.log("开始运行");//输出:开始运行
    foo.bar();//foo未定义,所以这里会抛出错误,下面的语句自然不会执行。
    console.log(data)//这里不可能运行
  },
  function rejected(err){
  	console.log("发生错误");
    console.log(err);//这里不可能运行
  }
)
a.then(
  function (data){
    console.log(data)//这里不可能运行
  },
  function rejected(err){
  	console.log("捕获错误");//输出:捕获错误
    console.log(err);//输出:ReferenceError: foo is not defined
  }
)

Promise可以信任的原因

可能大家都有一个疑问:为什么Promise的回调就比单纯使用回调更值得信任呢?仅仅是因为Promise把一切回调都变成异步,拒绝同步回调?是的,但这只是其中一部分原因,还有一些重要的原因我们还没有介绍。

鸭子类型(thenable)

首先需要介绍一下术语——鸭子类型:如果它看起来像只鸭子,那它一定是个鸭子。
即:一个对象中有then方法,不管这个方法是干什么的,我们就认定它是一个Promise对象。这听起来很荒唐,但很多时候这样很方便,我们可以快速,低成本的去判断一个对象是否是Promise。但方便并不是万无一失的,万一有一个不知道Promise是什么的人自定义了一个带then方法的对象,你去判断这个对象的时候,就会把这个不是Promise的对象判断为Promise。比如:

//函数isThenable功能:判断一个对象是否是thenable
function isThenable(obj){
  if(obj!=null && (typeof obj === "object" || typeof obj === "function") && typeof obj.then ==="function"){
	  return "这个对象是一个Promise"
	}else{
	  return "这个对象不是Promise"
	}
}
//一个非Promise对象
var o={then:function(){}}

console.log(isThenable(o))//输出:这个对象是一个Promise

如上代码所示,很显然对象o并不是一个Promise。这种判断本身就是不严谨的,最好不要用这种办法去判断对象是不是Promise。从现在开始我们将纠正这种判断方式:这种判断方式只能判断一个对象是否类似Promise,如果一个对象带有then方法,我们将这个对象称为thenable。thenable是一种鸭子类型。比如:

//函数isThenable功能:判断一个对象是否是thenable
function isThenable(obj){
  if(obj!=null && (typeof obj === "object" || typeof obj === "function") && typeof obj.then ==="function"){
	  return "这个对象是一个thenable"
	}else{
	  return "这个对象不是thenable"
	}
}
//一个非Promise对象
var o={then:function(){}}

console.log(isThenable(o))//输出:这个对象是一个thenable

现在我们知道了鸭子类型,就可以提出一个问题了:如果有任何其他代码无意或者恶意地给Object.prototype、Array.prototype或任何其他原生原型添加then(),你无法控制也无法预测。比如:

//函数isThenable功能:判断一个对象是否是thenable
function isThenable(obj){
  if(obj!=null && (typeof obj === "object" || typeof obj === "function") && typeof obj.then ==="function"){
	  return "这个对象是一个thenable"
	}else{
	  return "这个对象不是thenable"
	}
}

//给Object.prototype、Array.prototype添加then()
Object.prototype.then=function(){}
Array.prototype.then=function(){}

//随便定义一个对象和一个数组
var v1={hello:"world"};
var v2=[1,2,3];

//发现随便定义的对象都是一个thenable
console.log(isThenable(v1))//输出:这个对象是一个thenable
console.log(isThenable(v2))//输出:这个对象是一个thenable

并且,如果回调的函数中返回了 ‘有空的then()方法的对象’ 或者 返回的对象中的 ‘then(par1,par2)方法中没有调用then(par1,par2)中传入的参数par1或者par2’ ,那么如果Promise决议到了这个对象,这个Promise将会永远挂住。比如:

//第三方库中的函数
function getObj(){
  Object.prototype.then=function(){}//直接污染全局导致所有对象都变为thenable
  let thenable = {
    names:"hhc",
  };
  return thenable;
}
//或者第三方库函数如下:(也是一样的结果)
function getObj2(){
  let thenable = {
    then(hhh, reject) {
      console.log("then");
      // 不调用hhh和reject,可以取消下面两行注释看结果
      // hhh("data");
      // reject("reason")
    },
  };
  return thenable;
}

//用户第一步创建了一个Promise
let p1=new Promise((resolve, reject)=>{
  resolve('第一步');
})
//用户第二步调用了第三方库的一个函数,并把这个函数返回的对象当作返回值
let p2=p1.then((data)=>{
  //显示第一步的值
  console.log(data,"成功啦");//输出:第一步 成功啦
  // let o=getObj();
  let o=getObj2();
  return o
},(err)=>{
  console.log("失败啦");
});
//你会发现下面的代码永远不会执行
//用户第三步想查看第二步的值,并返回true
let p3=p2.then((data)=>{
  //显示第二步的值
  console.log(data);
  //同时打印一些数据
  console.log("第二步,成功啦");
  return true
},(err)=>{
  console.log("失败啦");
});

这时候就应该用promise.race()方法来避免Promise被永远挂住。
但是非攻击的情况下不要使用Object.prototype.then等类似的方法进行原型注入攻击

Promise.resolve()

就如上面所示,如果有任何其他代码无意或者恶意地给Object.prototype、Array.prototype或任何其他原生原型添加then(),你无法控制也无法预测。在实际开发中,肯定有很多的thenable的代码导致了Promise的错误,从而使Promise变得不再让人信任,比如下面这种情况:

//假设对象thenable是第三方代码库中的一个普通对象,具有then方法
var thenable= {
  then:function(cb,errcb){
    cb(42);
    errcb("抛出一个错误");
  }
}
//而我们错误的把这个thenable当作一个Promise对象,会出现下面的情况
thenable.then(
  function fulfilled(data){
    console.log(data);//输出:42
  },
  function rejected(err){
    console.log(err);//输出:抛出一个错误
  }
)
//代码同时输出了“42”和“抛出一个错误”

如上所示,这个thenable对象的行为和Promise并不完全一致,但我们使用第三方库的时候不知道这个thenable到底是不是JS中内置的Promise对象。这是第三方库恶意实现的thenable吗?还是只是因为它不知道Promise该如何运作?说实话这不重要,重要的是:我们如何把第三方库中的thenable对象变为可信任的Promise对象(即:JS内置Promise对象),这样可以让Promise更让人信任!
那如何使Promise更能让人信任呢?Promise官方给出了一个规范化thenable代码的方法Promise.resolve()
这个方法的作用就是:可以接收任何非Promise的thenable值,并试图展开这个值,并且展开过程会持续到提取出一个具体的非thenable的最终值,然后把这个最终值给一个新的Promise对象,并把这个最终值作为新Promise的决议值。
现在我们用Promise.resolve()方法对上述代码进行改进:

//假设对象thenable是第三方代码库中的一个普通对象,具有then方法
var thenable= {
  then:function(cb,errcb){
    console.log("代码执行了")
    cb(42);
    errcb("抛出一个错误");
  }
}
//把上面的thenable对象传入Promise.resolve()中
//Promise.resolve()会把thenable对象展开,所以会执行其中的代码
let p=Promise.resolve(thenable)//输出:代码执行了

p.then(
  function fulfilled(data){
    console.log(data);//输出:42
  },
  function rejected(err){
    console.log(err);//没有任何输出
  }
)

如果Promise.resolve()无法展开thenable获取结果,也会返回一个未决议的新的Promise对象,这可能会使整个Promise链被挂住,后面的代码将不会执行。比如:

//假设对象thenable是第三方代码库中的一个普通对象,具有then方法
var thenable= {
  then:function(cb,errcb){
    console.log("第一步执行了")//输出:第一步执行了
    return true;
  }
}
//把上面的thenable对象传入Promise.resolve()中
//Promise.resolve()会把thenable对象展开,所以会执行其中的代码
//但是这个thenable中无法展开获取结果,所以会获取到一个永远挂住无法决议的Promise
let p=Promise.resolve(thenable)//输出:第一步执行了


//下面的代码将不会执行
p.then(
  function fulfilled(data){
    console.log("第二步执行了")//没有任何输出
    console.log(data);//没有任何输出
  },
  function rejected(err){
    console.log("第二步执行了,发生异常")//没有任何输出
    console.log(err);//没有任何输出
  }
)

我们要小心这种情况,或者用promise.race()方法来避免程序被挂住。
如果我们传入Promise.resolve()方法中的对象不是thenable也不是Promise,而是一个普通对象,那么Promise.resolve()也会将其转换为一个完成状态的Promise对象。比如:

//假设对象thenable是第三方代码库中的一个普通对象,具有then方法
var obj= {
  sex:"男"
}
//把上面的普通对象传入Promise.resolve()中
let p=Promise.resolve(obj)


p.then(
  function fulfilled(data){
    console.log("第二步执行了")//输出:第二步执行了
    console.log(data);//输出:{ sex: '男' }
  },
  function rejected(err){
    console.log("第二步执行了,发生异常")//没有任何输出
    console.log(err);//没有任何输出
  }
)

如果我们传入Promise.resolve()方法中的对象已经是一个真正的Promise,那么你得到的就是它本身。比如:

var p1=new Promise((resolve, reject)=>{
  resolve('正确结果');
})

let p2=Promise.resolve(p1)

console.log(p1===p2)//输出:true
p2.then(
  function fulfilled(data){
    console.log("第二步执行了")//输出:第二步执行了
    console.log(data);//输出:正确结果
  },
  function rejected(err){
    console.log("第二步执行了,发生异常")//没有任何输出
    console.log(err);//没有任何输出
  }
)

所以我们在使用第三方库的Promise对象时,最好使用Promise.resolve()来过滤一下这个对象,这样可以使得新代码兼容旧代码,还可以让我们放心的使用第三方库提供的Promise对象。
还有一个要注意的细节,Promise.resolve()展开thenable时,是异步展开的,返回的Promise对象是未决议状态,异步执行完thenable中的then方法后,才可能改变新Promise到决议状态,即这个新Promise是异步完成的。如果Promise.resolve()中传入的是一个非thenable的普通值或者普通对象,则返回的Promise对象是已决议的状态,即这个新Promise是同步完成的。如果Promise.resolve()中传入的是一个Promise对象,则不会创建新Promise对象,这时Promise.resolve()方法返回的就是原来的旧Promise对象,决议状态自然取决于旧Promise对象。
当然在Promise链内部使用thenable对象作为完成决议值也是会被自动展开的(之前的鸭子类型中有提到过),比如:

//第三方库中的函数
function getObj(){
  let thenable = {
    then(hhh, reject) {
      console.log("then被调用了");//输出:then被调用了
      hhh("数据正确");//该行代码有效
      reject("reason");//该行代码无效,但也执行了
      console.log("哈哈哈哈");//输出:哈哈哈哈
    },
  };
  return thenable;
}

//用户第一步创建了一个Promise
let p1=new Promise((resolve, reject)=>{
  resolve('第一步');
})
//用户第二步调用了第三方库的一个函数,并把这个函数返回的对象当作返回值
let p2=p1.then((data)=>{
  //显示第一步的值
  console.log(data,"成功啦");//输出:第一步 成功啦
  let o=getObj();//用第三方库获取一个thenable对象,同时展开其中的数据,即:执行一遍其中的then函数
  return o
},(err)=>{
  console.log("失败啦");
});
//用户第三步查看第二步的输出
p2.then((data)=>{
  console.log("第三步执行")//输出:第三步执行
  //显示第二步的值
  console.log(data)//输出:数据正确
},(err)=>{
  console.log("失败啦");
})

注:我们需要知道Promise()构造器的第一个参数会展开thenable(和Promise.resolve()一样),而第二个参数是不会展开thenable的,详细例子请查看Promise.reject()的具体内容

链式流

其实通过前面的例子也不难发现,Promise不是单纯的一次性的this-then-that操作。而是可以把多个Promise连接起来完成一系列异步操作。这种方式得以实现的关键在于以下两个Promise的固有行为:

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

下面是对这两个固有行为的解释,然后说明应该如何正确使用链式流控制异步序列。

var p=Promise.resolve(21);

var p2=p.then(function(data){
  console.log(data);//输出:21
  return data*2;//用data*2(即:42)来填充p2
})
// p2调用then()调用在运行时会从return v*2语句接受完成值。
// 当然,p2.then()又创建了一个新的Promise,可以用一个变量接收
p2.then(data=>{
  console.log(data);//输出42
})

如果必须创建一个临时变量会有点麻烦,所以可以把上面代码优化一下:

var p=Promise.resolve(21);
p.then(function(data){
  console.log(data);//输出:21
  return data*2;//用data*2(即:42)来填充p2
}).then(data=>{
  console.log(data);//输出42
})

但是这里有个问题:如果步骤2需要等待步骤1异步来完成一些事情该怎么办?我们在上面的代码中使用了立即返回return语句,这会立即完成链接的Promise。如何解决这个问题,还需要回忆一下,使得Promise中每个步骤都有异步能力的关键是什么?是在异步完成或失败后调用resolve()或者reject()函数。所以我们得用resolve()或者reject()函数来代替return语句,使得Promise不会立即完成。但是并没有这样的语法,所以我们使用了一个稍微取巧的方法:用return返回一个Promise对象或者返回一个thenable值。就像Promise.resolve()会自动展开接收到的thenable值一样,return返回的thenable值也会自动展开,转换为一个Promise。根据这个方法我们就可以自由的去使用异步链了。例如:

var p= Promise.resolve(21);
p.then(function(data){
  console.log(data);//输出:21
  return new Promise(function(resolve,reject){
    setTimeout(function(){
      resolve(data*2);//异步等待10秒后用data*2(即:42)来填充p2
    },10000);
  });
}).then(data=>{
  console.log(data);//10s后输出42
})

我们可以把延迟Promise(没有决议的消息)的创建过程一般化到一个工具中,以便在多个步骤中复用。在开发时,我们也经常会遇到这样的情况:他们想要通过本身并不支持Promise的工具(就像下面代码中的ajax(),它接收的是一个回调函数)实现Promise的异步流控制。虽然原生的ES6 Promise机制并不会自动为我们提供这个模式,但所有实际的Promise库都会提供,通常这个过程称为“提升”或“Promise化”。

//假设第三方的ajax函数如下:
// url是接口,resolve和reject是ajax收到消息后的回调函数
function ajax(url,resolve,reject){
  const xhr=new XMLHttpRequest();
    xhr.open("GET",url);
    xhr.send();
    // 处理结果(绑定事件)
    xhr.onreadystatechange=function(){
      if (xhr.readyState == 4){
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
              // 成功  
              resolve(xhr.response);
            } else {
              // 失败
              reject("Request was unsuccessful: " + xhr.status);
            }
        }
    }
}
//封装Ajax请求
function myAjax(url){
  return new Promise(function(resolve,reject){
    //ajax()中的回调函数应该是我们这个Promise的resolve或者reject
    ajax(url,resolve,reject);
  })
}
//封装延迟Promise工具
function delay(time){
  return new Promise(function(resolve,reject){
    setTimeout(function(){
      let str_tmp="延迟了"+time+"秒"
      resolve(str_tmp);//这里换为reject()后就可以变为超时控制器
    },time);
  })
}
//自定义一个超时Promise工具,delay为规定的超时时间,单位毫秒
function timeoutPromise(time){
  return new Promise(function(resolve,reject){
    setTimeout(function(){
      let str_tmp="Timeout!异步调用超时"+time+"秒"
      reject(str_tmp);
    },time);
  });
}
//使用Promise.race防止ajax调用卡住
Promise.race([myAjax("https://url1"),timeoutPromise(3000)]).then(data=>{
  console.log(data);//输出Ajax调用结果
  return delay(10000);//延迟10秒后执行下一步
},err=>{
  console.log(err);//输出报错内容
}).then(data=>{
  console.log(data);
  //使用Promise.race防止ajax调用卡住
  return Promise.race([myAjax("https://url2"),timeoutPromise(3000)])
},err=>{
  console.log(err);//输出报错内容
})

默认完成处理函数和默认拒绝处理函数

根据上面的例子,我们知道then和Promise的构造函数所接收的第二个参数是一个拒绝函数,当上一步出错时,下一步就会捕获这个错误,并且选择停止Promise或者继续执行Promise链,下面做两个例子:
例一:发生错误后停止Promise链

Promise.resolve(1).then((data)=>{
  foo.bar(data)//发生未定义错误
  return "第一步成功"
}).then((data)=>{
  //这段不会执行
  console.log(data)
  return "第二步成功"
},(err)=>{
  console.log("第一步失败")//输出:第一步失败
  console.log("失败原因",err)//输出:失败原因 ReferenceError: foo is not defined
}).then((data)=>{
  //以下代码不会执行
  console.log(data)
})

例二:发生错误后处理错误继续执行

Promise.resolve(1).then((data)=>{
  foo.bar(data)//未定义错误
  return "第一步成功"
}).then((data)=>{
  //这段不会执行
  console.log(data)
  return "第二步成功"
},(err)=>{
  console.log("第一步失败")//输出:第一步失败
  console.log("失败原因",err)//输出:失败原因 ReferenceError: foo is not defined
  return "第一步数据失败处理完成,第二步成功"
}).then((data)=>{
  console.log(data)//输出:第一步数据失败处理完成,第二步成功
})

假如上一步发生错误,且下一步的then()中未定义拒绝处理函数,则会有一个默认拒绝处理函数被调用,并且会把错误持续用throw语句抛出到下一层,这使得错误可以继续沿着Promise链传播下去,直到遇到自定义的错误拒绝函数。如果走到最后都一直没有遇到错误拒绝处理函数,会把错误直接抛出到系统。
比如:未定义错误拒绝函数

Promise.resolve(1).then((data) => {
  foo.bar(data)//发生未定义错误
  return "第一步成功"
})
  .then((data) => {
    //这段不会执行
    console.log(data)
    return "第二步成功"
  })
  .then((data) => {
    //这段不会执行
    console.log(data)
  })
//输出: UnhandledPromiseRejectionWarning: ReferenceError: foo is not defined

等同于如下代码:

Promise.resolve(1).then((data) => {
  foo.bar(data)//发生未定义错误
  return "第一步成功"
})
  .then((data) => {
    //这段不会执行
    console.log(data)
    return "第二步成功"
  }, (err) => { 
    throw err 
  })
  .then((data) => {
    //这段不会执行
    console.log(data)
  }, (err) => { 
    throw err 
  })
//输出: UnhandledPromiseRejectionWarning: ReferenceError: foo is not defined

比如:在第三步中定义了错误拒绝函数

Promise.resolve(1).then((data)=>{
  foo.bar(data)//发生未定义错误
  return "第一步成功"
})
  .then((data)=>{
  //这段不会执行
  console.log(data)
  return "第二步成功"
})
  .then((data)=>{
  //这段不会执行
  console.log(data)
},err=>{
  //以下代码会执行
  console.log("第二步没有执行,但在第三步捕获了第二步的错误")//输出:第二步没有执行,但在第三步捕获了第二步的错误
  console.log(err)//输出:ReferenceError: foo is not defined
  return Promise.resolve("第三步发现了错误")
})
  .then((data)=>{
  //以下代码会执行
  console.log(data)//输出:第三步发现了错误
  return "第四步执行完毕"
},err=>{
  //这段不会执行
  console.log("在第三步捕获了错误")
  console.log(err)
})

同理,如果没有给then()传递一个适当有效的完成处理函数,系统会自动补上一个默认完成处理函数,比如:

let p=Promise.resolve("第一步完成")
//这里只传入一个错误拒绝函数
p.then(
  //如果省略或者传入一个非法的完成处理函数,会发生如下情况
  null,err=>{
  //这一段永远不会执行
  console.log("上一步出错了")
}).then(data=>{
  console.log(data)//输出:第一步完成
},err=>{
  //这一段永远不会执行
  console.log("上一步出错了")
})

经过上面的代码分析,你会发现,默认的完成处理函数只是把接收到的任何传入值传递给下一个步骤(下一个Promise)而已。等同于如下代码:

let p=Promise.resolve("第一步完成")
//这里只传入一个错误拒绝函数
p.then(
  //默认的完成处理函数等同于如下函数
  data=>{
    return data
  },err=>{
  //这一段永远不会执行
  console.log("上一步出错了")
}).then(data=>{
  console.log(data)//输出:第一步完成
},err=>{
  //这一段永远不会执行
  console.log("上一步出错了")
})
.catch(err=>{…})错误穿透

上面的代码有一个点值得注意:.then(null,err=>{…})这种模式——只处理拒绝(如果有的话),但又把完成值传递下去——有一个缩写形式的API:.catch(err=>{…})
有了上面默认错误拒绝函数的基础,我们可以完成一个错误穿透操作:所有Promise链中的步骤都不需要进行错误处理,Promise链中可能发生的错误都可以在最后一个.catch(err=>{…})中进行处理,比如:

let p = Promise.resolve("第一步完成")
//这里只传入一个错误拒绝函数
p.then(
  //默认的完成处理函数等同于如下函数
  data => {
    console.log(data)//输出:第一步完成
    throw "第二步失败"
    return "第二步完成"
  }
  )
  .then(
  data => {
    //这一段代码不会执行
    console.log(data)
    return "第三步完成"
  }
  )
  .catch(err=>{
    //处理错误
    console.log(err)//输出错误:"第二步失败"
  })

但是错误穿透也有一个特性:如果某个步骤报错了,后面的所有步骤都不会运行,任务自然会中断,我们可以根据需求使用错误穿透模式。
现在总结一下Promise使得链式流程控制可行的原因,即Promise特性:

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

Promise规范了异步,使得一团乱麻的回调,变成可以顺序表达的优美代码,但是,仍然有大量重复样板代码(then()以及里面的函数声明)在后面将会见到用生成器实现的更优美可读的代码。

Promise.reject()

这里将介绍决议(resolve),完成(fulfill)和拒绝(reject)命名的区别
根据前面的介绍,我们在使用new Promise((resolve,reject)=>{})构造函数时为什么不使用“fulfill”来替代“resolve”?
现在我们来解释下为什么用单词“resolve”来表达Promise的结果。
要解答这个问题,还需要介绍一个方法Promise.reject(),Promise.reject()不会像Promise.resolve()一样进行展开。如果向Promise.reject()传入一个Promise/thenable对象,它会把这个Promise/thenable对象原封不动地设置为拒绝理由放入到一个新的结果是拒绝值的Promise对象中,然后把新Promise对象返回。后续的拒绝处理函数接收到的是你实际传递给Promise.reject()的那个Promise/thenable,而不是其底层的立即值。如果向Promise.reject()传入一个普通值,则会把这个普通值设置为拒绝理由放入到一个新的结果是拒绝值的Promise对象中,然后把新Promise对象返回。
下面看一个Promise.reject()例子:

//向Promise.reject()传入一个普通值,用一个变量p1去接收其返回的新Promise对象
let p1=Promise.reject("普通错误")
p1.then((data)=>{},(err)=>{
  //处理错误
  console.log(err)//输出:普通错误
})

//向Promise.reject()传入一个正确完成的Promise对象
//用一个变量p2去接收Promise.reject()返回的新的错误结果的Promise对象
let p2=Promise.reject(Promise.resolve("一个正确的Promise对象"))
p2.then((data)=>{},(err)=>{
  //处理错误
  console.log(err)//输出:Promise { '一个正确的Promise对象' }
})

现在我们换个思路去看,如果传入Promise.resolve()函数内部的对象是thenable,则Promise.resolve()会展开内部的thenable,得到最终值,如果传入Promise.resolve()函数内部的对象是Promise,则Promise.resolve()将原封不动的返回内部的原Promise,也就是说,如果Promise.resolve()中传入的是一个已经是拒绝值的Promise对象,或者一个将会执行第二参数的thenable,则Promise.resolve()返回的是一个拒绝值的Promise对象
例如:

//定义一个thenable
var rejectedTh={
  then:(resolved,rejected)=>{
    //执行第二个参数
    rejected("错误值")
  }
}
//Promise.resolve展开thenable得到一个错误Promise值
var rejectedPr=Promise.resolve(rejectedTh);
rejectedPr.then((data)=>{
  //这里不会执行
  console.log("第一步正常")
},(err)=>{
	console.log(err)//输出:错误值
})

//Promise.resolve原封不动的返回一个错误Promise值
var rejectedPr2=Promise.resolve(Promise.reject("错误Promise值"));
rejectedPr2.then((data)=>{
  //这里不会执行
  console.log("第一步正常")
},(err)=>{
	console.log(err)//输出:错误Promise值
})

通过上一个例子,我们不难看出Promise.resolve()返回的Promise对象结果不一定是完成的,也可能是拒绝的,所以得用“resolve(决议)”来表达Promise的结果,而不是用“fulfill(完成)”来表达Promise的结果。而“reject(拒绝)”用来表示Promise的拒绝结果完全没问题。
注:我们需要知道Promise()构造器的第一个参数会展开thenable(其他特性和Promise.resolve()一样),而第二个参数不会(其他特性和Promise.reject()一样)。例如:

var rejectedPr=new Promise((resolve,reject)=>{
  // 用一个被拒绝的promise完成这个promise
  resolve(Promise.reject("错误数据"))
  //这里resolve会展开内部的Promise,而内部的Promise是一个拒绝值,所以最后的结果是拒绝
})
rejectedPr.then(data=>{
  // 这里不会有任何输出
  console.log("第一步成功了")
},err=>{
  console.log(err)//输出:错误数据
})

var testPr=new Promise((resolve,reject)=>{
  // 用一个被拒绝的promise完成这个promise
  reject(Promise.resolve("这是个正确完成的Promise对象"))
  //这里reject不会展开内部的Promise,而是直接把Promise传递到下一步中
})
testPr.then(data=>{
  // 这里不会有任何输出
  console.log("第一步成功了")
},err=>{
  console.log(err);//输出:Promise { '这是个正确完成的Promise对象' }
})

根据上面的分析,我们知道了什么时候用决议(resolve),什么时候用拒绝(reject)。但是什么时候用完成(fulfill)呢?下面我们来关注下提供给then()方法的回调。它们应该如何正确命名呢?这里有多种情况,ES6默认的一种命名方法,如下:onFulfilled()和onRejected()

function onFulfilled(data){
  console.log(data)
}
function onRejected(err){
  console.error(err)
}
p.then(onFulfilled,onRejected)

原因是ES6官方认为,对于then()的第一个参数来说,总是在处理完成的情况下调用的,所以不需要使用标识两种状态的术语:“resolve(决议)”。
但是也要注意then()的第一个和第二个参数都可以使得 then()返回的 Promise 处于三种状态中的任意一种(等待,完成,拒绝)

Promise的其他API介绍

Promise.all([…])并行且全都完成

Promise.all([…])的作用是,等待其中的Promise全部完成后才能返回一个新的完成状态的Promise
应用:假设想同时发送两个ajax请求,等它们不管以什么顺序全部完成后,再发送第三个ajax请求。代码如下:

//假设request()是一个Promise.aware Ajax工具
var p1=request("http://url1")
var p2=request("http://url2")
Promise.all([p1,p2]).then(function(msgs){
  //当p1,p2都完成时才会执行
  console.log(msgs)//会输出一个数组,里面存有p1、p2的完成值
  return request("http://url3");
}).then(function(msgs){
  // 输出第三次ajax请求返回的数据
  console.log(msgs)
},function(err){
  //如果前两次ajax中,任何一个结果值是拒绝的,则输出拒绝情况
  console.log(err)
})

Promise.all([…])需要的参数是一个数组,严格来说传递给Promise.all([…])的数组中的值可以是Promise、thenable,甚至可以是立即值。Promise.all([…])被调用后会返回一个新promise对象,且这个新promise对象会接收一个决议值(就是上面代码中的msgs或者err)。如果传入Promise.all([…])的数组中的promise全都是完成的promise,则Promise.all([…])返回的新promise中所接收的完成决议值是 所有完成消息组成的数组,而且该消息组成的数组的值顺序与传入Promise.all([…])的数组中的promise顺序一致(与完成顺序无关)。如果传入Promise.all([…])的数组中的promise有一个出现拒绝情况,则只返回最先拒绝的结果值给新promise,并把新promise设置为拒绝状态。比如:

var p1=new Promise((resolve, reject)=>{
  setTimeout(()=>{
    reject("错1")
  },1000)
})
var p2=Promise.reject("错2")
var p3=Promise.reject("错3")
Promise.all([p1,p2,p3]).then(function(msgs){
  //当p1,p2,p3都完成时才会执行,这里不会执行
  console.log(msgs)
}).then(function(msgs){
  console.log(msgs)
},function(err){
  //数组[p1,p2,p3]中,任何一个结果值是拒绝的,则输出最先拒绝情况
  //这里p2,p3是同时完成的,则先输出数组最前面的
  console.log(err)//输出:错2
})

就本质而言,Promise.all([…])的数组中的每个值都会通过 Promise.resolve()方法 过滤。如果传入Promise.all([…])中的数组是空的,则Promise.all([…])与Promise.resolve()的作用是相同的,会返回一个立即完成的promise对象。
假如传入Promise.all([…])中的Promise对象其中有任何一个被拒绝,则Promise.all([…])返回的Promise对象就会立即被拒绝,并丢弃来自其他所有promise的全部结果。

Promise.any([…])并发任何一个完成则完成

Promise.any([…])这个模式类似于Promise.all([…]),但是会忽略拒绝,所以只需要完成一个而不是全部。

Promise.first([…])第一的胜出

Promise.first([…])这个模式类似于与Promise.any([…])竞争,只要第一个Promise完成,它就会忽略后续的任何拒绝和完成。

Promise.none([…])并行且全都失败

Promise.none([…])这个模式类似于Promise.all([…]),不过完成和拒绝的情况互换了。所有的Promise都要被拒绝,即拒绝转化为完成值,反之亦然。

Promise.last([…])最后的胜出

Promise.last([…])与Promise.first([…])相反,等最后一个决议后,把最后一个成功完成的值当作结果

.finally()

.finally()

promise.race([…])并发中最先完成的作为结果

前面有介绍,此处跳过。

并发迭代,自定义Promise.map([…],function)

https://blog.csdn.net/zjjcchina/article/details/122597335
我们将定义一个Promise.map([…],function)方法,这个方法功能类似于数组的Array.map()方法。
现在假设有如下需求:我们需要对一个数组中的所有元素同时进行处理(即:并发执行某个处理数据的函数)。要求:这个数组中的元素可以是Promise或其他任何值。我们需要知道所有数据是否都处理完成,并且用一个新数组存储这些新数据。
现在拆分任务:首先这个Promise.map([…],function)要接收一个 “数组” 和一个 “要在每个值上运行的函数(任务)” 作为参数。Promise.map([…],function)本身要返回一个Promise,其完成值是一个新数组,这个新数组保存任务执行之后的异步完成值(值的顺序要和传入前的一致)
实现代码如下:

if(!Promise.map){
  Promise.map= function(arr,fn){
    //返回一个等待所有promise完成的新promise
    return Promise.all(
      //使用Array.prototype.map()方法,把普通数组转换为Promise对象数组
      arr.map(value=>{
        return Promise.resolve(fn(value))
      })
    )
  }
}

使用方法如下:

if(!Promise.map){
  Promise.map= function(arr,fn){
    //返回一个等待所有promise完成的新promise
    return Promise.all(
      //使用Array.prototype.map()方法,把普通数组转换为Promise对象数组
      arr.map(value=>{
        return Promise.resolve(fn(value))
      })
    )
  }
}
//使用方法如下:
let p1="hhh"
let p2=Promise.resolve("ajax2")
// let p3=Promise.reject("错误3")
let p3=Promise.resolve("ajax3")
// 待处理的数组
let pList=[p1,p2,p3]
// 自定义任务函数
function getValue(promise){
  // 取出最终值进行处理
  return Promise.resolve(promise).then(value=>{
    return value+"处理成功"
  })
}
Promise.map(pList,getValue).then(dataList=>{
  console.log(dataList)//输出:[ 'hhh处理成功', 'ajax2处理成功', 'ajax3处理成功' ]
},err=>{
  console.log("报错内容:",err)
})

用Promise实现异步队列queue([…],function)函数

https://blog.csdn.net/qq_55870308/article/details/124249723
以下代码会按从左到右顺序执行数组[p1,2,p3,4,5]中的元素。必须先执行完毕前面的才会执行后面的。

function queue(num,fn){
  let promise = Promise.resolve();
   num.map(value=>{
       promise = promise.then(res => {
          return Promise.resolve(fn(value))
       })
   })
}
function ajax(v){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log(v)
      resolve(v)
    },1000)
  })
}
let p1=Promise.resolve("hhc")
let p3=Promise.resolve("ajax3")
// let p3=Promise.reject("错误2")
queue([p1,2,p3,4,5],ajax)

小结

resolve,reject,race,all是函数属性,构造函数本身就具有的
then,catch是原型属性,是promise实例具有的属性

Promise的局限性

promise通常用来解决回调地狱的问题,但也有几个不可忽视的缺点
promise一旦新建就会立即执行,无法中途取消
当处于pending状态时,无法得知当前处于哪一个状态,是刚刚开始还是刚刚结束
如果不设置回调函数,promise内部的错误就无法反映到外部
promise封装ajax时,由于promise是异步任务,发送请求的三步会被延后到整个脚本同步代码执行完,并且将响应回调函数延迟到现有队列的最后,如果大量使用会大大降低了请求效率。

无法避免原型注入污染

无法捕获最后一步的错误

理论上的解决方法

node中的解决方法如下:https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode

Promise使用小结

let a= new Promise(function(resolve, reject){
  //做一些异步操作
  setTimeout(function(){
    console.log('执行完成Promise');
    resolve('要返回的数据可以任何数据例如接口返回数据');
  }, 2000);
}) 
let b=Promise.resolve("任何对象转换为Promise")
let p=b.then(data=>{//then会返回一个新的Promise
  console.log("获取上一步的结果")
  return "返回给下一个Promise的完成值"
},err=>{
  console.log("获取上一步的错误")
})

迭代器

迭代器知识点可以结合python的迭代器一起学习和对比
迭代器的目的:
1、使所有可遍历的数据结构用统一的方法去访问其中的数据(即:不用关心数据结构的内部情况,直接取值)
统一的方法指的是:for…of… 语句 和 next() 方法
2、对于数量很庞大的数据,不用一次性全加载到内存中(比如把1万亿个数据加载到一个数组,就是把大数据一次性加载到内存中),而是需要一个就取一个(缺点,只能从头到尾一个一个取,不能回头,ps:其实回头也是可以用自定义next()方法实现的,但是这样会破坏默认的迭代器协议规则,最后做出来的东西大概率没有什么用)
一般来说,我们访问数组中的元素的方法有很多,比如:

// 遍历数组
arr=[1,2,3,"a"]
for(var i=0;i<arr.length;i++){
  console.log(arr[i])
}
arr.forEach((item)=>{
  console.log(item);
})
function test(){
	console.log(...arguments);
}
test(arr)//输出:[ 1, 2, 3, 'a' ]

// 遍历字符串中字符
str="abc"
for(var i=0;i<str.length;i++){
  console.log(str[i])
}

//遍历对象中的属性
obj={a:1,b:2,c:"hhc"}
for(let i in obj){
  console.log(i,obj[i])
}

// 遍历一串变量
function test(){
	console.log(...arguments);
}
test(1,2,3,4)//输出:1 2 3 4

for…of…

通过上述代码,我们可以看到,遍历一个类似的数据结构,却有很多种遍历方法,这无疑增加了代码的复杂程度,所以有必要统一一下遍历方法:使用for…of…:
但是使用for…of…有一个前提:被for…of…遍历的对象必须是可迭代的对象
比如:数组和字符串,这两种数据结构的原型对象都有实现迭代器协议,即它们的原型对象都有实现Symbol.iterator()方法。这个方法后面会讲,现在先看for…of…的用法:
只要对象中实现了Symbol.iterator()方法,就可以用for…of…遍历该对象,比如:

// 遍历数组
arr=[1,2,3]
for(let i of arr){
  console.log(i)
}

// 遍历字符串
str="abc"
for(let i of str){
  console.log(i)
}

// 遍历实现了Symbol.iterator()方法的对象
obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]: () => {//Symbol.iterator()方法其实就是返回一个带有next的对象
    let index = 0
    return {
      next: () => {//自定义next
        if (index < Object.keys(obj).length) {
          return { done: false, value: Object.keys(obj)[index++] }
        } else {
          return { done: true, value: undefined }
        }
      }
    }
  }
}
for(let i of obj){
  console.log(i)
}

迭代器方法next()

拥有next()方法的对象就是迭代器对象。
JS中有一个默认的约定:next()方法的一般作用是返回数据结构中的下一个值到一个新对象中,或者计算出数据结构的下一个值到一个新对象中,或者取出某个大数据的下一个值到一个新对象中。这个新对象的结构是这样的:

// 当next发现数据结构遍历完毕后返回的对象是:
{ done: true, value: undefined }
// 当next发现数据结构没有遍历完成时返回的对象是:
{ done: false, value: "数据结构的下一个值" }

比如自定义一个迭代器对象:

// 自定义一个用来遍历数组的迭代器对象
class arrIterator {
  constructor(array) {
    this.array = array
    this.index = 0
    this.next=function(){
      if (this.index < this.array.length) {
        return { done: false, value: this.array[this.index++] }
      } else {
        return { done: true, value: undefined }
      }
    }
  }
}
arr=[1,2,3]
// 根据数组创建迭代器对象
arr_iter=new arrIterator(arr);
console.log(arr_iter.next())
console.log(arr_iter.next())
console.log(arr_iter.next())
console.log(arr_iter.next())

虽然上面的代码确实实现了迭代器的功能,但是它并不完整,因为它并不能被for…of…遍历。一个对象要想用for…of…遍历,它必须实现迭代器协议,成为可迭代对象,即:拥有Symbol.iterator方法,并且Symbol.iterator方法必须返回一个迭代器对象。

Symbol.iterator方法,即迭代器协议

Symbol.iterator方法是迭代器协议,对象拥有这个方法就是可迭代对象。
Symbol.iterator方法的唯一作用是:返回一个迭代器对象。(在上一个例子中,对象arr_iter本身就是迭代器,而现在的 Symbol.iterator方法 则是 从不是迭代器的对象中 创建迭代器)
比如:

// 遍历实现了Symbol.iterator()方法的对象
obj = {
  a: 1,
  b: 2,
  c: 3,
  [Symbol.iterator]: () => {//Symbol.iterator()方法其实就是返回一个带有next的对象
    let index = 0;
    let m=new Map();
    m.set("a",1)
    m.set("b",2)
    m.set("c",3)
    return {
      next: () => {//自定义next
        let mapEntriesArray=[...m.entries()]
        if (index < m.size) {
          return { done: false, value:mapEntriesArray[index++] }
        } else {
          return { done: true, value: undefined }
        }
      }
    }
  }
}
for(let i of obj){
  console.log(i)
}

总结:可迭代对象,不一定是迭代器,可迭代对象调用Symbol.iterator方法会返回一个迭代器,至于返回的迭代器是什么样的。可以在实现Symbol.iterator方法时自定义。
然后迭代器其实就是实现next()方法的对象,一般情况下,默认next()方法的功能是:返回下一个值。
通过上述分析迭代器其实不一定要有Symbol.iterator方法,但是有Symbol.iterator方法会使遍历对象变得更简单,更快,更省内存。
但是所有规范中都有一个默认的约定:迭代器对象必须实现next()方法和Symbol.iterator方法,即:迭代器对象必须是可迭代对象,迭代器对象的Symbol.iterator方法返回的是它自身。

生成器

生成器打破了函数的完整执行

生成器可以暂停函数的执行,比如:

let x=1;
function* foo(){
  x++;
  yield;//暂停!
  console.log(x)
}
function bar(){
  x++;
}
//通过调用foo()创建一个迭代器it来控制生成器
var iterator=foo();
console.log(iterator)//输出:Object [Generator] {}
iterator.next()//无输出
console.log(x)//输出:2
bar()
console.log(x)//输出:3
iterator.next()//输出:3

现在来分析一下运行过程
(1)it=foo()运算并没有执行生成器函数foo(),而只是构造了一个迭代器(iterator),由这个迭代器控制函数foo()的执行。我们可以用console.log(iterator)查看到iterator的对象类型是一个Object [Generator] {}
(2)第一个iterator.next()启动了生成器函数foo(),并运行了foo()第一行的x++
(3)之后在下一行遇到一个yield;语句,然后foo()就暂停执行了。这个时候第一个iterator.next()调用就结束了。此时foo()还是活跃的,但处于暂停状态。
(4)我们查看x的值,发现为2。
(5)然后我们调用bar(),它通过x++再次递增x
(6)我们再次查看x的值,此时为3
(7)最后再执行第二次iterator.next(),把暂停状态到foo()函数恢复到执行状态,接着就是从foo()函数刚才暂停的位置继续往下执行,这时*foo()函数的console.log(x)被执行,输出x的值为3

生成器的输入与输出

生成器是特殊的函数,但是它仍然是一个函数,这意味着它本身的一些基本特性没有改变。比如,它仍然可以接收参数(即输入),也能够返回值(即输出)。例如:

function *foo(x,y){
  return x*y
}
var it = foo(6,7);//输入了两个参数,但是并没有真正执行*foo()函数
var result=it.next();//用it迭代器来正式执行*foo()函数,并用result来接收返回值
console.log(result)//输出一个对象:{ value: 42, done: true }
//我们都知道这个对象是迭代器next方法返回的值,参考上面的迭代器章节
//这个对象中,value代表*foo()函数return返回的值,done代表*foo()函数是否执行完毕,执行完毕则为true
console.log(result.value);// 输出:42

事实上,foo(6,7)的调用只是创建了一个迭代器对象it,用来控制生成器foo()的执行。调用it.next()就是指示生成器函数foo()从上一个位置开始继续执行,在下一个yield处停止执行,如果没有遇到yield语句,则执行到生成器函数的所有内容执行结束。
it.next()调用后返回一个对象{ value: …, done: bool },这个对象中有一个value属性,会保存从*foo()中返回的值(如果有的话)。

yield表达式

从生成器中断处输入输出(迭代消息传递)

从上面的分析,我们知道yield处会中断生成器函数的运行。但yield的用途不仅仅如此,yield配合next还可以实现从函数中断的地方进行输入参数和输出结果。
例如,从yield处输入一个值:

function *foo(x){
  let y =x*(yield);//yield 表达式如果用在另一个表达式中,必须放在圆括号里面
  return y
}
let it=foo(6)
it.next();//开始执行*foo()函数,到yield处停止
// 然后再次调用it.next()函数,并传入一个值7。这个传入的参数值7,将会替换掉yield,使语句 let y =6*(yield) 变为 let y =6*7 继续执行
let result=it.next(7);// 这时函数*foo()执行完毕,返回的值由result存储
// 最后输出*foo()函数的返回值
console.log(result.values)//输出:42

对于大部分开发者来说,上述代码很有迷惑性:根据你的视角不同,yield和next()调用有一个错位的感觉,一般来说,在函数执行中断时传入参数到yield位置,需要的next()调用语句要比yield语句多一个。但是只要按照上述代码的注释理解一遍执行过程,就会知道这没什么难的。(即:next()调用遇到yield先停止,下一次next()调用时再输入参数到yield的位置)
知道生成器*foo()中断时如何输入后,我们看看中断时如何输出:
例如,用yield表达式输出一个值:

function *foo(str){
  //首先对str进行处理,处理的结果返回给result
  let result=str+"吃饭"
  //在这里用yield中断,并返回result
  yield result;
  //下一步再对x处理,处理结果返回给resultTwo
  let resultTwo=str+"吃饱了"
  //再次用yield中断,并返回resultTwo
  yield resultTwo;
}
let it=foo("hhc");
let result1=it.next()
console.log(result1.value)//输出:hhc吃饭
let result2=it.next()
console.log(result2.value)//输出:hhc吃饱了

通过上述代码和注释,我们可以清晰的知道yield表达式是如何向外界传递消息的。这里还有一个细节,*foo()函数没有return语句,再结合刚才的例子,我们可以发现有无return我们都可以向函数外部输出值,所以有无return语句对函数输出没有影响
现在我们把输入和输出结合起来,看一个例子:

function *foo(str){
  //首先触发中断
  //然后返回str+"开始吃饭"
  //等待第一次外部输入的值,当外部输入值后,把值赋给result
  let result=(yield str+"开始吃饭")
  //然后又触发中断,并返回result+"已经吃完了"
  //等待第二次外部输入新的值,赋值给resultTwo
  let resultTwo=(yield result+"已经吃完了")
  //最后一次返回值
  return resultTwo+"饱了";
}
let it=foo("hhc");
let result1=it.next()//首先触发中断,获取第一次结果
console.log(result1.value)//输出:hhc开始吃饭
let result2=it.next("hhc")//然后第一次中途输入,并触发第二次中断,并返回第二次结果
console.log(result2.value)//输出:hhc已经吃完了
let result3=it.next("hhc")//最后返回一个最终值,foo执行完毕
console.log(result3.value)//输出:hhc饱了

上面这段代码就不是人读的,可以不用看了,看前两个例子就行懂了就行。
总之就是,第一个it.next()就是触发中断和返回yield后面的值。第二个it.next(“hhc”)就是把"hhc"传入函数中替代掉第一个yield,然后继续执行第一个yield处的代码(但是这里会忽略刚才第一次yield返回的值),直到遇到第二个yield后,再次中断,并返回第二个yield后的值。后面都是类似的,直到结束。

生成器应用

通过生成器实现一个无限数字序列生产者:

例如:斐波那契数列

function* fibo_cycle() {
  let num1 = 1, num2 = 1, sum;
  for (let n = 1; ; n++) {
    if (n == 1 || n == 2) {
      yield 1;
    } else {
      sum = num1 + num2;   //用sum累加前两个数之和
      num1 = num2;
      num2 = sum;
      yield sum
    }
  }
}
let it = fibo_cycle();//用生成器生成一个斐波那契数列迭代器
console.log(it.next().value)//1
console.log(it.next().value)//1
console.log(it.next().value)//2
console.log(it.next().value)//3
console.log(it.next().value)//5
console.log(it.next().value)//8

用for…of…循环对上面代码进行一点点优化:

function* fibo_cycle() {
  let num1 = 1, num2 = 1, sum;
  for (let n = 1; ; n++) {
    if (n == 1 || n == 2) {
      yield 1;
    } else {
      sum = num1 + num2;   //用sum累加前两个数之和
      num1 = num2;
      num2 = sum;
      yield sum
    }
  }
}
let n=0;
//迭代器可以用for..of..循环遍历
for(let v of fibo_cycle()){
  n++;
  console.log(v);
  //用if控制循环,避免死循环
  if(n>=3){
    break;
  }
}

在上面这个例子中,我们发现生成器*fibo_cycle()生成的迭代器实例在循环中遇到break后好像就永远停留在了挂起状态。其实有一个隐藏的功能帮你管理挂起状态的生成器:当for…of…循环中出现异常或结束(通常是由于break、return或者未捕获异常引起的结束)后,会向生成器的迭代器发送一个信号使其终止。即使for…of…会自动发起这个信号,但是你可能希望手动发送这个信号,那你可以通过调用生成器生成的迭代器自带的return()方法实现这一点。(具体请看下面的一个例子)
如果在生成器内有try…finally语句,那么即使生成器已经外部结束,finally里面的代码也会运行。如果需要清理资源的话(数据库连接等),这一点非常有用:

function* fibo_cycle() {
  try {
    let num1 = 1, num2 = 1, sum;
    for (let n = 1; ; n++) {
      if (n == 1 || n == 2) {
        yield 1;
      } else {
        sum = num1 + num2;   //用sum累加前两个数之和
        num1 = num2;
        num2 = sum;
        yield sum
      }
    }
  }
  //清理子句
  finally {
    console.log("关闭数据库连接")
  }
}
let n = 0;
//迭代器可以用for..of..循环遍历
let it=fibo_cycle();
for (let v of it) {
  n++;
  console.log(v);
  //用if控制循环,避免死循环
  if (n >= 3) {
    // 让生成器的迭代器终止运行
    console.log(it.return("生成器生成的迭代器被关闭了").value);
    // 这里不需要break
  }
}
//输出:
// 1
// 1
// 2
// 关闭数据库连接
// 生成器生成的迭代器被关闭了

用生成器模拟多线程的资源竞争问题

当使用生成器操控全局变量或者对象引用时,可以修改原数据。这就会导致生成器生成的一个迭代器修改了数据,然后用生成器生成的另一个迭代器去读,会导致数据在不同的运行情况下,结果不同。

不用Promise实现可控制的异步

缺点:代码耦合度太高,相互依赖,维护成本太高,代码不易懂,不建议使用。

// 首先定义一个异步任务foo()
let z;
function foo(x,y){
  setTimeout(() => {
      z=x+y
      if(isNaN(z)){
        it.throw("运算错误");//调用main返回的迭代器对象,抛出错误
      }else{
        it.next(z);//调用main返回的迭代器对象
      }
  }, 1000);
  return z
}
// 然后定义一个生成器函数*main()来控制foo()
function *main(){
  try{
    let text= yield foo(11,31);
    console.log(text);
  }
  catch(err){
    // 捕获错误
    console.log("捕获错误")
    console.error(err);
  }
}
let it=main()
it.next();//启动异步任务,即:调用foo函数

延伸:生成器中的错误处理

虽然上一段代码不是很好,但是其中有一个知识点在这可以说明一下:生成器内部的yield foo(11,31)语句中的foo()语句如果用throw抛出错误会被生成器内部的try捕获,抛出异常给catch处理。但是setTimeout中的回调函数用throw抛出错误却不会被生成器中的try捕获,而要使用it.throw()方法抛出错误才能被生成器中的try捕获。

function foo(){
  throw new Error("错误")
}

function *main(){
  try{
    let text= yield foo(11,31);
    console.log(text);
  }
  catch(err){
    // 捕获foo中用throw抛出的错误
    console.log("捕获错误");//输出:捕获错误
    console.error(err);//输出:错误
  }
}
let it=main()
it.next();

当然还有其他情况,比如生成器中触发了异常(或者在生成器函数中用throw抛出异常)却没处理,则要在生成器生成的迭代器执行时捕获,例1:

function *main(){
  let x=yield;
  x.getName();
}
let it=main();
try{
  it.next()
  it.next(42)
}catch(err){
  console.log("捕获错误")//输出:捕获错误
  console.error(err)//输出:TypeError: x.getName is not a function
}

例2:

function *errFun(){
  var x=yield;
  throw new Error("错误");
}
let it=errFun();
try{
  it.next()
  it.next(333)
}catch(err){
  console.log("捕获错误")//输出:捕获错误
  console.error(err)//输出:Error: 错误
}

如果用生成器生成的 迭代器 从外部用it.throw()方法向 生成器内部 抛入错误,但生成器内部没有try…catch…捕获错误,那么生成器又会把错误抛出来

function *main(){
  var x=yield;
  //下面的输出代码将不会被执行
  console.log(x)
}

let it=main();
try{
  it.next()
  it.throw("抛入的错误")
}catch(err){
  console.log("捕获错误")//输出:捕获错误
  console.error(err)//输出:抛入的错误
}

生成器+Promise

先看个基础的例子:
我们希望用Promise结合生成器,实现按顺序执行任务的方式

// 建立一个加法函数,加法计算完毕后返回一个成功的Promise
function sum(x,y){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      z=x+y
      resolve(z)
    }, 1000);
  })
}
// 现在我们用生成器实现:按照顺序计算value1、value2、value3的值,并用value3构造出结果并返回
function *main(){
  try{
    let value1 =yield sum(1,1);//先算出value1,把结果抛出去
    let value2 =yield sum(1,2);//再算出value2
    let value3 =yield sum(2,3);//最后算出value3
    let result=value3//用value3构造出result
    return result//把结果result返回回去
  }catch(err){
    console.log(err)
  }

}
//开始执行
let it =main();
let p_value1=it.next();//第一步sum(1,1)开始
p_value1["value"].then((value1)=>{//等待第一步完成,获取sum(1,1)的返回值value1
  let p_value2=it.next(value1)//把value1传入生成器中,并开始第二步sum(1,2)
  p_value2["value"].then((value2)=>{//等待第二步完成,获取sum(1,2)的返回值value2
    let p_value3=it.next(value2)//把value2传入生成器中,并开始第三步sum(2,3)
    p_value3["value"].then((value3)=>{//等待第三步完成,获取sum(2,3)的返回值value3
      let result=it.next(value3)//把value3传入生成器中,并开始第四步用value3构造出result,然后返回result
      console.log(Promise.resolve(result["value"]))//输出:Promise { 5 }
    },(err)=>{
      it.throw(err)
    })
  },(err)=>{
	  it.throw(err)
	})
},(err)=>{
  it.throw(err)
})

用递归函数抽象出Promise流程控制

从上面的例子我们可以看到虽然生成器中很简单,但是外层的Promise流程控制却很困难,但是仔细一点可以发现这个Promise流程控制都是一些重复的代码,而且是一层套一层,所以考虑使用递归函数把Promise流程控制抽象出来。如下:

// 建立一个加法函数,加法计算完毕后返回一个成功的Promise
function sum(x,y){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      z=x+y
      resolve(z)
    }, 1000);
  })
}
// 用生成器实现:按照顺序计算value1、value2、value3的值
function *main(){
  try{
    let value1 =yield sum(1,1);//先算出value1,把结果抛出去
    let value2 =yield sum(1,2);//再算出value2
    let value3 =yield sum(2,3);//最后算出value3
    let result=value3//用value3构造出result
    return result//把结果result返回回去
  }catch(err){
    console.log(err)
  }

}

// 构造一个递归函数,使它代替Promise流程控制,成为生成器函数的执行器
// 它的输入参数generator是一个生成器函数
function run(generator){
  let args=[].slice.call(arguments,1),it;
  it=generator.apply(this,args);
  //返回一个Promise用于生成器完成
  return Promise.resolve().then(function handleNext(value){
    //对下一个yield出的值运行
    let next=it.next(value);
    return (function handleResult(next){
      //生成器运行完毕了吗?
      if (next.done){
        return next.value;
      }
      // 否则继续运行
      else{
        return Promise.resolve(next.value).then(
          //成功就恢复异步循环,把决议值发回生成器
          handleNext,
          //如果value是被拒绝的Promise,就把错误传回生成器进行处理
          function handleErr(err){
            return Promise.resolve(
              it.throw(err)
            ).then(handleResult)
          }
        )
      }
    })(next);
  })
}

run(main).then(data=>{
  console.log(data)//输出:5
})

生成器中的Promise并发

就上一段代码而言,出于性能考虑,它并不是最优的。因为我们可以发现每一个yield传递的值之间并没有依赖关系,就是说value2不用等待value1计算完成才能进行计算。但是在上一段代码中,每个yield必须等待上一个yield值完成才能继续进行下去,也就是说value2虽然不用等待value1计算完成才能进行计算,但是value2却因为有yield的原因而去等待value1计算完成,而这个等待是没有必要的。所以我们把上述代码优化为如下形式:

// 建立一个加法函数,加法计算完毕后返回一个成功的Promise
function sum(x,y){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      z=x+y
      resolve(z)
    }, 1000);
  })
}
// 构造一个递归函数,使它代替Promise流程控制,成为生成器函数的执行器
// 它的输入参数generator是一个生成器函数
function run(generator){
  let args=[].slice.call(arguments,1),it;
  it=generator.apply(this,args);
  //返回一个Promise用于生成器完成
  return Promise.resolve().then(function handleNext(value){
    //对下一个yield出的值运行
    let next=it.next(value);
    return (function handleResult(next){
      //生成器运行完毕了吗?
      if (next.done){
        return next.value;
      }
      // 否则继续运行
      else{
        return Promise.resolve(next.value).then(
          //成功就恢复异步循环,把决议值发回生成器
          handleNext,
          //如果value是被拒绝的Promise,就把错误传回生成器进行处理
          function handleErr(err){
            return Promise.resolve(
              it.throw(err)
            ).then(handleResult)
          }
        )
      }
    })(next);
  })
}
// 用生成器实现:用value1与value2的值计算value3
function *main(){
  try{
    //让两个请求”并行“
    let p1 = sum(1,1);//与p2同时开始计算
    let p2 = sum(1,2);//与p1同时开始计算
    //等待两个Promise决议
    let value1 = yield p1
    let value2 = yield p2
    //用value1和value2计算value3
    let value3 = yield sum(value1,value2);
    let result= value3//用value3构造出result
    return result//把结果result返回回去
  }catch(err){
    console.log(err)
  }

}

run(main).then(data=>{
  console.log(data)//输出:5
})

再用Promise.all([…])优化一下上述代码:

// 建立一个加法函数,加法计算完毕后返回一个成功的Promise
function sum(x,y){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      z=x+y
      resolve(z)
    }, 1000);
  })
}
// 构造一个递归函数,使它代替Promise流程控制,成为生成器函数的执行器
// 它的输入参数generator是一个生成器函数
function run(generator){
  let args=[].slice.call(arguments,1),it;
  it=generator.apply(this,args);
  //返回一个Promise用于生成器完成
  return Promise.resolve().then(function handleNext(value){
    //对下一个yield出的值运行
    let next=it.next(value);
    return (function handleResult(next){
      //生成器运行完毕了吗?
      if (next.done){
        return next.value;
      }
      // 否则继续运行
      else{
        return Promise.resolve(next.value).then(
          //成功就恢复异步循环,把决议值发回生成器
          handleNext,
          //如果value是被拒绝的Promise,就把错误传回生成器进行处理
          function handleErr(err){
            return Promise.resolve(
              it.throw(err)
            ).then(handleResult)
          }
        )
      }
    })(next);
  })
}
// 用生成器实现:用value1与value2的值计算value3
function *main(){
  try{
    //让两个请求”并行“
    let value_list=yield Promise.all([sum(1,1),sum(1,2)])
    let value1 = value_list[0]
    let value2 = value_list[1]
    //用value1和value2计算value3
    let value3 = yield sum(value1,value2);
    let result= value3//用value3构造出result
    return result//把结果result返回回去
  }catch(err){
    console.log(err)
  }
}

run(main).then(data=>{
  console.log(data)//输出:5
})

async、await

就是*和yield+执行器co模块

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值