ES6 promise时序,本质,常见错误

Posted 小章鱼哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES6 promise时序,本质,常见错误相关的知识,希望对你有一定的参考价值。

ES6 promise时序,本质,常见错误

过年好!

最近在工作中使用promise遇到了这样的问题:

promise的时序问题。


目录


一个关于时序的问题

const p = new Promise(

    (resolve,reject) => 
        console.log(1);
        resolve(2);
        console.log(3);

    


);

console.log(4);

p.then(
    result => console.log(result);
)

console.log(5);

代码的在控制台的输出顺序是什么?

1 3 4 5 2

为什么?

事件循环

javascript 引擎并不是独立运行的,它运行在宿主环境中,对多数开发者来说通常就是 Web浏览器。

经过最近几年(不仅于此)的发展,JavaScript已经超出了浏览器的范围, 进入了其他环境,比如通过像Node.js 这样的工具进入服务器领域。实际上,JavaScript 现如今已经嵌入到了从机器人到电灯泡等各种各样的设备中。

但是,所有这些环境都有一个共同“点”(thread,也指线程。不论真假与否,这都不算一个很精妙的异步笑话),即它们都提供一种机制来处理程序中多个块的执行,且执行每块时调用JavaScript 引擎,这种机制被称为事件循环。

换句话说,JavaScript 引擎本身并没有时间的概念,只是一个按需执行 JavaScript任意代码 片段的环境。“事件”(JavaScript代码执行)调度总是由包含它的环境进行。

所以,举例来说,如果你的 JavaScript 程序发出一个 Ajax请求,从服务器获取一些数据,那你就在一个函数(通常称为回调函数)中设置好响应代码,然后 JavaScript 引擎会通知 宿主环境:“嘿,现在我要暂停执行,你一旦完成网络请求,拿到了数据,就请调用这个 函数。”

然后浏览器就会设置侦听来自网络的响应,拿到要给你的数据之后,就会把回调函数插入到事件循环,以此实现对这个回调的调度执行。

// eventLoop是一个用作队列的数组 //(先进,先出)
var eventLoop = [ ];
var event;
//“永远”执行 
while (true) 
    // 一次tick
    if (eventLoop.length > 0) 
        // 拿到队列中的下一个事件
        event = eventLoop.shift();
        // 现在,执行下一个事件 
        try 
            event(); 
        
        catch (err) 
            reportError(err);
        
    

这当然是一段极度简化的伪代码,只用来说明概念。不过它应该足以用来帮助大家有更好的理解。

你可以看到,有一个用 while循环实现的持续运行的循环,循环的每一轮称为一个 tick。 对每个 tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这些事件就是你的回调函数。

一定要清楚,setTimeout(..)并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来 某个时刻的 tick会摘下并执行这个回调。

如果这时候事件循环中已经有 20 个项目了会怎样呢?你的回调就会等待。它得排在其他项目后面——通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么 setTimeout(..) 定时器的精度可能不高。大体说来,只能确保你的回调函数不会在指定的时间间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的 状态而定。

所以换句话说就是,程序通常分成了很多小块,在事件循环队列中一个接一个地执行。严格地说,和你的程序不直接相关的其他事件也可能会插入到队列中。

但是

ES6 精确指定了事件循环的工作细节,这意味着在技术上将其纳入了 JavaScript 引擎的势力范围,而不是只由宿主环境来管理。这个改变的一个主要原因是 ES6 中 Promise 的引入,因为这项技术要求对事件循环队列的调度运行能够直接进行精细控制。

ES6 promise的本质区别

回忆一下,我们用回调函数来封装程序中的continuation,然后把回调交给第三方(甚至可能是外部代码),接着期待其能够调用回调,实现正确的功能。

通过这种形式,我们要表达的意思是:“这是将来要做的事情,要在当前的步骤完成之后发生。”

但是,如果我们能够把控制反转再反转回来,会怎样呢?如果我们不把自己程序的continuation传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么,那将会怎样呢?

这种范式就称为 Promise。

——–以上引用全部来自《你不知道的javascript》中卷

举个例子吧。

1. getA回调getC

function getA(data,callback) 
  console.log('a');
  setTimeout(() => 
    callback();
  , 0);



setTimeout(() => 
console.log('d');
, 0);

getA('aaa', getC);


function getC() 
  console.log('c');


控制台的打印顺序是: a d c。

settimeoutgetAgetC是一个个代码片段。
在事件循环中放在任务队列中被执行。

一个浏览器环境,只能有一个事件循环,但是任务队列可以有多个。而一个事件循环可以多个任务队列。

