Promise

讲述Promise之前,先向大家推荐一个不错的关于Promise的学习资料JavaScript Promise迷你书(中文版)

var promise = new Promise(function(resolve, reject) {
    resolve("示例");
});
promise.then(function(value) {
    console.log(value);
}, function(error) {
    console.log(error);
});

一、什么是Promise

Promise是抽象异步处理对象以及对其进行各种操作的组件。

1. 回调函数的异步处理

getAsync("fileA.txt", function(error, result){
    if(error){
        // 取得失败时的处理
        throw error;
    }
    // 取得成功时的处理
});

注意:Node.js等规定在JavaScript的回调函数的第一个参数为 Error 对象,这也是它的一个惯例。

2. Promise简介

var promise = new Promise(function(resolve, reject): {
    // 异步处理
    // 处理结束后、调用resolve(成功) 或 reject(失败)
});
promise.then(onFulfilled, onRejected)

(1)new Promise(function(resolve, reject):Promise构造器,返回一个promise对象 (2)resolve(成功):调用 onFulfilled (3)reject(失败):调用 onRejected 补充:Pending:promise对象刚被创建后的初始化状态 (4)promise.then(onFulfilled, onRejected):pomise对象会在变为resolve或者reject的时候分别调用相应注册的回调函数 等价于(promise.then的语法糖):

promise.then(function(value) {
    // onFulfilled 成功
}).catch(function(error){
    // onRejected 失败
});

(5)promise.catch(function(error) {}):用来注册当promise对象状态变为Rejected时的回调函数;是 promise.then(undefined, onRejected); 方法的一个别名(语法糖) 注意:catch 是ECMAScript的 保留字 (Reserved Word)。基于ECMAScript 3实现的浏览器,会出现 identifier not found 这种语法错误;而在ECMAScript 5中保留字都属于 IdentifierName ,也可以作为属性名使用了。 为了回避ECMAScript保留字带来的问题,可以使用 promise["catch"](function(error) {}) 如果需要支持IE8及以下版本的浏览器的话,那么一定要将这个 catch 问题牢记在心中!!!

3. 编写promise代码,把XHR处理包装

function getURL(url) {
    return promise = 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();
    });
}
// 运行示例
getURL("http://httpbin.org/get").then(function onFulfilled(value) {
    console.log(value);
}, function onRejected(error) {
    console.log(error);
});

二、实战Promise

1. 创建promise对象

  • (1)new Promise(function(resolve, reject) {});
  • (2)Promise.resolve(value)
  • (3)Promise.reject(error)

示例(1):

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

可以认为是下述代码的语法糖:

new Promise(function(resolve) {
    resolve("ligang");
}).then(function(value) {
    console.log(value);
});

示例(2):

Promise.reject("system error").catch(function(error) {
    console.error(error);
});

可以认为是下述代码的语法糖:

new Promise(function(resolve, reject) {
    reject("system error");
}).then(null, function(error) {
    console.error(error);
});

2. Promise只能异步操作?

var promise = new Promise(function(resolve, reject){
    console.log(1);
    resolve(2);
});
promise.then(function(value) {  // .then 中指定的方法调用是异步进行的
    console.log(value);
});
console.log(3);
// 运行结果: 1 -> 3 -> 2

3. 同步调用和异步调用同时存在导致的混乱

这个问题的本质是接收回调函数的函数,会根据具体的执行情况,可以选择是以同步还是异步的方式对回调函数进行调用。在开发中经常出现!!

function onReady(fn) {
    var readyState = document.readyState;
    if(readyState === 'interactive' || readyState === 'complete') {
        fn();
    }else {
        window.addEventListener('DOMContentLoaded', fn);
    }
}
onReady(function() {
    console.log(1)
});
console.log(2);

运行结果:

  • 如果在调用onReady之前DOM已经载入的话,对回调函数进行同步调用。输出:1 -> 2
  • 如果在调用onReady之前DOM还没有载入的话,通过注册 DOMContentLoaded 事件监听器来对回调函数进行异步调用。输出:2 -> 1

出现上述两种情况,往往会导致程序不能按预期执行。 (1)异步调用方式

