Promise详解

      Promise是我最喜欢的es6语法,也是面试中最容易问到的部分。那么怎么作到在使用中驾轻就熟,在面试中脱颖而出呢?
      先来个面试题作作:html

面试题:用Promise封装一下原生ajax

      面试官常常会让手写一个Promise封装,写出下面这一版就好了(想了解更多的可自行扩展):java

function ajaxMise(url, method, data, async, timeout) {
    var xhr = new XMLHttpRequest()
    return new Promise(function (resolve, reject) {
        xhr.open(method, url, async);
        xhr.timeout = options.timeout;
        xhr.onloadend = function () {
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304)
                resolve(xhr);
            else
                reject({
                    errorType: 'status_error',
                    xhr: xhr
                })
        }
        xhr.send(data);
        //错误处理
        xhr.onabort = function () {
            reject(new Error({
                errorType: 'abort_error',
                xhr: xhr
            }));
        }
        xhr.ontimeout = function () {
            reject({
                errorType: 'timeout_error',
                xhr: xhr
            });
        }
        xhr.onerror = function () {
            reject({
                errorType: 'onerror',
                xhr: xhr
            })
        }
    })
}
复制代码

Promise简介

      Promise是一个对象,保存着将来将要结束的事件。她有两个特征,引用阮一峰老师的描述就是:react

(1)对象的状态不受外界影响。Promise对象表明一个异步操做,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操做的结果,能够决定当前是哪种状态,任何其余操做都没法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其余手段没法改变。
(2)一旦状态改变,就不会再变,任什么时候候均可以获得这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。若是改变已经发生了,你再对Promise对象添加回调函数,也会当即获得这个结果。这与事件(Event)彻底不一样,事件的特色是,若是你错过了它,再去监听,是得不到结果的。es6

Promise基本用法
let promise1 = new Promise(function (resolve, reject){
    setTimeout(function (){
        resolve('ok') //将这个promise置为成功态(fulfilled),会触发成功的回调
    },1000)
})
promise1.then(fucntion success(val) {
    console.log(val) //一秒以后会打印'ok'
})
复制代码
最简单代码实现一个Promise
class PromiseM {
    constructor (process) {
        this.status = 'pending'
        this.msg = ''
        process(this.resolve.bind(this), this.reject.bind(this))
        return this
    }
    resolve (val) {
        this.status = 'fulfilled'
        this.msg = val
    }
    reject (err) {
        this.status = 'rejected'
        this.msg = err
    }
    then (fufilled, reject) {
        if(this.status === 'fulfilled') {
            fufilled(this.msg)
        }
        if(this.status === 'rejected') {
            reject(this.msg)
        }
    }

}
//测试代码
var mm=new PromiseM(function(resolve,reject){
    resolve('123');
});
mm.then(function(success){
    console.log(success);
},function(){
    console.log('fail!');
});
复制代码

Micro-task / event loop

      上面提到Promise和事件的不一样,除此以外还有一个重要不一样,就是Promise建立是micro-task。再看一道面试题:面试

面试题:写出下面代码的输出顺序

console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');
复制代码

      正确答案是:'script start'、'script end'、'promise1'、'promise2'、'setTimeout'。缘由就是:ajax

  • setTimeout(或者事件)注册的是一个task,由Event Loop控制
  • Promise注册的是一个micro-task

      Event Loop是js的一个重要机制,就是遇到事件或者setTimeout等就会把对应的回调函数放入一个事件队列(task queue),等到主程序执行完毕就依次把队列里的函数压入栈中执行。能够参考阮一峰老师的JavaScript 运行机制详解:再谈Event Loop,不过貌似老师的网站被攻击尚未恢复。
      可是Promise不是上面的机制,她建立的是一个微任务(micro-task),micro-task的执行老是在当前执行栈结束和下一个task执行以前,顺序就是“当前执行栈” -> “micro-task” -> “task queue中取一个回调” -> “micro-task” -> ... (不断消费task queue) -> “micro-task”,总之就是当前执行栈为空时,就到了一个micro-task的检查点。
      下面是micro-task的定义:数据库

Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.
编程

      Promise注册的是micro-task,因此上面题目中:主线程中'script start'、'script end'先打印,而后清空微任务队列,'promise1'、'promise2'打印,而后取出task queue中的回调执行,'setTimeout'打印。后端

