资讯专栏INFORMATION COLUMN

异步流程之Promise解析

luoyibu / 1662人阅读

摘要:采用链式的,可以指定一组按照次序调用的回调函数。异步操作成功异步操作成功上面代码中,第一个方法指定的回调函数,返回的是另一个对象。这时,第二个方法指定的回调函数,就会等待这个新的对象状态发生变化。方法是的别名,用于指定发生错误时的回调函数。

好久没有更新文章了,最近刚好遇到考试,而且一直在做数据库课设。

本来这篇文章是上个星期想要分享给工作室的师弟师妹们的,结果因为考试就落下了。

其实我并不是很想写Promise,毕竟现在更好的方式是结合await/asyncPromise编写异步代码。但是,其实觉得Promise这个东西对于入门ES6,改善“回调地狱”有很大的帮助,那也算是回过头来复习一下吧。

本文很多地方参考了阮一峰的《ES6标准入门》这一本书,因为学ES6,这本书是最好的,没有之一。当然,整理的文章也有我自己的思路在,还有加上了自己的一些理解,适合入门ES6的小伙伴学习。

如果已经对Promise有一定的了解,但并没有实际的用过,那么可以看一下在实例中使用如何更加优雅的使用Promise一节。

另外,本文中有三个例子涉及“事件循环和任务队列”(均已在代码头部标出),如果暂时不能理解,可以先学完Promise之后去了解最后一节的知识,然后再回来看,这样小伙伴你应该就豁然开朗了。

引言 回调函数

所谓回调,就是“回来调用”,这里拿知乎上“常溪玲”一个很形象的例子: “ 你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。”

至于回调函数的官方定义是什么,这里就不展开了,毕竟和我们本篇文章关系不大。有兴趣的小伙伴可以去知乎搜一下。

不友好的“回调地狱”

写过node代码的小伙伴一定会遇到这样的一个调用方式,比如下面mysql数据库的查询语句:

connection.query(sql1, (err, result) => { //ES6箭头函数
    //第一次查询
    if(err) {
        console.err(err);
    } else {
        connection.query(sql2, (err, result) => {
            //第二次查询
            if(err) {
                console.err(err);
            } else {
                ...
            }
        };
    }
})

上面的代码大概的意思是,使用mysql数据库进行查询数据,当执行完sql1语句之后,再执行sql2语句。

可见,上面执行sql1语句和sql2语句有一个先后的过程。为了实现先去执行sql1语句再执行sql2语句,我们只能这样简单粗暴的去嵌套调用

如果只有两三步操作还好,那么假如是十步操作或者更多,那代码的结构是不是更加的复杂了而且还难以阅读。

所以,Promise就为了解决这个问题,而出现了。

promise用法

这一部分的内容绝大部分摘抄自《ES6标准入门》一书,如果你已经读过相关Promise的使用方法,那么你大可以快速浏览或直接跳过。

同时,你更需要留意一下catch部分和涉及“事件循环”的三个例子

promise是什么? promise的定义

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理,让开发者不用再关注于时序和底层的结果。Promise的状态具有不受外界影响不可逆两个特点,与译后的“承诺”这个词有着相似的特点。

Promise的三个状态

首先,Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都没有办法改变这个状态。

状态不可逆

其次,状态是不可逆的。也就是说,一旦状态改变,就不会再变成其他的了,往后无论何时,都可以得到这个结果。

对于Promise的状态的改变,只有两种情况:一是pending变成fulfilled,一是pending变成rejected。(注:下文用resolved指代fulfilled

只要这两种情况中的一种发生了,那么状态就被固定下来了,不会再发生改变。

同时,如果改变已经发生了,此时再对Promise对象指定回调函数,那么会立即执行添加的回调函数,返回Promise的状态。这与事件完全不同。事件的状态是瞬时性的,一旦错过,它的状态将不会被保存。此时再去监听,肯定是得不到结果的。

Promise怎么用? promise的基本用法

ES6规定,Promise对象是一个构造函数,用来生成Promise实例。

实例对象

这里,我们先来new一个全新的Promise实例。

const promise = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 异步操作成功*/) {
        resolve(value);
    } else {
        reject(error);                    
    }
});

可以看到,Promise构造函数接受一个匿名函数作为参数,在函数中,又分别接受resolvereject两个参数。这两个参数代表着内置的两个函数。