function onReady(fn) {
    var readyState = document.readyState;
    if(readyState === 'interactive' || readyState === 'complete') {
        setTimeout(fn, 0);
    }else {
        window.addEventListener('DOMContentLoaded', fn);
    }
}
onReady(function() {
    console.log(1)
});
console.log(2);
// 运行结果统一输出:2 -> 1

(2)Promise重写

function onReadyPromise() {
    return new Promise(function(resolve, reject) {
        var readyState = document.readyState;
        if(readyState === 'interactive' || readyState === 'complete') {
            resolve();
        }else {
            window.addEventListener('DOMContentLoaded', resolve);
        }
    });
}
onReadyPromise().then(function(){
    console.log(1);
}).catch(function(){
    console.error("error");
});
console.log(2);
// 运行结果统一输出:2 -> 1

总结:

  1. 绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。
  2. 如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料之外的后果。
  3. 对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题。
  4. 如果想在将来某时刻调用异步回调函数的话,可以使用 setTimeout 等异步API。

4. promise chain

Promise里可以将任意个方法连在一起作为一个方法链

function taskA(value) {
    console.log("Task A");
    return value = value + 1;
}
function taskB(value) {
    console.log("Tash B");
    return value = value * 2;
}
function onRejected(error) {
    console.log("Catch Error:A or B", error);
}
function finalTask(value) {
    console.log("Final Tash", value);
}
var promise = Promise.resolve(1);
// taskA、taskB的错误,都会被onRejected捕获
promise.then(taskA)
        .then(taskB)
        .catch(onRejected)
        .then(finalTask); 

5. then、catch返回新的promise对象

var promise = new Promise(function(resolve) {
    resolve(100);
});
var thenPromise = promise.then(function(value) {
    console.log(value);
});
var catchPromise = thenPromise.catch(function(error) {
    console.error(error);
});
console.log(promise === thenPromise);   // false
console.log(thenPromise === catchPromise);  // false

注意:在对Promise进行扩展的时候需要牢牢记住这一点,否则稍不留神就有可能对错误的promise对象进行了处理

// 错误使用
function badAsyncCall() {
    var promise = Promise.resolve();
    promise.then(function() {
        // 任意处理
        return newVar;
    });
    return promise;
}
// 正确使用,返回新的promise
function anAsyncCall() {
    var promise = Promise.resolve();
    return promise.then(function() {
        // 任意处理
        return newVar;
    });
}

6. Promise.all([]).then(function(result) {})

数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用 .then 方法

function timerPromisefy(delay) {
    return new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve(delay);
        }, delay);
    });
}
var startTime = Date.now();
Promise.all([
    timerPromisefy(8), 
    timerPromisefy(16), 
    timerPromisefy(32)
]).then(function(result) {
    console.log(Date.now() - startTime);    // 约32ms
    console.log(result);    // [8, 16, 32]
});

说明:Promise.all([])中的方法会同时开始执行(并行),而每个promise的结果和传递给Promise.all的promise数组的顺序是一致的。

7. Promise.race()

数组里只要有一个promise对象变为resolve或reject状态的时候,就会去调用 .then 方法

function timerPromisefy(delay) {
    return new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve(delay);
        }, delay);
    });
}
var startTime = Date.now();
Promise.race([
    timerPromisefy(8), 
    timerPromisefy(16), 
    timerPromisefy(32)
]).then(function(result) {
    console.log(Date.now() - startTime);    // 约8ms
    console.log(result);    // 8
});

补充:Thenable

1. 什么是Thenable

类Promise对象。拥有名为.then方法的对象。 jQuery.ajax(),它的返回值就是thenable对象。

2. 将Thenable转换为Promise对象

Promise.resolve方法可以将Thenable对象转换为Promise对象。

var promise = Promise.resolve($.ajax('/json/comment.json'));  // Thenable对象 => promise对象
promise.then(function(value){
   console.log(value);
});

3. 什么时候该使用Thenable

在Promise类库之间进行相互转换是使用Thenable的最佳场景 例如:将ES6的promise对象转换为Q的promise的对象

var Q = require("Q");
// 这是一个ES6的promise对象
var promise = new Promise(function(resolve){
    resolve(1);
});
// 变换为Q promise对象
Q(promise).then(function(value){
    console.log(value);
}).finally(function(){ 
    console.log("finally");
});