为何出现promise

      Promise提供了对js异步编程的新的解决方案,由于咱们一直使用的回调函数实际上是存在很大问题,只是限制于js的单线程等缘由不得不大量书写。固然Promise并非彻底摆脱回调,她只是改变了传递回调的位置。那么传统的回调存在什么问题呢?promise

嵌套

      这里所说的嵌套是指大量的回调函数会使得代码难以读懂和修改,试想一个这个场景:让你把下面的url4的调用提到url2以前。你须要很是当心的剪切代码,而且笨拙的粘贴,result4这个参数你还不敢修改,由于这要额外花费不少功夫而且存在风险。

$.ajax('url1',function success(result1){
    $.ajax('url2',function success(result2){
        $.ajax('url3',function success(result3){
            $.ajax('url4',function success(result4){
                //……
            })
        })
    })
})

复制代码

      固然,上面的问题有点戏剧成分,现实中极少出现这种难搞的状况。与此相比,回调函数带来的思惟上的难以理解是更致命的,由于咱们的大脑更喜欢同步的逻辑,这也是为何await关键字那么受欢迎的缘由。
      我记得有一次我给后端的同窗作JS新特性分享的时候,说到await关键字,有我的惊呼:“哇!这个不错啊,这就能够像写java同样写代码了”。

信任

      除去书写的不优雅和维护的困难之外,回调函数其实还存在信任问题。
      事实上回调函数不必定会像你指望的那样被调用。由于控制权不在你的手上。这种问题被称做“控制反转”。例以下面的例子:

$.ajax('xxxxxx',function success(result1){
    //好比成功以后我会操做数据库记录结算金额
})
复制代码

      上面是jQuery中的ajax调用,咱们指望在某些事件结束后,让第三方(jQ)帮咱们执行个人程序(回调)。
      那么,咱们和第三方之间并无一个契约或者规范能够遵循,除非你把你想使用的第三方库通读一遍,保证它作了你想作的事,但事实上你很难肯定。即便在本身的代码中,或者本身编写的工具,咱们都很难作到百分之百信任。

Promise解决方案

      Promise是一个规范,尝试以一种更加友好的方式书写代码。Promise对象接受一个函数做为参数,函数提供两个参数:

  • resolve:将promise从未完成切换到成功状态,也就是上面提到的从pending切换到fufilled,resolve能够传递参数,下一级promise中的成功函数会接收到它
  • reject:将promise从未完成切换到失败状态,即从pending切换到rejected
let promise1 = new Promise(function(reslove, reject){
    //reslove或者reject或者出错
})
promise1.then(fufilled, rejected).then().then() //这是伪代码
promise1.then(fufilled, rejected)//能够then屡次

function fufilled(data) {
    console.log(data)
}
function rejected(e){
    console.log(e)
}
复制代码

      正如上面提到的两个特征,一旦状态改变,这个Promise就已经完成决议(不会再更改),而且返回一个新的Promise,能够链式调用。而且能够注册多个then方法,他们同时决议而且互不影响。这种设计明显比回调函数要优雅的多,也更易于理解和维护。那么在信任问题上她又有哪些改善呢?
      Promise经过通知的机制将“控制反转”的关系又“反转”回来。回调是我传递给第三方一个函数,指望它在事件发生时帮我执行,而Promise是在你们都遵循规范的前提下,我会在事件发生时获得通知,这时我决定作一些事(执行一些函数)。看到了吧,这是有本质差别的。
      此外,回调函数还有如下信任问题,Promise也都作了相关约束:

  • 回调调用过早
  • 回调调用过晚(或者没有调用)
  • 调用次数太多
  • 没有把参数成功传递给你的回调
  • 吐掉了错误或者异常