resovle的作用是,将Promise对象的状态从“未完成(pending)”变为“成功(resolved)”,通常在异步操作成功时调用,并将异步操作的结果,做为它的参数传递出去。

reject的作用是,将Promise对象的状态从“未完成(pending)”变成"失败(rejected)",通常在异步操作失败时调用,并将异步操作的结果,作为参数传递出去。

接收状态的回调

Promise实例生成以后,可以使用then方法指定resolved状态和rejected状态。

//接上“实例对象”的代码
promise.then(function(value) {
    //success
},function(error) {
    //failure
});

可见,then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是promise对象的状态变为rejected时调用。其中,第二个函数是可选的。并不一定要提供。另外,这两个函数都接受Promise对象传出的值作为参数。

下面给出了一个简单的例子:

function timeout(ms) {
    return new Promise((resolve, reject) {
        setTimeout(resolve, ms, "done");                 
    });
}
timeout(100).then(function(value) {
    console.log(value); //done
});

上面的例子,是在100ms之后,把新建的Promise对象由pending状态变为resolved状态,接着触发then方法绑定的回调函数。

另外,Promise在新建的时候就会立即执行,因此我们也可以直接改变Promise的状态。

//涉及“事件循环”例子1
let promise = new Promise(function(resolve, reject) {
    console.log("Promise");
    resolve();
});

promise.then(function() {
    console.log("resolved.");
});

console.log("Hi!");
// Promise
// Hi!
// resolved

上面的代码中,新建了一个Promise实例后立即执行,所以首先输出的是"Promise",仅接着resolve之后,触发then的回调函数,它将在当前脚本所有同步任务执行完了之后才会执行,所以接下来输出的是"Hi!",最后才是"resolved"。(注:这里涉及到JS的任务执行过程和事件循环,如果还不是很了解这个流程可以全部看完后再回过来理解一下这段代码。)

关于Promise的基本用法,就先讲解到这里。

接下来我们来看一下Promise封装的原生方法。

Promise实例上的thencatch

Promise.prototype.then

Promise的原型上有then方法,前面已经提及和体验过,它的作用是为Promise实例添加状态改变时的回调函数。 then的方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

then方法返回的是一个Promise实例,因此可以采用链式写法,也就是说在then后面可以再调用另一个then方法。

const promise = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 异步操作成功*/) {
        resolve(obj);
    } else {
        reject(error);                         
    }
});
promise.then(function(obj) {
    return obj.a;
}).then(function(a) {
    //...
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

也就是说,在Promise中传参有两种方式:

一是实例Promise的时候把参数通过resovle()传递出去。

二是在then方法中通过return返回给后面的then

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

const promise1 = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 异步操作成功*/) {
        resolve("promise1");
    } else {
        reject(error);                   
    }
});
const promise2 = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 异步操作成功*/) {
        resolve("promise2");
    } else {
        reject(error);                   
    }
});
promise1.then(function() {
    return promise2;
}).then(function funcA(result) {
    console.log(result); //"promise2"
}, function funcB(err){
    console.log("rejected: ", err);
});

上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用funcA,如果状态变为rejected,就调用funcB

Promise.prototype.catch

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。

const promise = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 异步操作成功*/) {
        resolve(value);
    } else {
        reject(error);                    
    }
});
promise.then(function(value) {
    //success
},function(error) {
    //failure
});

于是,这段代码等价为:

const promise = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 异步操作成功*/) {
        resolve(value);
    } else {
        reject(error);                         
    }
})
promise.then(function() {
    //success
}).catch(function(err) {
    //failure
})

可见,此时“位置1”中的then里面的两个参数被剥离开来,如果异步操作抛出错误,就会调用catch方法指定的回调函数,处理这个错误。

值得一提的是,现在我们在给rejected状态绑定回调的时候,更倾向于catch的写法,而不使用then方法的第二个参数。这种写法,不仅让Promise看起来更加简洁,更加符合语义逻辑,接近try/catch的写法。更重要的是,Promise对象的错误具有向后传递的性质(书中说“冒泡”我觉得不是很合适,可能会误解),直到错误被捕获为止。

