Promise对象——JavaScript

JavaScript的异步执行

概述:

JavaScript语言是单线程的,为了解决排队等待的现象,JavaScript将任务的执行模式分成了同步和异步。
同步模式:传统的做法,后一个任务等待前一个任务结束,然后再执行,执行的执行顺序与任务的排列顺序是一致的、同步的。这往往用于一些简单的、快速的、不涉及IO读写的操作。
异步模式:每一个任务分成两段,第一段代码包含对外部数据的请求,第二段代码被写成一个回调函数,包含了对外部数据的处理。第一段代码执行完,不是立即执行第二段代码,而是将程序的执行权交给第二个任务。等到外部数据反悔了,再由系统通知执行第二段代码。所以,程序的执行的顺序与任务的执行顺序是不一致的、异步的。

1)回调函数:

回调函数时异步编程最基本的方法。
假定有两个函数f1和f2,后者必须等到前者执行完成,才能执行。这是,可以考虑改写f1,把F2写成f1的回调函数

function f1(callback) {
    //f1的代码
    
    //f1执行完成后,调用回调函数
    callback();
}

执行代码就相当于:

f1(f2);

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪,而且每个任务只能指定一个回调函数。

2) 事件监听

另一种思路就是采用事件驱动的模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
还是以f1和f2威力。首先为f1绑定一个事件

f1.on('done',f2);

上面这行代码的意思是,当f1发生done事件,就执行f2,然后,对f1进行改写:

function f1() {
    setTimeout(function() {
        //f1的任务代码
        f1.trigger('done');
    },1000);
}

上面代码中,f1.trigger('done')表示,执行完成后,立即触发"done"事件,从而开始执行f2.
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”,有利于实现模块化,缺点就是整个程序都会变成事件驱动型,运行流程会变得很不清晰。

发布/订阅

"事件"可以理解成为“信号”,如果存在一个“信号中心”,某个任务执行完成,就像信号中心“发布”一个信号,其他任务可以向信号中心"订阅"这个信号,从而知道什么时候自己可以开始执行。这就叫作“发布/订阅模式”,,又称“观察者模式”

首先,f2向“信号中心”jquery订阅“done”信号。

jQuery.subscribe("done", f2);

然后,f1进行如下改写:

function f1(){
    setTimeout(function() {
        //f1的任务代码
        jQuery.publish("done");
    },1000)
}

jQuery.publish("done")的意思是,f1执行完成后,向“信号中心”jQuery发布“done”信号,从而引发f2的执行。
f2完成执行后,也可以取消订阅(unsubscribe).

jQuery.unsubscribe("done", f2);

这种方法的性质与“事件监听”类似,但是明显优于后者,因为我们可以通过查看“消息中心”,了解存在多少信号,每个信号有多少订阅者,从而监视程序的运行。


异步操作的流程控制

如果有多个异步操作,就存在一个流程控制的问题:确定操作执行的顺序,以后如何保证遵守这种顺序。

function async(arg, callback) {
    console.log("参数为 " + arg + "  , 1秒后返回结果");
    setTimeout(function() {
        callback(arg * 2);
    }, 1000);
}

上面的代码的async函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函数。
如果有6个这样的异步任务,需要全部完成后,才能执行下一步的final函数。

function final(value) {
    console.log('完成: ', value);
}

请问该如何安排操作流程?

async(1, function(value) {
    async(value, function(value) {
        async(value,function(value) {
           async(value, function(value) {
               async(value, function(value) {
                    async(value, final);
               }); 
           });
        });
    });
});

上面代码采用6个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护.

串行执行

我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个,这就叫做串行执行。
 

var items = [1, 2, 3, 4, 5, 6];
var result = [];
function series(item) {
    if(item) {
        async( item, function(result) {
            result.push(result);
            return series(item.shift());
        });
    } else {
        return final(results);
    }
}
series(items.shift());

上面代码中,函数series就是串行函数,它会一次执行异步任务,所有任务都执行完后,才会执行final函数。items数组保存每一个异步任务的参数,result数组保持每一个异步任务的执行结果。

并行执行

流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。

var items = [1, 2, 3, 4, 5, 6];
var result = [];

items.forEach(function(item) {
    async(item, function(result){
        results.push(result);
        if(results.length == items.length) {
            final(results);
        }
    })
});

