异步发展流程 —— 手写一个符合 Promise/A+ 规范的 Promise

2018-10-11 admin

在这里插入图片描述

概述

Promise 是 js 异步编程的一种解决方案,避免了 “回调地狱” 给编程带来的麻烦,在 ES6 中成为了标准,这篇文章重点不是叙述 Promise 的基本用法,而是从零开始,手写一版符合 Promise/A+ 规范的 Promise,如果想了解更多 Promise 的基本用法,可以看 异步发展流程 —— Promise 的基本使用 这篇文章。

<br/>

Promise 构造函数的实现

我们在使用 Promise 的时候其实是使用 new 关键字创建了一个 Promise 的实例,其实 Promise 是一个类,即构造函数,下面来实现 Promise 构造函数。

Promise/A+ 规范的内容比较多,详情查看 <a></a>https://promisesaplus.com/,我们在实现 Promise 逻辑时会根据实现的部分介绍相关的 Promise/A+ 规范内容。

在 Promise/A+ 规范中规定:

  • 构造函数的参数为一个名为 executor 的执行器,即函数,在创建实例时该函数内部逻辑为同步,即立即执行;
  • executor 执行时的参数分别为 resolvereject,一个为成功时执行的函数,一个为失败时执行的函数;
  • executor 执行时,一旦出现错误立即调用 reject 函数,并设置错误信息给 reason 属性;
  • 每个 Promise 实例有三个状态 pendingfulfilledrejected,默认状态为 pending
  • 状态只能从 pendingfulfilled 或从 pendingrejected,且不可逆;
  • 执行 resolve 函数会使状态从 pending 变化到 fulfilled 并将参数存入实例的 value 属性中;
  • 执行 reject 函数会使状态从 pending 变化到 rejected 并将错误信息存入实例的 reason 属性中。

针对上面的 Promise/A+ 规范,Promise 构造函数代码实现如下:

// promise.js -- Promise 构造函数
function Promise(executor) {
    var self = this;
    self.status = "pending"; // 当前 Promise 实例的状态
    self.value = undefined; // 当前 Promise 实例成功状态下的值
    self.reason = undefined; // 当前 Promise 实例失败状态的错误信息
    self.onFulfilledCallbacks = []; // 存储成功的回调函数的数组
    self.onRejectedCallbacks = []; // 存储失败的回调函数的数组

    // 成功的执行的函数
    function resolve(value) {
        if (self.status === "pending") {
            self.status = "fulfilled";
            self.value = value;
            // 每次调用 resolve 时,执行 onFulfilledCallbacks 内部存储的所有的函数(在实现 then 方法中详细说明)
            self.onFulfilledCallbacks.forEach(function(fn) {
                fn();
            });
        }
    }

    // 失败执行的函数
    function reject(reason) {
        if (self.status === "pending") {
            self.status = "rejected";
            self.reason = reason;
            // 每次调用 reject 时,执行 onRejectedCallbacks 内部存储的所有的函数(在实现 then 方法中详细说明)
            self.onRejectedCallbacks.forEach(function(fn) {
                fn();
            });
        }
    }

    // 调用执行器函数
    try {
        executor(resolve, reject);
    } catch (e) {
        // 如果执行器执行时出现错误,直接调用失败的函数
        reject(e);
    }
}

// 将自己的 Promise 导出
module.exports = Promise;

上面构造函数中的 resolvereject 方法在执行的时候都进行了当前状态的判断,只有状态为 pending 时,才能执行判断内部逻辑,当两个函数有一个执行后,此时状态发生变化,再执行另一个函数时就不会通过判断条件,即不会执行判断内部的逻辑,从而实现了两个函数只有一个执行判断内部逻辑的效果,使用如下:

// verify-promise.js -- 验证 promise.js 的代码
// 引入自己的 Promise 模块
// 因为都验证代码都写在 verify-promise.js 文件中,后面就不再引入了
const Promise = require("./promise.js");

let p = new Promise((resolve, reject) => {
    // ...同步代码
    resolve();
    reject();
    // 上面两个函数只有先执行的 resolve 生效
});