const promise1 = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 异步操作成功*/) {
        resolve("promise1");
    } else {
        reject(error);                   
    }
});
const promise2 = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 异步操作成功*/) {
        resolve("promise2");
    } else {
        reject(error);                   
    }
});
promise1.then(function() {
    return promise2;
}).then(function funcA(result) {
    console.log(result); //"promise2"
}).catch(function(err) {
    console.log(err); //处理错误
})

上面的代码中一共有三个Promise,第一个由promise1产生,另外两个由不同的两个then产生。无论是其中的任何一个抛出错误,都会被最后一个catch捕获。

如果还是对Promise错误向后传递的性质不清楚,那么可以按照下面的代码做一下实验,便可以更加清晰的认知这个特性。

const promise1 = new Promise(function(resolve, reject) {
    //1. 在这里throw("promise1错误"),catch捕获成功
    // ... some code
    if(true) {
        resolve("promise1");
    } else {
        reject(error);                   
    }
});
const promise2 = new Promise(function(resolve, reject) {
    // ... some code
    //2. 在这里throw("promise2错误"),catch捕获成功
    if(true) {
        resolve("promise2");
    } else {
        reject(error);                   
    }
});
promise1.then(function() {
    return promise2;
}).then(function funcA(result) {
    console.log(result); //"promise2"
    //3. 在这里throw("promise3错误"),catch捕获成功
}).catch(function(err) {
    console.log(err); //处理错误
})

以上,分别将1、2、3的位置进行解注释,就能够证明我们以上的结论。

关于catch方法,还有三点需要提及的地方。

Promise中的错误传递是向后传递,并非是嵌套传递,也就是说,嵌套的Promise,外层的catch语句是捕获不到错误的。

const promise1 = new Promise(function(resolve, reject) {
    // ... some code
    if(true) {
        resolve("promise1");
    } else {
        reject(error);                   
    }
});
const promise2 = new Promise(function(resolve, reject) {
    // ... some code
    if(true) {
        resolve("promise2");
    } else {
        reject(error);                   
    }
});
promise1.then(function() {
    promise2.then(function() {
        throw("promise2出错");
    })
}).catch(function(err) {
    console.log(err);
});
//> Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}
//Uncaught (in promise) promise2出错

所以,代码出现了未捕获的错误,这就是为什么我强调说是“向后传递错误而不是冒泡传递错误”。

Promise没有使用catch而抛出未处理的错误。

const someAsyncThing = function() {
    return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2);
    });
};

someAsyncThing().then(function() {
    console.log("everything is great");
});

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

上面代码中,someAsyncThing函数产生的Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2秒之后还是会输出123。这就是说,Promise内部的错误不会影响到Promise外部的代码,通俗的说法就是“Promise会吃掉错误”。

解决的方法就是在then后面接一个catch方法。

涉及到Promise中的异步任务抛出错误的时候。

//涉及“事件循环”例子2
const promise = new Promise(function (resolve, reject){
    resolve("ok");
    setTimeout(function () { 
        throw new Error("test")      
    }, 0);
});
promise.then(function (value) { 
    console.log(value);
}).catch(function(err) {
    console.log(err);
});
// ok
// Uncaught Error: test

可以看到,这里的错误并不会catch捕获,结果就成了一个未捕获的错误。

原因有二:

其一,由于在setTimeout之前已经resolve过了,由于这个时候的Promise状态就变成了resolved,所以它走的应该是then而不是catch,就算后面再抛出错误,由于其状态不可逆的原因,依旧不会抛出错误。也就是下面这种情况:

 const promise = new Promise(function (resolve, reject) {
     resolve("ok");
     throw new Error("test"); //依然不会抛出错误
 });
//...省略

其二,setTimeout是一个异步任务,它是在下一个“事件循环”才执行的。当到了下一个事件循环,此时Promise早已经执行完毕了,此时这个错误并不是在Promise内部抛出了,而是在全局作用域中,于是成了未捕获的错误。(注:这里涉及到JS的任务执行过程和事件循环,如果还不是很了解这个流程可以全部看完后再回过来理解一下这段代码。)

解决的方法就是直接在setTimeout的回调函数中去try/catch

更多的方法 Promise.resolve

这个方法可以把现有的对象转换成一个Promise对象,如下:

const jsPromise = Promise.resolve($.ajax("/whatever.json"));

上面代码把jQuery中生成的deferred对象转换成了一个新的Promise对象。

Promise的参数大致分下面四种:

如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

参数是一个thenable对象。

thenable对象指的是具有then方法的对象,比如下面这个对象。

let thenable = {
    then: function(resolve, reject) {
    resolve(42);
    }
};

Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法,如下:

let thenable = {
    then: function(resolve, reject) {
    resolve(42);
    }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
    console.log(value);  // 42
});

参数不是具有then方法的对象,或根本就不是对象。

如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolved

不带有任何参数。

Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的Promise对象。

//涉及“事件循环”例子3
setTimeout(function () {
    console.log("three");
}, 0);

Promise.resolve().then(function () {
    console.log("two");
});

console.log("one");

// one
// two
// three

上面这个例子,由于Promise算是一个微任务,当第一次事件循环执行完了之后(console.log("one")),会取出任务队列中的所有微任务执行完(Promise.resovle().then),再进行下一次事件循环,也就是之后再执行setTimeout。所以输出的顺序就是onetwothree。(注:这里涉及到JS的任务执行过程和事件循环,如果还不是很了解这个流程可以全部看完后再回过来理解一下这段代码。)

Promise.reject

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected,并立即执行其回调函数。

注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

const thenable = {
    then(resolve, reject) {
        reject("出错了");
    }
};

Promise.reject(thenable)
    .catch(e => {
        console.log(e === thenable)
    });
// true

上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

其他

下面的方法只做简单的介绍,如果需要更详细的了解它,请到传送门处查询相关资料。

Promise.all

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。

const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all方法接受一个数组作为参数,p1p2p3都是Promise实例,如果不是,就会先调用上面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race
const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.race方法接受一个数组作为参数,p1p2p3都是Promise实例,如果不是,就会先调用上面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

Promise.all不同,只要其中有一个实例率先改变状态,p的状态就跟着改变。那么率先改变的Promise实例的返回值,就传递给p的回调函数。

done

Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

它的实现代码相当简单。

Promise.prototype.done = function (onFulfilled, onRejected) {
      this.then(onFulfilled, onRejected)
        .catch(function (reason) {
        // 抛出一个全局错误
        setTimeout( function() { throw reason }, 0);
        });
};

从上面代码可见,done方法的使用,可以像then方法那样用,提供fulfilledrejected状态的回调函数,也可以不提供任何参数。但不管怎样,done都会捕捉到任何可能出现的错误,并向全局抛出。

finally

finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

下面是一个例子,服务器使用Promise处理请求,然后使用finally方法关掉服务器。

server.listen(0)
    .then(function () {
        // run test
    });
    .finally(server.stop);

它的实现也非常的简单。

Promise.prototype.finally = function (callback) {
    let P = this.constructor;
    return this.then( function(value) {
        P.resolve(callback()).then(function() {
            return value;
        });
    }, function(reason) {
        reason => P.resolve(callback()).then(function() {
            throw reason;
        });
    });
};
JQuery的Deferred对象

最初,在低版本的JQuery中,对于回调函数,它的功能是非常弱的。无限“嵌套”回调,编程起来十分不友好。为了改变这个问题,JQuery团队就设计了deferred对象。

它把回调的嵌套调用改写成了链式调用,具体的写法也十分的简单。这里也不详细讲,想了解的小伙伴也可以直接到这个链接去看。传送门

外部修改状态

但是,由于deferred对象它的状态可以在外部被修改到,这样会导致混乱的出现,于是就有了deferred.promise

它是在原来的deferred对象上返回另外一个deferred对象,后者只开放与改变执行状态无关的方法,屏蔽与改变执行状态有关的方法。从而来避免上述提到的外部修改状态的情况。

如果有任何疑问,可以回到传送门一看便知。

值得一提的是,JQuery中的Promise与我们文章讲的Promise并没有关系,只是名字一样罢了。

虽然两者遵循的规范不相同,但是都致力于一件事情,那就是:基于回调函数更好的编程方式。

promise编程结构 返回新Promise

既然我们学了Promise,那么就应该在日常开发中去使用它。

然而,对于初学者来说,在使用Promise的时候,可能会出现嵌套问题。

比如说下面的代码:

var p1 = new Promise(function() {
    if(...) {
        reject(...);
    } else {
        resolve(...);
    }
});
var p2 = new Promise(function() {
    if(...) {
        reject(...);
    } else {
        resolve(...);
    }
});
var p3 = new Promise(function() {
    if(...) {
        reject(...);
    } else {
        resolve(...);
    }
});