上面代码中,forEach方法会同时发起6个异步任务,等到他们全部完成以后,才会执行final函数。
并行执行的好处是效率较高,比起串行执行一次只能执行一个任务,较为节约时间。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度,因此有了第三种流程控制方式。

并行与串行的结合

所谓并行与串行的结合,就是设置一个门槛,每次最多只能执行n个异步任务,这样就避免了过分占用系统资源。

var items = [1, 2, 3, 4, 5, 6];
var results = [];
var running = 0;
var limit = 2;

function launcher() {
    while(running < limit && items.length > 0) {
        var item = items.shift();
        async(item, function(result) {
            results.push(result);
            running--;
            if(items.length > 0) {
                launcher();
            } else if(running == 0) {
                final();
            }
        });
        running++;
    }
}

上面代码中,最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示若有任务都执行完了,这时就执行final函数。

Promise对象

简介

promise对象是一种规范,目的是为异步操作提供统一接口。
它是一个对象,也就是说与其他JavaScript对象的用法,没有什么两样;其次,它起到代理作用,充当异步操作与回调函数之间的中介。它是的异步操作具备同步操作的接口,使得程序具有正常的同步运行的流程,回调函数不必再一层层嵌套。
简单来说,他的思路是,等一个异步任务立即返回一个Promise对象,由于是立即返回,所以可采用同步操作的流程。这个promise对象有一个then方法,允许指定回调函数,在异步任务完成后调用。

比如,异步操作f1返回一个Promise对象,它的回调函数f2写法如下:

(new Promise(f1)).then(f2);

这种写法对于多层嵌套的回调函数尤其方便

//传统写法
setp1(function(value1) {
    setp2(function(value2) {
        setp3(function(value3) {
            //....
        }); 
    });
});

//promise的写法
(new Promise(step1))
    .then(step2)
    .then(step3)
    .then(step4);

从上面代码可以看出,采用promise接口以后,程序流程可以变得非常清楚,十分易读。注意,为了便于理解,上面代码的Promise规范就是为了解决这个问题而提出的,目的是使用正常的程序流程,来处理异步操作。它先返回一个Promise对象,后面的操作以同步的方式,寄存在这个对象上面。等到异步操作有了结果,在执行前期寄放在它上面的其他操作。

Promise接口

promise接口的基本思想是让异步任务返回一个Promise对象。
Promise对象只有三种状态。

  • 异步操作“未完成”(pending)
  • 异步操作“已完成”(resolve,又称fulfilled)
  • 异步操作“失败”(rejected)

这三种的状态的变化途径只有2中。

  • 异步操作从“未完成”到“已完成”
  • 异步操作从“未完成”到“失败”

这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,promise对象的最终结果只有两种。

  • 异步操作成功,promise对象传回一个值,状态变为resolved
  • 异步操作失败,promise对象抛出一个错误,状态变为rejected

Promise对象使用then方法添加回调函数。then方法可以接受两个回调函数,第一个是异步操作成功时(变为resolved状太)时的回调函数。第二个是异步操作失败(变为rejected)时的回调函数(可以省略)。一旦状态改变,就调用响应的回调函数。

//po是一个Promise对象
po.then(
    console.log,
    console.error
);

上面代码中,Promise对象po使用then方法绑定两个回调函数:操作成功时的回调函数console.log,操作失败时的回调函数console.error(/可以省略)。这两个函数都接受异步操作传回的值作为参数。
then方法可以链式使用

po    
    .then(step1)
    .then(step2)
    .then(step3)
    .then(
        console.log,
        console.error
    );

上面代码中,po的状态一旦变为resolved,就依次调用后面每一个then指定的回调函数,每一步都必须等到前一步完成,才会执行。最后一个then方法的回调函数console.log和console.error,用法上有一点重要的区别。console.log只显示回调函数step3的返回值,而console.error可以显示step1、step2、step3之中任意一个发生的错误。也就是说,假定step1操作失败,抛出一个错误,这时,step2和step3都不会再执行了。promise对象开始寻找,接下来第一个操作失败时的回调函数,在上面代码中是console.error.这就是说,promise对象的错误有传递性。

从同步的角度看,上面的代码大致等同于下面的形式