<br/>

实例方法的实现

1、then 方法的实现

没有 Promise 之前在一个异步操作的回调函数中返回一个结果在输入给下一个异步操作,下一个异步操作结束后需要继续执行回调,就形成回调函数的嵌套,在 Promise 中,原来回调函数中的逻辑只需要调用当前 Promise 实例的 then 方法,并在 then 方法的回调中执行,改变了原本异步的书写方式。

在 then 方法中涉及到的 Promise/A+ 规范:

  • Promise 实例的 then 方法中有两个参数,都为函数,第一个参数为成功的回调 onFulfilled,第二个参数为失败的回调 onRejected
  • 当 Promise 内部执行 resolve 时,调用实例的 then 方法执行成功的回调 onFulfilled,当 Promise 内部执行 reject 或执行出错时,调用实例的 then 方法执行错误的回调 onRejected
  • then 方法需要支持异步,即如果 resovlereject 执行为异步时,then 方法的回调 onFulfilledonRejected 需要在后面执行;
  • Promise 需要支持链式调用,Promise 实例调用 then 方法后需要返回一个新的 Promise 实例。如果 then 的回调中有返回值且是一个 Promise 实例,则该 Promise 实例执行后成功或失败的结果传递给下一个 Promise 实例的 then 方法 onFulfilled (成功的回调)或 onRejected(失败的回调)的参数,如果返回值不是 Promise 实例,直接将这个值传递给下一个 Promise 实例 then 方法回调的参数,then 的回调如果没有返回值相当于返回 undefined
  • Promise 实例链式调用 then 时,当任何一个 then 执行出错,链式调用下一个 then 时会执行错误的回调,错误的回调没有返回值相当于返回了 undefined,再次链式调用 then 时会执行成功的回调;
  • Promise 实例的链式调用支持参数穿透,即当上一个 then 没有传递回调函数,或参数为 null 时,需要后面调用的 then 的回调函数来接收;
  • executor 在 Promise 构造函数中执行时使用 try...catch... 捕获异常,但是内部执行的代码有可能是异步的,所以需要在 then 方法中使用 try...catch... 再次捕获;
  • Promise 实例的 then 方法中的回调为 micro-tasks(微任务),回调内的代码应晚于同步代码执行,在浏览器内部调用微任务接口,我们这里模拟使用宏任务代替。

针对上面的 Promise/A+ 规范,then 方法代码实现如下:

// promise.js -- then 方法
Promise.prototype.then = function(onFulfilled, onRejected) {
    // 实现参数穿透
    if(typeof onFulfilled !== "function") {
        onFulfilled = function (data) {
            return data;
        }
    }

    if(typeof onRejected !== "function") {
        onRejected = function (err) {
            throw err;
        }
    }

    // 返回新的 Promise,规范中规定这个 Promise 实例叫 promise2
    var promise2 = new Promise(function (resolve, reject) {
        if (this.status === "fulfilled") {
            // 用宏任务替代模拟微任务,目的是使 `then` 的回调晚于同步代码执行
            setTimeout(function () {
                try {  // 捕获异步的异常
                    // onFulfilled 执行完返回值的处理,x 为成功回调的返回值
                    var x = onFulfilled(this.value);

                    // 处理返回值单独封装一个方法
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            }.bind(this), 0);
        }

        if (this.status === "rejected") {
            setTimeout(function () {
                try {
                    // onRejected 执行完返回值的处理,x 为失败回调的返回值
                    var x = onRejected(this.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            }.bind(this), 0);
        }

        // 如果在 Promise 执行 resolve 或 renject 为异步
        // 将 then 的执行程序存储在实例对应的 onFulfilledCallbacks 或 onRejectedCallbacks 中
        if (this.status === "pending") {
            this.onFulfilledCallbacks.push(function() {
                setTimeout(function () {
                    try {
                        var x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }.bind(this), 0);
            });

            this.onRejectedCallbacks.push(function() {
                setTimeout(function () {
                    try {
                        var x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }.bind(this), 0);
            });
        }
    });

    return promise2;
};

在处理 then 回调的返回值时,其实就是在处理该返回值与 then 方法在执行后返回的新 Promise 实例(即 promise2)之间的关系,因为无论 Promise 的执行器在执行 resolve 还是 reject 是同步或是异步,都需要进行处理,所以我们单独封装一个函数 resolvePromise 来处理。

resolvePromise 函数有四个参数:

  • promise2:then 执行后返回的 Promise 实例;
  • x:then 的回调返回的结果;
  • resolve:promise2 的 resolve 函数;
  • reject:promise2 的 reject 函数。

在 resolvePromise 函数中涉及到的 Promise/A+ 规范:

  • 将每个 Promise 实例调用 then 后返回的新 Promise 实例称为 promise2,将 then 回调返回的值称为 x
  • 如果 promise2x 为同一个对象,由于 x 要将执行成功或失败的结果传递 promise2then 方法回调的参数,因为是同一个 Promise 实例,此时既不能成功也不能失败(自己不能等待自己完成),造成循环引用,这种情况下规定应该抛出一个类型错误来回绝;
  • 如果 x 是一个对象或者函数且不是 null,就去取 xthen 方法,如果 x 是对象,防止 x 是通过 Object.defineProperty 添加 then 属性,并添加 getset 监听,如果在监听中抛出异常,需要被捕获到,x.then 是一个函数,就当作 x 是一个 Promise 实例,直接执行xthen 方法,执行成功就让 promise2 成功,执行失败就让 promise2 失败,如果 x.then 不是函数,则说明 x 为普通值,直接调用 promise2resolve 方法将 x 传入,不满足条件说明该返回值就是一个普通值,直接执行 promise2resolve 并将 x 作为参数传入;
  • 如果每次执行 xthen 方法,回调中传入的参数还是一个 Promise 实例,循环往复,需要递归 resolvePromise 进行解析;
  • 在递归的过程中存在内、外层同时调用了 resolvereject 的情况,应该声明一个标识变量 called 做判断来避免这种情况。

针对上面的 Promise/A+ 规范,resolvePromise 函数代码实现如下:

// promise.js -- resolvePromise 方法
function resolvePromise(promise2, x, resolve, reject) {
    // 判断 x 和 promise2 是不是同一个函数
    if (promise2 === x) {
        reject(new TypeError("循环引用"));
    }

    // x 是对象或者函数并且不是 null,如果不满足该条件说明 x 只是一个普通的值
    if (x !== null && (typeof x === "object" || typeof x === "function")) {
        // 标识变量,防止递归内外层 resolve 和 reject 同时调用
        // 针对 Promise,x 为普通值的时候可以放行
        var called;

        // 为了捕获 Object.defineProperty 创建的 then 属性时添加监听所抛出的异常
        try {
            var then = x.then;

            if (typeof then === "function") { // then 为一个方法,就当作 x 为一个 promise
                // 执行 then,第一个参数为 this(即 x),第二个参数为成功的回调,第三个参数为失败的回调
                then.call(x, function (y) {
                    if (called) return;
                    called = true;

                    // 如果 y 是 Promise 就继续递归解析
                    resolvePromise(promise2, y, resolve, reject);
                }, function (err) {
                    if (called) return;
                    called = true;
                    reject(err);
                });
            } else { // x 是一个普通对象,直接成功即可
                resolve(x);
            }
        } catch(e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

上面我们按照 Promise/A+ 规范实现了 Promise 的 then 方法,接下来针对上面的规范,用一些有针对行的案例来对 then 方法一一进行验证。

验证异步调用 resolvereject

// 文件:verify-promise.js
// 验证 promise.js 异步调用 resolve 或 reject
let p = new Promise((resolve, reject) => {
    setTimeout(() => resolve(), 1000);
});

p.then(() => console.log("执行了"));

// 执行了

验证链式调用 then 返回 Promise 实例:

// 文件:verify-promise.js
// 验证 promise.js then 回调返回 Promise 实例
let p1 = new Promise((resolve, reject) => resolve());
let p2 = new Promise((resolve, reject) => resolve("hello"));

p1.then(() => p2).then(data => console.log(data));

// hello

验证链式调用 then 返回普通值:

// 文件:verify-promise.js
// 验证 promise.js then 回调返回普通值
let p = new Promise((resolve, reject) => resolve());

p.then(() => "hello").then(data => console.log(data));

// hello

验证链式调用 then 中执行出错链式调用 then 执行错误的回调后,再次链式调用 then

// 文件:verify-promise.js
// 验证 promise.js 链式调用 then 中执行出错链式调用 then 执行错误的回调后,再次链式调用 then
let p = new Promise((resolve, reject) => resolve());

p.then(() => {
    throw new Error("error");
}).then(() => {
    console.log("success");
}, err => {
    console.log(err);
}).then(() => {
    console.log("成功");
}, () => {
    console.log("失败");
});

// Error: error  at p.then...
// 成功

验证 then 的参数穿透:

// 文件:verify-promise.js
// 验证 then 的参数穿透
let p1 = new Promise((resolve, reject) => resolve("ok"));

let p2 = p1.then().then(data => {
    console.log(data);
    throw new Error("出错了");
});

p2.then().then(null, err => console.log(err));

// ok
// 出错了

验证 then 方法是否晚于同步代码执行:

// 文件:verify-promise.js
// 验证 then 方法是否晚于同步代码执行
let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => console.log(data));
console.log(2);

// 2
// 1

验证循环引用:

// 文件:verify-promise.js
// 验证 promise.js 循环引用
let p1 = new Promise((resolve, reject) => resolve());

// 让 p1 then 方法的回调返回自己
var p2 = p1.then(() => {
    return p2;
});

p2.then(() => {
    console.log("成功");
}, err => {
    console.log(err);
});

// TypeError: 循环引用  at resolvePromise...

验证 then 回调返回对象通过 Object.definePropertype 添加 then 属性并添加 get 监听,在触发监听时抛出异常:

// 文件:verify-promise.js
// 验证 promise.js then 回调返回对象通过 Object.definePropertype 添加 then 和 get 监听,捕获异常
let obj = {};
Object.defineProperty(obj, "then", {
    get () {
        throw new Error();
    }
});

let p = new Promise((resolve, reject) => resolve());
p.then(() => {
    return obj;
}).then(() => {
    console.log("成功");
}, () => {
    console.log("出错了");
});

// 出错了

验证每次执行 resolve 都传入 Promise 实例,需要将最终的执行结果传递给下一个 Promise 实例 then 的回调中:

// 文件:verify-promise.js
// 验证 promise.js 每次执行 resolve 都传入 Promise 实例
let p = new Promise((resolve, reject) => resolve());

p.then(() => {
    return new Promise((resolve, reject) => {
        resolve(new Promise(resolve, reject) => {
            resolve(new Promise(resolve, reject) => {
                resolve(200);
            });
        });
    });
}).then(data => {
    console.log(data);
});

// 200

2、catch 方法的实现

// promise.js -- catch 方法
Promise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected);
}

catch 方法可以理解为是 then 方法的一个简写,只是参数中少了成功的回调,所以利用 Promise/A+ 规范中参数穿透的特性,很容易就实现了 catch 方法,catch 方法的真相就是这么的简单。

验证 catch 方法:

// 文件:verify-promise.js
// 验证 promise.js 的 catch 方法
let p = new Promise((resolve, reject) => reject("err"));

p.then().catch(err => {
    console.log(err);
}).then(() => {
    console.log("成功了");
});

// err
// 成功了

静态方法的实现

1、Promise.resolve 方法的实现

Promise.resolve 方法传入一个参数,并返回一个新的 Promise 实例,这个参数作为新 Promise 实例 then 方法成功回调的参数,在调用时感觉直接成功了,其实是直接执行了返回 Promise 实例的 resolve

// promise.js -- Promise.resolve 方法
Promise.resolve = function (val) {
    return new Promise(function (resolve, reject) {
        resolve(val);
    });
}

验证 Promise.resolve 方法:

// 文件:verify-promise.js
// 验证 promise.js 的 Promise.resolve 方法
Promise.resolve("成功了").then(data => console.log(data));

// 成功了

2、Promise.reject 方法的实现

Promise.reject 方法与 Promise.resolve 的实现思路相同,不同的是,直接调用了返回新 Promise 实例的 reject

// promise.js -- Promise.reject 方法
Promise.reject = function (reason) {
    return new Promise(function (resolve, reject) {
        reject(reason);
    });
}

验证 Promise.reject 方法:

// 文件:verify-promise.js
// 验证 promise.js 的 Promise.reject 方法
Promise.reject("失败了").then(err => console.log(err));

// 失败了

3、Promise.all 方法的实现

Promise.all 方法可以实现多个 Promise 实例的并行,返回值为一个新的 Promise 实例,当所有结果都为成功时,返回一个数组,该数组存储的为每一个 Promise 实例的返回结果,这些 Promise 实例的返回顺序先后不确定,但是返回值的数组内存储的返回结果是按照数组中 Promise 实例最初顺序进行排列的,返回的数组作为返回 Promise 实例成功回调的参数,当其中一个失败,直接返回错误信息,并作为返回 Promise 实例失败回调的参数。

// promise.js -- Promise.all 方法
Promise.all = function (promises) {
    return new Promise(function (resolve, reject) {
        // 存储返回值
        var result = [];
        // 代表存入的个数,因为 Promise 为异步,不知道哪个 Promise 先成功,不能用数组的长度来判断
        var idx = 0;

        // 用来构建全部成功的返回值
        function processData(index, data) {
            result[index] = data; // 将返回值存入数组
            idx++;

            if (idx === promises.length) {
                resolve(result);
            }
        }

        for(var i = 0; i < promises.length; i++) {
            // 因为 Primise 为异步,保证 i 值是顺序传入
            (function (i) {
                promises[i].then(function (data) {
                    processData(i, data);
                }, reject);
            })(i);
        }
    });
}

验证 Promise.all 方法:

// 文件:verify-promise.js
// 验证 promise.js 的 Promise.all 方法
let p1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000));
let p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000));

Promise.all([p1, p2]).then(data => console.log(data));

// [1, 2]

let p3 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000));
let p4 = new Promise((resolve, reject) => setTimeout(() => reject(2), 1000));