p1.then(function(p1_data) {
    p2.then(function(p2_data) {
        // do something with p1_data
        p3.then(fuction(p3_data) {
        // do something with p2_data
            // p4...
        });
    });
});

假如说现在需要p1p2p3按照顺序执行,那么刚入门的小伙伴可能会这样写。

其实也没有错,这里是用了Promise,但是用得并不彻底,依然存在“回调”地狱,没有深入到Promise的核心部分。

那么我们应该怎么样更好的去运用它呢?

回顾一下前面Promise部分,你应该可以得到答案。

下面,看我们修正后的代码。

//同上,省略定义。

p1.then(function(p1_data) {
    return p2; //位置1
}).then(function(p2_data){ //位置2
    return p3;
}).then(function(p3_data){
    return p4;
}).then(function(p4_data){
    //final result
}).catch(function(error){
    //同一处理错误信息
});

可以看到,每次执行完了then方法之后,我们都return了一个新的Promise。那么当新的Promiseresolve之后,那么显而易见的,它会执行跟在它后面的then之中。

也就是说,在p1then方法执行完了之后,现在我们要去执行p2,那么这个时候我们在“位置1”给它return了一个新的Promise,所以此时的代码可以等价为:

p2.then(function(p2_data){ //位置2
    return p3;
}).then(function(p3_data){
    return p4;
}).then(function(p4_data){
    //final result
}).catch(function(error){
    //同一处理错误信息
});

可见,p2resolve之后,就可以被“位置2”的then接收到了。

于是,基于这个结构,我们就可以在开发中去封装出一个Promise供我们来使用。

在实例中使用

刚好最近在做一个mysql的数据库课设,这里就把我如何封装promise给贴出来。

下面的例子,可能有些接口刚接触node的小伙伴会看不懂,那么,我会尽量的做到无死角注释,大家也尽量关注一下封装的过程(注:重点关注标“*”的地方)。

首先是mysql.js封装文件。

var mysql = require("mysql");//引入mysql库
//创建一个连接池,同一个连接池可以同时存在多个连接,连接完成需要释放
var pool = mysql.createPool({ 
  ...//省略连接的配置
});

/**
 * 把mySQL查询功能封装成一个promise
 * @param String sql 
 * @returns Promise
**/
  
var QUERY = (sql) => {
    //注意这里new了一个新的promise(*)
    var connect = new Promise((resolve, reject) => { 
          //创建连接
        pool.getConnection((err, connection) => {
            //下面是状态执行(*)
            if (err) {
                reject(err);//如果创建连接失败的话直接reject(*)
            } else {
                //否则可以进行查询了
                connection.query(sql, (err, results) => {
                    //执行完查询释放连接
                    connection.release();
                    //在查询的时候如果出错直接reject
                    if (err) {
                        reject(err);//(*)
                    } else {
                        //否则成功,把查询的结果resolve出去
                        //然后给后面then去使用
                        resolve(results);//(*)
                    }
                });
            }
        });
    });
    //最后把promise给return出去(*)
    return connect; 
};

module.exports = QUERY; //把封装好的库导出

接下来,去使用我们封装好的查询Promise

假如我们现在想要使用查询功能获取某个数据表的所有数据:

var QUERY = require("mysql"); //把我们写的库给导入
var sql = `SELECT * FROM student`;//sql语句,看不懂直接忽略
//执行查询操作
QUERY(sql).then((results) => { //(*)
    //这里就可以使用查询到的results了
}).catch((err) => {
    //使用catch可以捕获到整条链抛出的错误。(*)
    console.log(err);
})

以上,就是一个实例了。所以以后,如果你想要封装一个Promise来使用,你可以这样来写。

如何更优雅的使用Promise?

那么,现在问题又来了,如果我们现在需要进行很多异步操作(比如Ajax通信),那么如果按照上面的写法,会导致then链条过长。于是,需要我们不停的去return一个新的Promise对象供后面使用。如下:

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL("http://azu.github.io/promises-book/json/comment.json").then(JSON.parse);
        },
        people: function getPeople() {
            return getURL("http://azu.github.io/promises-book/json/people.json").then(JSON.parse);
        }
    };
