ES6 Promise的使用和理解

·  阅读 12882
ES6 Promise的使用和理解

JS的异步

JS语言的执行环境是“单线程”的,即指一次只能完成一件任务;如果有多个任务,那么必须排队,前面一个任务完成,再执行后一个任务,以此类推。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

常用的异步编程模式

  1. 回调函数
    即f1,f2两个函数,f2要等待f1执行结果后执行,即 f1(f2)
  2. 事件驱动的方式
    f1.on('done', f2); (JQ写法,f1完成时,trigger("done")则执行f2)
  3. 发布-订阅 模式
  4. Promise对象实现

Promise对象

本文着重讲ES6的Promise对象的定义和用法 阮一峰老师的ES6详解 - Promise对象 相信大家在学习ES6的过程中都或多或少的学习过阮老师的ES6教程,那么这里简单举一些例子讲述Promise对象的特点和使用方法

基础使用方法

ES6提供Promise构造函数,我们创造一个Promise实例,Promise构造函数接收一个函数作为参数,这个传入的函数有两个参数,分别是两个函数 resolvereject作用是,resolve将Promise的状态由未成功变为成功,将异步操作的结果作为参数传递过去;相似的是reject则将状态由未失败转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去。
实例创建完成后,可以使用then方法分别指定成功或失败的回调函数,比起f1(f2(f3))的层层嵌套的回调函数写法,链式调用的写法更为美观易读

let promise = new Promise((resolve, reject)=>{
    reject("拒绝了");
});
promise.then((data)=>{
    console.log('success' + data);
}, (error)=>{
    console.log(error)
});

执行结果:"拒绝了"

复制代码

Promise的特点

  • 对象不受外界影响,初始状态为pending(等待中),结果的状态为resolve和reject,只有异步操作的结果决定这一状态
  • 状态只能由pending变为另外两种的其中一种,且改变后不可逆也不可再度修改,
    即pending -> resolved 或 pending -> reject
let promise = new Promise((resolve, reject)=>{
    reject("拒绝了");
    resolve("又通过了");
});
promise.then((data)=>{
    console.log('success' + data);
}, (error)=>{
    console.log(error)
});

执行结果: "拒绝了"

复制代码

上述代码不会再执行resolve的方法

then方法的规则

  • then方法下一次的输入需要上一次的输出
  • 如果一个promise执行完后 返回的还是一个promise,会把这个promise 的执行结果,传递给下一次then
  • 如果then中返回的不是Promise对象而是一个普通值,则会将这个结果作为下次then的成功的结果
  • 如果当前then中失败了 会走下一个then的失败
  • 如果返回的是undefined 不管当前是成功还是失败 都会走下一次的成功
  • catch是错误没有处理的情况下才会走
  • then中不写方法则值会穿透,传入下一个then

用node fs模块读取文件的流程来测试 我们创建一个读取文件的方法,在Promise中定义如果读取成功则展示文件的内容,否则报出错误

let fs = require('fs');

function read(file, encoding) {
    return new Promise((resolve, reject)=>{
        fs.readFile(filePath, encodeing, (err, data)=> {
            if (err) reject(err);
            resolve(data);
        });
    })
}
复制代码

由于想看到多次连贯回调,我们专门设置3个txt文件,其中1号文件的内容为2号文件的文件名,2号文件的内容为3号文件的文件名,3号中展示最终内容

执行代码如下:

read('1.promise/readme.txt', 'utf8').then((data)=>{
    console.log(data)
});
复制代码

读取一个文件的打印结果为,readme2.txt
我们改造这个代码,添加多个回调,在最后一个之前的所有then中都return出当前返回的promise对象

read('readme.txt', 'utf8').then((data)=>{
    return read(data, 'utf8');
}).then((data)=>{
    return read(data, 'utf8')
}).then((data)=>{
    console.log(data);
});

最终输出 readme3.txt的内容

复制代码

再对下一步then进行新的处理,我们对readme3.txt的内容进行加工并返回