Promise.all([p3, p4]).then(data => {
    console.log(data);
}).catch(err => {
    console.log(err);
});

// 2

4、Promise.race 方法的实现

Promise.race 方法与 Promise.all 类似,同样可以实现多个 Promise 实例的并行,同样返回值为一个新的 Promise 实例,参数同样为一个存储多个 Promise 实例的数组,区别是只要有一个 Promise 实例返回结果,无论成功或失败,则直接返回这个结果,并作为新 Promise 实例 then 方法中成功或失败的回调函数的参数。

// promise.js -- Promise.race 方法
Promise.race = function (promises) {
    return new Promise(function (resolve, reject) {
        for(var i = 0; i < promises.length; i++) {
            promises[i].then(resolve, reject);
        }
    });
}

验证 Promise.race 方法:

// 文件:verify-promise.js
// 验证 promise.js 的 Promise.race 方法
let p1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000));
let p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000));

Promise.race([p1, p2]).then(data => console.log(data));

// 2

let p3 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000));
let p4 = new Promise((resolve, reject) => setTimeout(() => reject(2), 1000));

Promise.all([p3, p4]).then(data => {
    console.log(data);
}).catch(err => {
    console.log(err);
});

// 2

使用 promises-aplus-test 测试 Promise 是否符合 Promise/A+ 规范