function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    // [] 用来保存初始化的值 相当于声明results = []
    var pushValue = recordValue.bind(null, []);
    return request.comment() //位置1
        .then(pushValue)
          .then(request.people)
          .then(pushValue); 
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

可以看到,在“位置1”处的代码,return request.comment().then(pushValue).then(request.people).then(pushValue); 使用了三个thennew了两个新的Promise

因此,如果我们将处理内容统一放到数组里,再配合for循环进行处理的话,那么处理内容的增加将不会再带来什么问题。首先我们就使用for循环来完成和前面同样的处理。

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL("http://azu.github.io/promises-book/json/comment.json").then(JSON.parse);
        },
        people: function getPeople() {
            return getURL("http://azu.github.io/promises-book/json/people.json").then(JSON.parse);
        }
    };

前面这一部分是不需要改变的。

function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    // [] 用来保存初始化值
    var pushValue = recordValue.bind(null, []);
    // 返回promise对象的函数的数组
    var tasks = [request.comment, request.people];
    var promise = Promise.resolve();
    // 开始的地方
    for (var i = 0; i < tasks.length; i++) {
        var task = tasks[i];
        promise = promise.then(task).then(pushValue);
    }
    return promise;
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

使用for循环的时候,每次调用then都会返回一个新创建的Promise对象 因此类似promise = promise.then(task).then(pushValue);的代码就是通过不断对promise进行处理,不断的覆盖 promise变量的值,以达到对Promise对象的累积处理效果。 但是这种方法需要promise这个临时变量,从代码质量上来说显得不那么简洁。 如果将这种循环写法改用Array.prototype.reduce的话,那么代码就会变得聪明多了。

于是我们再对main函数进行修改:

function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    var tasks = [request.comment, request.people];
    return tasks.reduce(function (promise, task) {
        return promise.then(task).then(pushValue);
    }, Promise.resolve());
}

(注:Array.prototype.reduce第一个参数执行数组每个值的回调函数,第二个参数是初始值。回调函数中,第一个参数是上一次调用回调返回的值或提供的初始值,第二个是数组中正在处理的元素。)

最后,重写完了整个函数就是:

function sequenceTasks(tasks) {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    return tasks.reduce(function (promise, task) {
        return promise.then(task).then(pushValue);
    }, Promise.resolve());
}
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL("http://azu.github.io/promises-book/json/comment.json").then(JSON.parse);
        },
        people: function getPeople() {
            return getURL("http://azu.github.io/promises-book/json/people.json").then(JSON.parse);
        }
    };