过早或者过晚

      一个Promise回调必定会在当前栈执行完毕和下一个异步时机点上调用,即便像下面这样的同步resolve代码也会异步执行,而你传给工具库的回调函数却可能被同步执行(调用过早)或者被忘记执行(或者过晚)。

new Promise(function (resolve) {
    resolve(111111);
})
复制代码
次数太多或者没有传递参数

      Promise只能被决议一次,若是你屡次决议,她只会执行第一次决议,例如:

new Promise(function (reslove, reject) {
    resolve()
    setTimeout(function () {
        resolve(2)
    },1000)
    resolve(3)
}).then(function (val) {
    console.log(val)   //undefined
})
复制代码

      成功回调的参数是经过resolve传递的,例如像上面的代码同样,没有传递参数,那么val收到的会是undefined,因此,不管如何都会收到参数。注意:resolve只接收一个参数,以后的参数会被忽略。

吞掉错误

      Promise的错误处理机制是这样的:若是显示的调用reject并传递错误理由,这个消息会传递给拒绝回调。
      此外,若是任意过程当中出现错误(例如TypeError或者ReferenceError),这个错误会被捕捉,而且使这个Promise拒绝,也就是说这个错误消息也会传递给拒绝回掉,这与传统的回调是不一样的,传统的回调一旦出错会引发同步相应,而不出错则是异步。

promise并发控制

all / race

      allrace两个函数都是并发执行promise的方法,他们的返回值也是promiseall会等全部的promise都决议以后决议,而race是只要有一个决议就会决议。

Promise.all([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
});
复制代码

注意:若是参数为空,all方法会马上决议,而race方法会挂住。

面试题:封装一个promise.all方法
Promise.all = function(ary) {
    let num = 0
    let result = []
    return new Promise(function(reslove, reject){
        ary.forEach(promise => {
            promise.then(function(val){
                if(num >= ary.length){
                    reslove(result)
                }else{
                    result.push(val)
                    num++
                }
            },function(e){
                reject(e)
            })
        })
    })
}

复制代码

thenalbe

如何检测一个对象是Promise?

      你肯能会想到 instanceof Promise,但遗憾的是不能够。缘由是每种环境都封装了本身的Promise,而不是使用原生的ES6 Promise
      因此目前判断Promise的一种方法就是判断它是否是thenable对象(若是它是一个对象或者函数,而且它具备then方法)。
      这是一种js常见的类型检测方法——鸭子类型检测:

鸭子类型检测:若是它看起来像鸭子,叫起来也像鸭子,那么它就是鸭子

resolve/reject

      resolve返回一个当即成功的Promisereject返回一个当即失败的Promise,他们是new Promise的语法糖,因此下面两个写法是等价的:

let p1 = new Promise(function(resolve, reject){
    reslove(11111)
})

let p2 = Promise.resolve(11111) //这和上面的写法结果同样
复制代码

      此外,若是传入reslove方法的参数不是promise而是一个thenable值,那么reslove会将它展开。最终的决议值由then方法来决定。

错误处理

      上面提到,Promise是异步处理错误,也就是说个人错误要在下一个Promise才能捕获到,大多状况这是好的,可是存在一个问题:若是捕获错误的代码再出现错误呢?
      个人作法一般是在代码的最后加catch

let p1 = new Promise(function(reslove, reject){
    ajax('xxxxx')
})

p1
    .then(fullfilled, rejected)
    .then(fullfilled, rejected)
    .catch(function(e){
        //处理错误
    })
复制代码

结尾

      文章到这里就结束了,若是你看完了而且所以思考了一些东西,我很高兴。
      接下来会继续更新Promise+generator、异步函数等Promise相关知识,愿共同进步。

相关文章
相关标签/搜索