Promise()

什么是同步异步

同步:当我们发出了请求,并不会等待响应结果,而是会继续执行后面的代码,响应结果的处理在之后的事件循环中解决。
异步:那么同步的意思,就是等结果出来之后,代码才会继续往下执行。

我们可以用一个两人问答的场景来比喻异步与同步。A向B问了一个问题之后,不等待B的回答,接着问下一个问题,这是异步。A向B问了一个问题之后,然后就笑呵呵的等着B回答,B回答了之后他才会接着问下一个问题,这是同步。
(async/await使用同步的思维,来解决异步的问题)

什么是回调1

理解一个新东西,很有必须去理解下它的概念,因为这是最简洁明了,前人总结的。
回调的概念

A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.
中文意思:回调是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行。
有点绕,好,咱们说大白话。就是 B函数被作为参数传递到A函数里,在A函数执行完后再执行B。

下面咱们看看代码怎么实现回调。

function A(callback){
    console.log("I am A");
    callback();  //调用该函数
}

function B(){
   console.log("I am B");
}

A(B);

这应该是最最简单的回调了,我想大家应该明白回调的释义了吧。
当然,这么简单的同步回调代码是不会用的,现实中用都是相对比较复杂带传参。

同步回调和异步回调

一开始我被回调和异步有点搞晕了。还以为回调就一定是异步的呢。
其实不然,相信上面的A,B函数的例子我们已经明白,回调并不一定就是异步。他们自己并没有直接关系
下面我们可以理解下 同步回调和异步回调。
同步回调
就是上面的A B函数例子,它们就是同步的回调。
异步回调
因为js是单线程的,但是有很多情况的执行步骤(ajax请求远程数据,IO等)是非常耗时的,如果一直单线程的堵塞下去会导致程序的等待时间过长页面失去响应,影响用户体验了。
如何去解决这个问题呢,我们可以这么想。耗时的我们都扔给异步去做,做好了再来通知我们做完了,我们拿到数据继续往下走。

假设有三个函数

f1()
f2()
f3()

但是,f1执行很耗时,而f2需要在f1执行完之后执行。
为了不影响 f3的执行,我们可以把f2写成f1的回调函数。

//最原始的写法-同步写法

f1(); //耗时很长,严重堵塞
f2(); 
f3(); //导致f3执行受到影响


//改进版-异步写法
function f1(callback){
  setTimeout(function () {
    // f1的任务代码
    callback();
  }, 1000);
}

f1(f2); 
f3();

上面的写法是利用setTimeOutf1的逻辑包括起来,模拟javascript中的异步编程。这样的话,f1异步了,不再堵塞f3的执行。
顺道说下,js是单线程的,这里所谓的异步也是伪异步,并不是开了多线程的异步。它是什么原理呢,其实是任务栈,setTimeOut方法的原理是根据后面的定时时间,过了这个定时时间后,将f1加入任务栈,注意仅仅是加入任务栈,并不是放进去就执行,而是根据任务栈里的任务数量来确定的。

什么是回调2

回调的定义
刚开始学习javascript时,对回调函数的理解仅仅停留在知道定义阶段。什么是回调函数? 就是将一个函数作为参数传递给另一个函数,作为参数的这个函数就是回调函数。 至于为什么要用到回调函数?回调函数有什么作用? 当时对这些一无所知! 最近学习node.js涉及到了大量的异步编程,很多地方都需要用到回调函数,所以这两天深入了解了JavaScript的回调函数,下面是我对回调函数的理解。

函数也是对象
想要弄明白js回调函数,首先要清楚函数的规则,在javascript中函数是一个对象,准确的来说函数是用function()构造函数创建的一个function对象,因此我们可以将函数存储在变量中,当然也就可以将存储在变量中的函数作为一个参数传递给另一个函数,这就是回调函数。
举个例子:

var callback = function(arg3) {
    console.log('callback Totle is:' + arg3)
  }

function fn(arg1, arg2, cb) {
  var Total = arg1 + arg2;
  cb(Total);
  console.log('mainFunction Totle is:' + Total)
}

fn(2, 2, callback)   // 调用fn()函数,并传入2, 2, callback作为参数

上面例子中我们将一个匿名函数赋值给变量callback,同时将callback作为参数传递给了fn()函数,这时在函数fn()中callback就是回调函数
同步回调和异步回调
上面的代码执行结果为:

callback Totle is:4
mainFunction Totle is:4

不对啊! 回调函数不是应该在主函数的最后执行吗?
对,很多介绍回调函数的例子讲到这里是就完了,异步回调函数的确是应该在函数的最后执行,不过上面的例子是一个同步回调函数,函数的执行顺序依然自上而下顺序执行。 那么什么是异步回调呢? 我们又怎么实现异步回调呢? 下面我们举两个例子来说明:

示例1:

function f2() {

    console.log('f2 finished') 
}

function f1(cb) {

    setTimeout(cb,1000)        //用setTimeout()模拟耗时操作
    console.log('f1 finished')
}

f1(f2); //得到的结果是 f1 finished ,f2 finished
这里我们用setTimeout()来模拟耗时操作的前提是js中的setTimeout()函数支持异步处理,所以我们得到的结果是 f1 finished ,f2 finished

示例2:

var fs = require("fs");

fs.readFile('input.txt','utf-8', function (err, data) {
    if (err) return console.error(err);
    console.log(data.toString());
});

console.log("程序执行结束!");

程序执行的结果是:

$ node app
程序执行结束!
我们来测试一下异步回调函数

上面例子中我们先创建了一个文件input.txt,里面的内容是:'我们来测试一下异步回调函数'
如果按照同步的思维,程序应该执行fs.readFile,直到文件读完之后才执行后面的console.log("程序执行结束!"); 然而node中的fs.readFile是支持异步处理的,因此程序执行到这儿的时候并不会阻塞,而是继续向后执行,当文件读取完毕之后再自动调用传入的匿名回调函数,因此出现了上面的结果。

什么是同步异步3

回调:
具名回调

function lean(some){
    console.log(some)
}
function we(cb, something){
    something += ' is coll';
    cb(something)
}
we(lean,'Nodejs');
//函数名回调  Nodesj is coll

匿名回调

function we(cb, something){
    something += ' is coll';
    cb(something)
}

we(function(something){
    console.log(something)
},'vuejs')

同步:

var c = 0;
function plus(){    c += 1}
function printIt(){ console.log(c)}
plus();
printIt();
// 1  立即就打印出了1
// 我们先调用plus()函数去累加c,然后在调用printIt()打印出c,这个是同步的过程

异步:

var c = 0;
function plus(){
    setTimeout(function(){
        c += 1
    }, 1000)
}
function printIt(){ console.log(c)}
plus();
printIt();
//0 立即就打印出了0 
// 这里我们也是先调用plus()再调用print(),为什么打印出来的还是0呢?

当执行plus()函数时因为setTimeout是异步的函数,他会在1000后才开始执行,而JavaScript解析器会把这样异步的函数放到消息队列里,继续执行同步的代码(即执行下面的printIt()函数),等同步的代码执行完,在去执行消息队列里的代码。

那如果我们想要PrintIt()函数是在plus()函数后面执行该怎么做呢?可以用回调来解决

var c = 0;
function printIt(){ console.log(c)}

function plus(callback){
    setTimeout(function(){
        c += 1;
        callback()
    }, 1000)
}
plus(printIt);
//1000毫秒后输出1

这就是异步回调。


Promise是什么,先拉出来溜溜

复杂的概念先不讲,我们先简单粗暴地把Promise用一下,有个直观感受。那么第一个问题来了,Promise是什么玩意呢?是一个类?对象?数组?函数?

先别猜了,直接打印出来看看,console.dir(Promise)


image.png

这么一看就明白了,Promise是一个构造函数,自己身上有all , reject , resolve 这几个眼熟的方法,原型上有then , catch等方法,这么说Promise new出来的对象上也是有then , catch方法的咯。

Promise的基本用法1
function runAsync(){
    //因Promise是构造函数,我们先new一他的个实例p
    var p = new Promise(function(resolve, reject){
        console.log('我还是同步的哦1')
        //做一些异步操作
        setTimeout(function(){
           
            resolve('3');
            console.log('4');
        }, 2000);
      console.log('我还是同步的哦2')
    });
    return p;
     //等到异步操作完成后,return出实例p,实例上有then , catch方法            
}
//runAsync();
runAsync().then(function(data){
    console.log(data);
    //后面可以用传过来的数据做些其他操作
    //......
});
//输出顺序:
我还是同步的哦1 
我还是同步的哦2
4
3

//then方法会接受一个参数,即上面resolve()里传的参数。resolve()是处理异步的函数,该函数JavaScript已经定义好,我们可以如上面直接使用
setTimeout()是回调,所以4,3都是在1,2后面输出的(不理解看回调的定义),而异步是在回调里头的,所以会是在最后输出3。

这时候你应该有所领悟了,原来then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。

看到这里你可能会说,把回调函数封装下,给runAsync传进去不是一样?如下

function runAsync(cb){
    //因Promise是构造函数,我们先new一他的个实例p
  
        console.log('我还是同步的哦1')
        //做一些异步操作
        setTimeout(function(){
           
            cb('3');
            console.log('4');
        }, 2000);
      console.log('我还是同步的哦2')
    
     //等到异步操作完成后,return出实例p,实例上有then , catch方法
};
function callback(data){
    console.log(data)
};
runAsync(callback);
//输出顺序:
 我还是同步的哦1
 我还是同步的哦2