function main() {
    return sequenceTasks([request.comment, request.people]);
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

需要注意的是,在sequenceTasks中传入的应该是返回Promise对象的函数的数组,而不是一个Promise对象,因为一旦返回一个对象的时候,异步任务其实已经是开始执行了。

综上,在写顺序队列的时候,核心思想就是不断的去return新的Promise并进行状态判断 。而至于怎么写,要根据实际情况进行编程。

是回调不好还是嵌套不好?

本质上来说,回调本身没有什么不好的,但是因为回调的存在,使得我们无限的嵌套函数构成了“回调地狱”,这对开发者来说无疑是特别不友好的。而虽然Promise只是回调的语法糖,但是却提供给我们更好的书写方式,解决了回调地狱嵌套的难题。

更多

最后,这里算是一个拓展和学习方向,学习起来有一定的难度。

为什么JavaScript使用异步的方式来处理任务?

由于JavaScript是一种单线程的语言,所谓的单线程就是按照我们书写的代码一样一行一行的执行下来,于是每次只能做一件事。

如果我们不是用异步的方式而用同步的方式去处理任务,假如现在我们有一个网络请求,请求后面是与其无关的一些操作代码。那么当请求发送出去的时候,由于现在执行代码是按部就班的,于是我们就必须等待网络请求的应答之后,我们才能继续往下执行我们的代码。而这个等待,不仅花费了我们很多时间。同时,也阻塞了我们后面的代码。造成了不必要的资源浪费。

于是,当使用异步的方式来处理任务的时候,每次发送请求,JavaScript中的执行栈会把异步操作交给浏览器的webCore内核来处理,然后继续往下执行代码。当主线程的执行栈代码执行完毕之后,就会去检查任务队列中有没有任务需要执行的。

如果有,则取出来到主线程的执行栈中执行,执行完毕后,更新dom,然后再进行一次同样的循环。

而任务队列中任务的添加,则是靠浏览器内核webCore。每次异步操作完成之后,webCore内核就会把相应的回调函数添加到任务队列中。

值得注意的是,任务队列中任务按照任务性质划分为宏任务和微任务。而由于任务类型的不同,可能存在多个类型的任务队列。但是事件循环只能有一个。

所以现在我们把宏任务和微任务考虑进去,第一次执行完脚本的代码(算是一次宏任务),那么就会到任务队列的微任务队列中取出其所有任务放到主线程的执行栈中来执行,执行完毕后,更新dom。下一次事件循环,再从任务队列中取出一个宏任务,然后执行微任务队列中的所有微任务。再循环...

注:第一次执行代码的时候,就已经开始了第一次的事件循环,此时的script同步代码是一个宏任务。

整个过程,也就是下面的这一个图:

常见的异步任务有:网络请求、IO操作、计时器和事件绑定等。

以上,如果你能够看懂我在讲什么,那么说明你真正理解了JS中的异步,如果不懂,那么你需要去了解一下“事件循环、任务队列、宏任务与微任务”,下面是两篇不错的博客,值得学习。

事件循环:http://www.ruanyifeng.com/blo...

对JS异步任务执行的一个总结:http://www.yangzicong.com/art...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89763.html

相关文章

  • promise + yield = 异步流程控制

    摘要:或许你说你之前用过来做异步流程控制。那么作为一个程序好奇猫,你一定剖析过的源码吧,很好奇它怎么使用来控制的同步。 promise + yield = 异步流程控制 异步计算已经成为前后端不阻塞主线程的不二选择,无论是增加性能或是提升用户体验,anyway,这年头谁不用两下并发呢? 既然说到异步那就不得不提 promise 了,这个新的语法糖虽然建立在 callback 之上,但也好歹止...

    SnaiLiu 评论0 收藏0
  • ES6&ES7中的异步Generator函数与异步编程

    摘要:传统的异步方法回调函数事件监听发布订阅之前写过一篇关于的文章,里边写过关于异步的一些概念。内部函数就是的回调函数,函数首先把函数的指针指向函数的下一步方法,如果没有,就把函数传给函数属性,否则直接退出。 Generator函数与异步编程 因为js是单线程语言,所以需要异步编程的存在,要不效率太低会卡死。 传统的异步方法 回调函数 事件监听 发布/订阅 Promise 之前写过一篇关...

    venmos 评论0 收藏0
  • 简单理解Javascript的各种异步流程控制方法

    摘要:所以仅用于简化理解,快速入门,依然需要阅读有深入研究的文章来加深对各种异步流程控制的方法的掌握。 原文地址:http://zodiacg.net/2015/08/javascript-async-control-flow/ 随着ES6标准逐渐成熟,利用Promise和Generator解决回调地狱问题的话题一直很热门。但是对解决流程控制/回调地狱问题的各种工具认识仍然比较麻烦。最近两天...

    makeFoxPlay 评论0 收藏0
  • 关于 ES6 中 Promise 的面试题

    摘要:执行,输出,宏任务执行结束。到此为止,第一轮事件循环结束。参考入门阮一峰系列之我们来聊聊一道关于应用的面试题阿里前端测试题关于中函数的理解与应用这一次,彻底弄懂执行机制一个面试题原生的所有方法介绍附一道应用场景题目异步流程控制 说明 最近在复习 Promise 的知识,所以就做了一些题,这里挑出几道题,大家一起看看吧。 题目一 const promise = new Promise((...

    dreambei 评论0 收藏0
  • js异步从入门到放弃(实践篇) — 常见写法&面试题解析

    摘要:前文该系列下的前几篇文章分别对不同的几种异步方案原理进行解析,本文将介绍一些实际场景和一些常见的面试题。流程调度里比较常见的一种错误是看似串行的写法,可以感受一下这个例子判断以下几种写法的输出结果辨别输出顺序这类题目一般出现在面试题里。 前文 该系列下的前几篇文章分别对不同的几种异步方案原理进行解析,本文将介绍一些实际场景和一些常见的面试题。(积累不太够,后面想到再补) 正文 流程调度...

    Awbeci 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<