自从ES6 Promise成为标准之后,看到了很多文章介绍Promise的用法,然后简单提一下promise的好处,并没有说的很详细,而且也没提Promise的缺点。
因此自己去详细的了解了一下,主要看的文章是《你不知道的Javascript》中关于异步和性能、Promise的部分,自己的理解有些头目。

这期间看了阮一峰的关于异步和循环事件机制的文章,同样也看了朴灵的批注(虽然这个事情引起了广泛关注),但是我觉得行为上没有对错,描述的知识对就是对了,错就是错了,目的是一样的。

印象笔记的分享功能关掉了,因此朴灵的文章我是在360DOC上看到的:

因为我是先看的《你不知道》,因此在看阮一峰的文章的时候,也很懵逼,看了朴灵的批注才明白阮一峰一些内容是存在问题的。

一、事件循环机制和事件循环队列

javascript 是单线程,而本身Javascript 引擎做的就是执行程序中的代码块,而javascript 引擎是运行在宿主环境(浏览器、node等),因此实际上,是javascript的宿主环境实现的事件循环机制,而不是javascript引擎.

javascript引擎需要做的事情就是,执行代码块。

阮一峰文章中说:

同步任务在主线程排队,异步任务不进入主线程,而进入任务队列。

这里就使得我非常的费解,异步本身也是在线程中运行的,成为异步的原因只是当前无法立即拿到结果,需要经过一些操作才能拿到结果,但是在这之前,可以进行其他的操作

宿主环境实现的 事件循环机制如下:

  • 存在一个事件循环队列,该队列中存在等待的事件
  • 存在一个循环(如 while(true))一直在循环事件队列,如果存在等待的事件,则从队列中出队一个事件,并执行

异步是如何做的?

异步中常使用回调函数(回调不是异步必须的),以 setTimeout() 举例,回调函数没有直接回调函数在事件循环队列入队,而是当定时器到了之后才会把会调函数入队。

比如:

function handle() {return false;}
setTimeout(handle, 1000);

上面的定时器中,1S之后才会将 handle 挂入事件循环队列,因此如果当前的事件循环队列中已经存在很多的事件需要处理,则可能无法立即将 handle 出队并执行,因此会导致 setTimeout 定时不准。

而且由于上面的原因,setTimeout() 只可能准时执行或者是延后执行,而不会提前执行。

引用《你不知道》中 Kyle Simpson 的一段话:

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

而且看了诸多文章,我不认同阮一峰的 任务队列的概念,而是觉得 事件循环队列的概念更好

事件循环队列队列最典型的例子就是:

    console.log("A");
    setTimeout(function () {
        console.log("B")
    }, 0);
    console.log("C");

二、任务队列

ES6中有一个新的概念任务队列。 我不确定这个叫法是否是准确的,因为 Kyle Simpson 也说了,只是一个概念,但是这个概念非常的好。

任务队列的概念对于我理解Promise有比较明显的帮助。

首先在事件循环队列队列中,每次循环称为一个tick(tick的概念也是 kyle Simpson提出的),队列有等待事件就出队并执行。而回调函数是插在整个事件循环队列之后的,只有前面所有的事件处理后,才会执行回调函数。

而任务队列是挂在每一个tick之后的,也就是说,回调函数没有挂在整个事件循环队列最后,而是挂在当前的tick后面的任务队列中,因此tick执行后,不会进行下一次事件循环,而是先处理任务队列中的内容。

对比前面的事件循环队列的例子,这里引用《你不知道》关于任务队列的例子:

这个例子是构想的,因为 设想一个调度任务(直接地,不要hack)的API,称其为schedule(..)。

    console.log("A");
    setTimeout(function () {
        console.log("B");
    }, 0);
    // 理论上的"任务API"
    schedule(function () {
        console.log("C");
        schedule(function () {
            console.log("D");
        });
    });

上面的例子会输出 A C D B , 而不是A B C D 。因为 schedule 是理论上的任务API,因此他不会在 "B" 的后面加入事件循环队列,而是在 tick 之后直接跑任务队列。

而 setTimeout() 是加入的事件循环队列的,因此相对于 C D , B 会最后输出。

Promise 是基于任务队列的

三、回调的缺点

用了这么多年回调,缺点自然暴露的很多,促使Promise(不仅仅ES6,很多库早就提供Promise了)发展的原因并不仅仅是一些文章简单的提一下回调地狱,只是写法上的改变这么简单。

下面两点是 Kyle Simpson 关于异步回调的总结,并不是我的总结。

1、 顺序问题

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

难于理解的代码是坏代码,会导致坏bug

2、控制反转

回调会受到控制反转的影响,因为回调暗中把控制权交给第三方(通常是不受你控制的第三方工具!)来调用你代码中的continuation。

这种控制转移导致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。

因此 Promise 首先解决的是 控制反转问题,也就是信任

四、Promise 范式

这里的 Promise 并不是ES6的Promise API 而是概念。

Pormise 最明显的特征就是 不可变性,一旦决议了,就永远保持这个状态,这对解决信任问题很有帮助。

Promise 决议后就是外部不可变的值,我们可以安全地把这个值传递给第三方,并确信它不会被有意无意地修改。特别是对于多方查看同一个Promise决议的情况,尤其如此。
一方不可能影响另一方对Promise 决议的观察结果。不可变性听起来似乎一个学术话题,但实际上这是Promise 设计中最基础和最重要的因素,我们不应该随意忽略这一点。
————《你不知道的javascript》

所以我很认同Promise 是一种封装和组合未来值的易于复用的机制。

五、Promise 的局限性

Promise自然是很好的,但是本身也存在局限性。

1、 异常吞没

如果没有 Promise.catch() ,则如果出现异常,则无法捕获,并且这个异常会沿着 Pormise链 一直传递。

也就是说的 顺序错误处理,但是如果在 catch 的 handler 中也出现错了,则也有问题。

2、单一值

Promise 在进行决议的时候要么有一个 resolved 值, 要么有一个 rejected 的理由,都是单一的。

这个问题到也比较容易解决,比如使用数组或者对象,ES6 的结构赋值对使用数组来说,也非常的方便。

这种情况主要是 如果需要用到的条件是两个Promise的决议值,需要获取这两个Promise的值,可以使用,主要的核心思想还是,每一个Promise一个值。

3、只能决议一次

一个Promise只能决议一次,比如我点击一个按钮,会触发一个决议,则之后再点击就无法再次 resolve 或者 reject 这个Promise 了,因为已经决议了。

可以选择的解决方式是,每次点击,都返回一个新的 Promise。

4、无法取消

一个Promise设立后,是无法人为干预并且取消的。(一些库实现了)

人为取消,会影响Promise的信任问题

上面是我想要了解的几个缺点,至于性能什么的,不是主要问题。

五、ES6 Promise

ES6 提供了 Promise 的 API,但是API比较少,主要是下面的:

Promise.prototype.then()
Promise.prototype.catch()
Promise.all()
Promise.race()
Promise.resolve()
Promise.reject()

Promise API的介绍和使用,网上文章太多了,就不赘述,可以看一下阮一峰的文章:

u=1487385531,1597092167&fm=27&gp=0.jpg