promises-aplus-test 是专门用来验证 Promise 代码是否符合 Promise/A+ 规范的包,需要通过 npm 下载。

npm install promises-aplus-test -g

测试方法:

  • promise.js 中写入测试代码;
  • 在命令行中输入命令 promises-aplus-test + fileName

测试代码:

// promise.js -- 测试方法 Promise.derfer
// Promise 语法糖
// 好处:解决 Promise 嵌套问题
// 坏处:错误处理不方便
Promise.derfer = Promise.deferred = function () {
    let dfd = {};

    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject;
    });

    return dfd;
}

输入命令:

promises-aplus-test promise.js

执行上面命令后,会根据 Promise/A+ 规范一条一条进行极端的验证,当验证通过后会在窗口中这一条对应的执行项前打勾,验证不通过打叉,直到所有的规范都验证完毕。

原文链接:https://segmentfault.com/a/1190000016653968

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-42445.html

文章标题:异步发展流程 —— 手写一个符合 Promise/A+ 规范的 Promise

相关文章
一些前端学习中好的书籍,整理
一、Javascript方面的书籍: 1 JavaScript权威指南(第6版):号称javascript圣经,前端必备;前端程序员学习核心JavaScript语言和由Web浏览器定义的JavaScript API的指南和综合参考手册; 2...
2015-11-12
bootstrap PrintThis打印插件使用详解
bootstrap PrintThis打印效果图: 如图,这个是调用了谷歌的打印,使用很方便,可以自己調样式,需要的可以研究一下,这边贴出使用方法。 附上GitHub:https://github.com/jasonday/printTh...
2017-03-16
html5+JavaScript教程-微信打飞机小游戏源码
js &#x2F;&#x2F; JavaScript Document var c = document.getElementById(&quot;dotu&quot;); var cxt = c.getContext(&quot;2d&q...
2015-11-12
JavaScript编辑器推荐
主流编辑器有SublimeText,Notepad++,webstorm等,是使用最广泛的编辑器,但也有一些JavaScript编辑器提供有着各自的特性和功能,适应不同人的需求,以下是几款优秀的编辑器,相信你一定能找到自己喜欢的。 1. W...
2015-11-12
js性能优化 如何更快速加载你的JavaScript页面
确保代码尽量简洁 不要什么都依赖JavaScript。不要编写重复性的脚本。要把JavaScript当作糖果工具,只是起到美化作用。别给你的网站添加大量的JavaScript代码。只有必要的时候用一下。只有确实能改善用户体验的时候用一下。 ...
2015-11-12
10个强大的纯CSS3动画案例分享
我们的网页外观主要由CSS控制,编写CSS代码可以任意改变我们的网页布局以及网页内容的样式。CSS3的出现,更是可以让网页增添了不少动画元素,让我们的网页变得更加生动有趣,并且更易于交互。本文分享了10个非常炫酷的CSS3动画案例,希望大家...
2015-11-16
2015年JavaScript或“亲库而远框架”
2014年过去了,作为一个JavaScript开发者很难满怀信心的去“挽回”一个特定的库或技术,即便是强大的Angular,似乎也因为最近的一些事情而动摇。 2014年10月的ng-europe会议上,Angular开发者团队透露了一个关于...
2015-11-12
Android中Okhttp3实现上传多张图片同时传递参数
之前上传图片都是直接将图片转化为io流传给服务器,没有用框架传图片。 最近做项目,打算换个方法上传图片。 Android发展到现在,Okhttp显得越来越重要,所以,这次我选择用Okhttp上传图片。 Okhttp目前已经更新到Okhttp...
2017-03-17
JavaScript实现PC手机端和嵌入式滑动拼图验证码三种效果
PC和手机端网站滑动拼图验证码效果源码,同时包涵了弹出式Demo,使用ajax形式提交二次验证码所需的验证结果值,嵌入式Demo,使用表单形式提交二次验证所需的验证结果值,移动端手动实现弹出式Demo三种效果 首先要确认前端使用页面,比如...
2017-03-17
Angular2-primeNG文件上传模块FileUpload使用详解
近期在学习使用Angular2做小项目,期间用到很多primeNG的模块。 本系列将结合实战总结angular2-primeNG各个模块的使用经验。 文件上传模块FileUploadModule 首先要在使用该组件的模块内导入文件上传模块 ...
2017-03-09
回到顶部