read('readme.txt', 'utf8').then((data)=>{
    return read(data, 'utf8');
}).then((data)=>{
    return read(data, 'utf8')
}).then(data=>{
    return data.split('').reverse().join(); // 这一步返回的是一个普通值,普通值在下一个then会作为resolve处理
}).then(null,data=>{   // 特意不对成功做处理,放过这一个值,进入下一步
    throw new Error('出错')  // 由于上一步返回的是普通值,走成功回调,不会走到这里
}).then(data=>{
    console.log(data)  // 最终会打印出来上上步的普通值
});
复制代码

这里我们将内容处理后,则将一个普通值传给了下次的then,但是由于下一个then没有处理成功方法(null)

这个普通值会继续传入下一个then,最终会作为成功值打印出来。

最后我们看一下对错误的处理,在处理完readme3.txt的结果后,我们将这个值传入下一个then中,令其作为一个文件名打开,然而此时已经找不到这个不存在的文件了,那么在最后一步就会打印出报错的结果

read('readme.txt', 'utf8').then((data)=>{
    return read(data, 'utf8');
}).then((data)=>{
    return read(data, 'utf8')
}).then(data=>{
    return data.split('').reverse().join();
}).then(null,data=>{
    throw new Error('出错')
}).then(data=>{
    return read(data, 'utf8')
}).then(null,(err)=>{
    console.log(err)
});
复制代码

结果:

Promises A+ (Promises Aplus)

Promises Aplus规范即规定了Promise的原理,源代码规范等,通过这个规范,我们可以自己实现一个基于PromiseA+规范的Promise类库,下面我们展示一下源码的实现

/**
 * Promise 实现 遵循promise/A+规范
 * 官方站: https://promisesaplus.com/
 * Promise/A+规范译文:
 * https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/#note-4
 */

// promise 三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(excutor) {
    let self = this; // 缓存当前promise实例对象
    self.status = PENDING; // 初始状态
    self.value = undefined; // fulfilled状态时 返回的信息
    self.reason = undefined; // rejected状态时 拒绝的原因
    self.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
    self.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数

    function resolve(value) { // value成功态时接收的终值
        if(value instanceof Promise) {
            return value.then(resolve, reject);
        }

        // 为什么resolve 加setTimeout?
        // 2.2.4规范 onFulfilled 和 onRejected 只允许在 execution context 栈仅包含平台代码时运行.
        // 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

        setTimeout(() => {
            // 调用resolve 回调对应onFulfilled函数
            if (self.status === PENDING) {
                // 只能由pedning状态 => fulfilled状态 (避免调用多次resolve reject)
                self.status = FULFILLED;
                self.value = value;
                self.onFulfilledCallbacks.forEach(cb => cb(self.value));
            }
        });
    }

    function reject(reason) { // reason为失败态时接收的原因
        setTimeout(() => {
            // 调用reject 回调对应onRejected函数
            if (self.status === PENDING) {
                // 只能由pedning状态 => rejected状态 (避免调用多次resolve reject)
                self.status = REJECTED;
                self.reason = reason;
                self.onRejectedCallbacks.forEach(cb => cb(self.reason));
            }
        });
    }

    // 捕获在excutor执行器中抛出的异常
    // new Promise((resolve, reject) => {
    //     throw new Error('error in excutor')
    // })
    try {
        excutor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}


复制代码

这一部分代码我们对resolve和reject进行了判断处理,接着我们构造then方法

/**
 * [注册fulfilled状态/rejected状态对应的回调函数]
 * @param  {function} onFulfilled fulfilled状态时 执行的函数
 * @param  {function} onRejected  rejected状态时 执行的函数
 * @return {function} promise2  返回一个新的promise对象
 */