3
4

效果也是一样的,还费劲用Promise干嘛。那么问题来了,有多层回调该怎么办?如下代码

Promise的基本用法2
Release: production-20171127052209
web-a44ad2fc4f114b24efcf.js:1 Environment: production
function fn(num) {
    return new Promise(function(resolve, reject) {
        if (typeof num == 'number') {
            resolve();
        } else {
            reject();
        }
    })
    .then(function() {
        console.log('参数是一个number值');
    })
    .then(null, function() {
        console.log('参数不是一个number值');
    })
}

fn('hahha');
fn(1234);

// 参数是一个number值
// 参数不是一个number值

then方法的执行结果也会返回一个Promise对象。因此我们可以进行then的链式执行,这也是解决回调地狱的主要方式。

var fn = function(num) {
    return new Promise(function(resolve, reject) {
        if (typeof num == 'number') {
            resolve(num);
        } else {
            reject('TypeError');
        }
    })
}

fn(2).then(function(num) {
    console.log('first: ' + num);
    return num + 1;
})
.then(function(num) {
    console.log('second: ' + num);
    return num + 1;
})
.then(function(num) {
    console.log('third: ' + num);
    return num + 1;
});

// first: 2
// second: 3
// third: 4

OK,了解了这些基础知识之后,我们再回过头,利用Promise的知识,对最开始的ajax的例子进行一个简单的封装。看看会是什么样子。





链式操作的用法

从表面看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的

function runAsync1(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务1执行完成');
            resolve('随便什么数据1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务2执行完成');
            resolve('随便什么数据2');
        }, 2000);
    });
    return p;            
}
function runAsync3(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务3执行完成');
            resolve('随便什么数据3');
        }, 2000);
    });
    return p;            
}

如何做到能够按顺序,每个2秒输出每个异步回调中的内容,在runAsync1中传为resolve的数据,能在接下来的then方法中拿到。如下

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return runAsync3();
})
.then(function(data){
    console.log(data);
});
//输出结果:
异步任务1执行完成
随便什么数据1
异步任务2执行完成
随便什么数据2
异步任务3执行完成
随便什么数据3
reject的用法

事实上,我们前面的例子都是只有“执行成功”的回调,还没有“失败”的情况,reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调。看下面的代码。

function getNumber(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            var num = Math.ceil(Math.random()*10); //生成1-10的随机数
            if(num<=5){
                resolve(num);
            }
            else{
                reject('数字太大了');
            }
        }, 2000);
    });
    return p;            
}

getNumber()
.then(
    function(data){
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
        console.log('rejected');
        console.log(reason);
    }
);

getNumber函数来异步获取一个数字,2秒后执行完成,如果数字小于等于5,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为失败的原因。

运行getNumber并且在then中传递了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到下面两种结果:
“resolved 1” 或者 “rejected 数字太大了”

catch的用法

我们知道Promise对象除了then方法,还有一个catch方法,他是做什么的呢?其实他很then的第二个参数一样,用来指定reject的回调,用法如下:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
    console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:

resolved
4
rejected
ReferenceError: somedata is not defined(...)

也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。

all的用法

Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用上面定义好的runAsync1、runAsync2、runAsync3这三个函数,看下面的例子:

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

用Promise.all来执行,all接收一个数组参数,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。那么,三个异步操作返回的数据哪里去了呢?都在then里面呢,all会把所有异步操作的结果放进一个数组中传给then,就是上面的results。所以上面代码的输出结果就是:


image.png

有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

race的用法

all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」,这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,我们把上面runAsync1的延时改为1秒来看一下:

Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

这三个异步操作同样是并行执行的。结果你应该可以猜到,1秒后runAsync1已经执行完了,此时then里面的就执行了。结果是这样的:


image.png

在then里面的回调开始执行时,runAsync2()和runAsync3()并没有停止,仍旧再执行。于是再过1秒后,输出了他们结束的标志。

这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:

//请求某个图片资源
function requestImg(){
    var p = new Promise(function(resolve, reject){
        var img = new Image();
        img.onload = function(){
            resolve(img);
        }
        img.src = 'xxxxxx';
    });
    return p;
}

//延时函数,用于给请求计时
function timeout(){
    var p = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('图片请求超时');
        }, 5000);
    });
    return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
    console.log(results);
})
.catch(function(reason){
    console.log(reason);
});

requestImg函数会异步请求一张图片,我把地址写为"xxxxxx",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。运行结果如下:


image.png

参考链接:

  1. 关于js的回调、同步回调、异步回调
  2. 大白话讲解Promise(一)
  3. 大白话讲解Promise(二)理解Promise规范
  4. 大白话讲解Promise(三)搞懂jquery中的Promise
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容