所以这段代码内部执行的过程是这样子的:

  • 代码开始执行时,所有这些代码在macrotask queue中,取出来执行之。
  • 执行 macrotask queue 中的第一个任务,setTimeout是异步任务,生成新的macrotask queue1号,getA是同步任务,执行之,所以打印出a
  • getA中的setTimeout是异步任务,创建macrotask queue2号。
  • 取出macrotask queue1号,执行之,打印出d
  • 取出macrotask queue2号,执行之,打印出c

2. getA封装成promise方法,then的时候getC

new Promise((resolve) => 
  console.log('a');
  resolve(true);
).then(getC);

setTimeout(() => 
console.log('d');
, 0);

function getC() 
  console.log('c');


控制台的打印顺序是: a c d。

前文提到,整个script代码,放在了macrotask queue中,setTimeout也放入macrotask queue
但是
promise.then放到了另一个任务队列microtask queue中。这两个任务队列执行顺序如下,取1个macrotask queue中的task,执行之。然后把所有microtask queue顺序执行完,再取macrotask queue中的下一个任务。

所以这段代码内部执行的过程是这样子的:

  • 代码开始执行时,所有这些代码在macrotask queue中,取出来执行之。
  • 执行 macrotask queue中的第一个任务,由于promise实例是同步任务执行之,所以打印出a
  • setTimeout被推入macrotask queue1号,promise.then被推入microtask queue1号。
  • 由于在第一步中已经执行完了第一个 macrotask , 所以接下来会顺序执行所有的 microtask, 也就是promise.then 的回调函数,从而打印出c
  • 取出macrotask queue1号,执行之,打印出d

3. mix

setTimeout(() => 
  console.log(4);
, 0);
new Promise(
  (resolve) => 
    console.log(1);
    for (let i = 0; i < 10000; i++) 
      i === 9999 && resolve();
    
    console.log(2);
  
).then(() => 
  console.log(5);
).then(() => 
  console.log(6);
);
console.log(3);

这个就很明朗了。

输出结果为:1 2 3 5 4。

setTimeout 是属于 macrotask 的,而整个 script 也是属于一个 macrotask, promise.then回调 是 microtask 。

执行过程大概如下:

  1. 由于整个 script 也属于一个 macrotask, 由于会先执行macrotask 中的第一个任务,再加上promise构造函数因为是同步的,所以会先打印出 1和2。

  2. 然后继续同步执行末尾的 console.log(3) 打印出3。

  3. 此时 setTimeout 被推进到 macrotask 队列中, promise.then 回调被推进到microtask队列中由于在第一步中已经执行完了第一个 macrotask , 所以接下来会顺序执行所有的 microtask, 也就是 promise.then 的回调函数,从而打印出5。

  4. microtask 队列中的任务已经执行完毕,继续执行剩下的 macrotask 队列中的任务,也就是 setTimeout, 所以打印出4。

——–以上引用全部来自知乎https://www.zhihu.com/question/36972010


ES6 promise的常见错误

有没有平时遇到的业务逻辑复杂的情况时,对于promise的书写有些无从下手。

下面列出一些常见的应用场景的错误写法和推荐写法。

1. promise & forEach

错误代码:

array.forEach(function(element) 
    return developer.getResources(element)
        .then((data) = > 
            name = data.items[0];
            return developer.getResourceContent(element, file);
        )
        .then((response) = > 
            fileContent = atob(response.content);
            self.files.push(
                fileName: fileName,
                fileType: fileType,
                content: fileContent
            );
            self.resultingFunction(self.files)
        ).catch ((error) = > 
            console.log('Error: ', error);
        )
);

更优的代码:

var promises = array.map(function(element) 
      return developer.getResources(element)
          .then((data) = > 
              name = data.items[0];
              return developer.getResourceContent(element, file);
          )
          .then((response) = > 
              fileContent = atob(response.content);
              self.files.push(
                  fileName: fileName,
                  fileType: fileType,
                  content: fileContent
              );
          ).catch ((error) = > 
              console.log('Error: ', error);
          )
);

Promise.all(promises).then(() => 
    self.resultingFunction(self.files)
);

promise.all替代forEach

参考:

《你不知道的javascript》中卷
https://www.zhihu.com/question/36972010
https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
https://stackoverflow.com/questions/38362231/how-to-use-promise-in-foreach-loop-of-array-to-populate-an-object

以上是关于ES6 promise时序,本质,常见错误的主要内容,如果未能解决你的问题,请参考以下文章

ES6 Promise.all() 错误句柄 - 是不是需要 .settle()? [复制]

ES6之Promise封装ajax()

处理 Promise 中的错误 - Es6

防止 ES6 Promise 吞下错误(不使用 .catch)

ES6深入浅出-9 Promise-3.Promise的细节

ES6 很可能会出现简单的 promise 错误