Promise.prototype.then = function (onFulfilled, onRejected) {
    // 成功和失败的回调 是可选参数
    
    // onFulfilled成功的回调 onRejected失败的回调
    let self = this;
    let promise2;
    // 需要每次调用then时都返回一个新的promise
    promise2 = new Promise((resolve, reject) => {
    // 成功态
        if (self.status === 'resolved') {
            setTimeout(()=>{
                try {
                    // 当执行成功回调的时候 可能会出现异常,那就用这个异常作为promise2的错误的结果
                    let x = onFulfilled(self.value);
                    //执行完当前成功回调后返回结果可能是promise
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        // 失败态
        if (self.status === 'rejected') {
            setTimeout(()=>{
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        if (self.status === 'pending') {
           // 等待态时,当一部调用resolve/reject时,将onFullfilled/onReject收集暂存到集合中
           self.onResolvedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
            self.onRejectedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onRejected(self.reason);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
        }
    });
    return promise2
}
// 其中规范要求对回调中增加setTimeout处理

复制代码

可以看到resolve和reject都有一个处理新promise的方法resolvePromise,对其进行封装,达到处理不同情况的目的

/**
 * 对resolve 进行改造增强 针对resolve中不同值情况 进行处理
 * @param  {promise} promise2 promise1.then方法返回的新的promise对象
 * @param  {[type]} x         promise1中onFulfilled的返回值
 * @param  {[type]} resolve   promise2的resolve方法
 * @param  {[type]} reject    promise2的reject方法
 */
function resolvePromise(promise2,x,resolve,reject){
    if(promise2 === x){ // 如果从onFullfilled中返回的x就是promise2,就会导致循环引用报错
        return reject(new TypeError('Chaining cycle'));
    }
    let called; // 声明避免多次使用
    // x类型判断 如果是对象或者函数
    if(x!==null && (typeof x=== 'object' || typeof x === 'function')){
    // 判断是否是thenable对象
        try{
            let then = x.then; 
            if(typeof then === 'function'){
                then.call(x,y=>{ 
                    if(called) return; 
                    called = true;
                    resolvePromise(promise2,y,resolve,reject);
                },err=>{ 
                    if(called) return;
                    called = true;
                    reject(err);
                });
            }else{
            // 说明是普通对象/函数
                resolve(x);
            }
        }catch(e){
            if(called) return;
            called = true;
            reject(e);
        }
    }else{ 
        resolve(x);
    }
}
复制代码

以上基本实现了Promise的基本方法,根据Promise的用法,补充一些类上的方法

// 用于promise方法链时 捕获前面onFulfilled/onRejected抛出的异常
Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason);
    })
}
Promise.resolve = function(value){
    return new Promise((resolve,reject)=>{
        resolve(value);
    })
}
Promise.prototype.catch = function(onRejected){
    // 默认不写成功
    return this.then(null,onRejected);
};
/**
 * Promise.all Promise进行并行处理
 * 参数: promise对象组成的数组作为参数
 * 返回值: 返回一个Promise实例
 * 当这个数组里的所有promise对象全部变为resolve状态的时候,才会resolve。
 */
Promise.all = function(promises){
    return new Promise((resolve,reject)=>{
        let arr = [];
        let i = 0;
        function processData(index,data){
            arr[index] = data;
            if(++i == promises.length){
                resolve(arr);
            }
        }
        for(let i = 0;i<promises.length;i++){
            promises[i].then(data=>{ // data是成功的结果
                processData(i,data);
            },reject);
        }
    })
}
/**
 * Promise.race
 * 参数: 接收 promise对象组成的数组作为参数
 * 返回值: 返回一个Promise实例
 * 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理(取决于哪一个更快)
 */
Promise.race = function(promises){
    return new Promise((resolve,reject)=>{
        for(let i = 0;i<promises.length;i++){
            promises[i].then(resolve,reject);
        }
    })
}

复制代码

最后我们导出方法

module.exports = Promise;
复制代码

至此一个符合PromiseA+规范的自己写的源码库完成了,可以测试使用这个库替代Promise,以测试是否有逻辑错误等,或者可以使用

npm install promises-aplus-tests -g
promises-aplus-test 文件名
复制代码

插件来测试该源码是否符合PromiseA+规范

希望这篇文章能帮到你,以上