try {
    var v1 = step1(p0);
    var v2 = step2(v1);
    var v3 = step3(v2);
    console.log(v3);
} catch (error) {
    console.error(error);
}

Promise对象的生成

ES6提供了原生的promise构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例

var promise = new Promise(function(resolve, reject) {
    //异步操作的代码
    
    if(/* 异步操作成功 */) {
        resolve(value);
    } else {
        reject(error);
    }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。他们是两个函数,由JavaScript引擎提供,不用自己部署。resolve函数的作用是,将promise对象的状态从“未完成”变为“成功”(即从pending变为resolve),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去,reject函数的作用是,将promise对象的状态从”未完成“变为”失败“(即从pending变为rejected),在异步操作失败是调用,并将异步操作报出的错误,作为参数传递出去。
promise实例生成以后,可以用then方法分别指定resolved状态和reject状态的回调函数。

po.then(function(value) {
    //success
}, function(value) {
    //failure
})

用法辨析

promise的用法,简单来说就是一句话:使用then方法添加回调函数。但是不同的写法有一些细微的差别,下面有四种写法,并有详细的解析

//写法1
doSomething().then(function() {
    return doSomethingElse();
});

//写法2
doSomething().then(function() {
    doSomethingElse();
});

//写法3
doSomething().then(doSomethingElse());

//写法4
doSomething().then(doSomethingElse);

为了便于讲解,下面这四种写法都用then接一个回调函数finalHandler。写法一的finalHandler回调函数的参数,是doSomethingElse函数的运行结果。

doSomething().then(function() {
    return doSomethingElse();
}).then(finalHandler);

写法2的finalHandler回调函数的参数是undefined

doSomething().then(function() {
    doSomethingElse();
    return;
}).then(finalHandler);

写法3的finalHandler回调函数的参数,是doSomethingElse函数返回的回调函数的运行结果。

doSomething().then(doSomethingElse())
    .then(finalHandler);

写法4与写法一只有一个差别,那就是doSomethingElse会接收到doSomething()返回的结果

doSomething().then(doSomethingElse)
    .then(finalHandler);

 Promise的应用

加载图片:

我们可以把图片的加载写成一个Promise对象。

var preloadImage = function (path){
    return new Promise(function (resolve, reject) {
        var image = new Image();
        image.onload = resolve;
        image.onerror = reject;
        image.src = path;
    });
};

Ajax操作:

ajax操作是典型的异步操作,传统上往往写成下面这样

function search(term, onload, onerror) {
    var xhr, result, url;
    url = 'http://example.com/search?' + term;
    
    xhr = new XMLHttpRequset();
    xhr.open('GET',url, true);

    xhr.onload = function(e) {
        if (this.status === 200) {
            results = JSON.parse(this.responseText);
            onload(results);
        }
    };
    xhr.onerror = function(e) {
        onerror(e);
    };

    xhr.send();
}
search("hello world", console.log, console.error);

如果是用promise对象,就可以写成下面这样:

function search(term) {
    var url = 'http://example.com/search?1=' + term;
    var xhr = new XMLHttpRequest();
    var result;
    
    var p = new Promise(function(resolve, reject) {
        xhr.open('GET', url, true);
        xhr.onload = function (e) {
            if(this.status == 200) {
                reusult = JSON.parse(this.responseText);
                resolve(result);    
            }
        };
        xhr.onerror = function(e) {
            reject(e);
        }; 
    });

    return p; 
}

search("Hello world").then)(console.log, console.error);

加载图片的例子,也可以用ajax来完成

function imgLoad(url) {
    return new Promise(function(resolve, reject) {
        var request = new XMLHttpRequest();
        request.open('GET', url);
        request.responseType = 'blob';
        request.onload = function() {
            if (request.status == 200) {
                resolve(request.response);
            } else {
                reject(new Error('图片加载失败: ' + request.statusText));
            }
        };
       request.onerror = function() {
            reject(new Error('发生网络错误'));
        };
        request.send();
    });
}

小结:

promise对象的有点在于,让回调函数变成了规范的链式写法,程序流程可以看的清楚。他的一整套接口,可以实现许多强大的功能,比如为多个异步操作部署一个回调函数,为多个回调函数中抛出的错误统一指定处理方法等。

而且,他还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以不用担心错过了某个事件或语句。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值