三、使用Promise.race和delay取消XHR请求

1. 通过超时取消XHR操作

function copyOwnFrom(target, source) {
    Object.getOwnPropertyNames(source).forEach(function (propName) {
        Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
    });
    return target;
}
function TimeoutError() {
    var superInstance = Error.apply(null, arguments);
    copyOwnFrom(this, superInstance);
}
TimeoutError.prototype = Object.create(Error.prototype);
TimeoutError.prototype.constructor = TimeoutError;

function delayPromise(ms) {
    return new Promise(function (resolve) {
        setTimeout(resolve, ms);
    });
}
function timeoutPromise(promise, ms) {
    var timeout = delayPromise(ms).then(function () {
            return Promise.reject(new TimeoutError('Operation timed out after ' + ms + ' ms'));
        });
    return Promise.race([promise, timeout]);
}
function cancelableXHR(URL) {
    var req = new XMLHttpRequest();
    var promise = new Promise(function (resolve, reject) {
            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.onabort = function () {
                reject(new Error('abort this request'));
            };
            req.send();
        });
    var abort = function () {
        // 如果request还没有结束的话就执行abort
        // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
        if (req.readyState !== XMLHttpRequest.UNSENT) {
            req.abort();
        }
    };
    return {
        promise: promise,
        abort: abort
    };
}
var object = cancelableXHR('http://httpbin.org/get');
// main
timeoutPromise(object.promise, 1000).then(function (contents) {
    console.log('Contents', contents);
}).catch(function (error) {
    if (error instanceof TimeoutError) {
        object.abort();
        return console.log(error);
    }
    console.log('XHR Error :', error);
});

补充:Deferred

1. 什么是Deferred

Deferred和Promise不同,它没有共通的规范,每个Library都是根据自己的喜好来实现的。比如 jQuery.Deferred 和 JSDeferred 等

2. Deferred和Promise关系

Deferred和Promise并不是处于竞争的关系,而是Deferred内涵了Promise。

3. 基于Promise实现Deferred

function Deferred() {
    this.promise = new Promise(function(resolve, reject) {
        this._resolve = resolve;
        this._reject = reject;
    }.bind(this));
}
Deferred.prototype.resolve = function(value) {
    this._resolve.call(this.promise, value);
};
Deferred.prototype.reject = function(error) {
    this._reject.call(this.promise, error);
};

4. 示例【实现上述XHR包裹】

function getURL(url) {
    var deferred = new Deferred();
    var req = new XMLHttpRequest();
    req.open('GET', url, true);
    req.onload = function() {
        if(req.status === 200) {
            deferred.resolve(req.responseText);
        }else {
            deferred.reject(new Error(req.statusText));
        }
    }
    req.onerror = function() {
        deferred.reject(new Error(req.statusText));
    }
    req.send();
    return deferred.promise;
}
getURL("http://httpbin.org/get").then(function(value) {
    console.log(value);
}).catch(function(error) {
    console.error(error);
});

5. Deferred和Promise区别

区别在于上述图中的“特权方法”。如上述,对比上述两个版本的 getURL: 1. Deferred 的话不需要将代码用Promise括起来,由于没有被嵌套在函数中,可以减少一层缩进 2. Deferred 没有Promise里的错误处理逻辑 3. Promise一般都会在构造函数中编写主要处理逻辑,对 resolve、reject 方法的调用时机也基本是很确定的

new Promise(function (resolve, reject){
    // 在这里进行promise对象的状态确定
}); 

4. 而使用Deferred的话,并不需要将处理逻辑写成一大块代码,只需要先创建deferred对象,可以在任何时机对 resolve、reject 方法进行调用。

var deferred = new Deferred(); // 可以在随意的时机对
    `resolve`、`reject` 方法进行调用

Promise代表了一个对象,这个对象的状态现在还不确定,但是未来一个时间点它的状态要么变为正常值(FulFilled),要么变为异常值(Rejected);而Deferred对象表示了一个处理还没有结束的这种事实,在它的处理结束的时候,可以通过Promise来取得处理结果。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券