1. 90前端首页
  2. 前端开发
  3. JavaScript

JavaScript——Promise进阶

最近新型冠状病毒严重,在家也没啥事,写写文章,Promise 前端开发或多或少都有了解或使用到,我也经常用到。 希望阅读本章能帮助了解Promise到底是怎么实现的,或多或少能帮你^_^

为什么出现Promise

在javascript开发过程中,代码是单线程执行的,同步操作,彼此之间不会等待,这可以说是它的优势,但是也有它的弊端,如一些网络操作,浏览器事件,文件等操作等,都必须异步执行,针对这些情况,起初的操作都是使用回调函数实现。

实现方式如下(伪代码):

function One(callback) {
    if (success) {
        callback(err, result);
    } else {
        callback(err, null);
    }
}

One(function (err, result) {
    //执行完One函数内的内容,成功的结果回调回来向下执行
})

上述代码只是一层级回调,如果代码复杂后,会出现多层级的回调,代码可读性也会很差,那有没有一种方式,不用考虑里面的内容,直接根据结果成功还是失败执行下面的代码呢?有的,Promise(承诺),在ES6中对Promise进行了统一的规范

什么是promise

Promise可能大家都不陌生,因为Promise规范已经出来好一段时间了,同时Promise也已经纳入了ES6,而且高版本的chrome、firefox浏览器都已经原生实现了Promise,只不过和现如今流行的类Promise类库相比少些API。

Promise规范如下:

  • 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
  • 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
  • promise必须实现then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致
  • then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。

Promise原理与讲解

原理剖析--极简promise

由以上规范就容易就能实现这个类的大致结构

<!DOCTYPE html>
<html>
<head lang=\"en\">
    <meta charset=\"UTF-8\">
    <title>Promise</title>
</head>
<body>
<script type=\"text/javascript\">
    class Promise {
        callbacks = [];
        constructor(fn) {
            fn(this.resolve.bind(this));
        }
        then(onFulfilled) {
            this.callbacks.push(onFulfilled);
        }
        resolve(value) {
            this.callbacks.forEach(fn => fn(value));
        }
    }

    //Promise应用
    let p = new Promise(resolve => {
        setTimeout(() => {
            resolve(\'测试\');
        }, 2000);
    })
    p.then((tip)=>{
        console.log(\'tip1\',tip) // tip1,测试
    })
    p.then((tip)=>{
        console.log(\'tip2\',tip) // tip2,测试
    })
</script>
</body>
</html>

这个简单版本大致逻辑是:
实例化Promise时,其类构造函数初始化执行了回调函数,并将绑定了当前实例的resolve方法作为参数回传给回到函数。接着调动Promise对象的then方法, 注册异步操作完成后的回调函数。 当异步操作完成时,调用resolve方法, 该方法执行then方法注册的回调函数。
这里then 方法注册完成时的回到是一个数组, then方法可以多次调用。注册的函数会在异步操作完成后根据添加的顺序依次执行。

相信仔细的人应该可以看出来,then方法应该能够链式调用,但是上面的最基础简单的版本显然无法支持链式调用。想让then方法支持链式调用,其实也是很简单的(如果写过jQuery插件的同学应该熟悉):

// 在上方代码添加
then(onFulfilled) {
    this.callbacks.push(onFulfilled);
    return this; //看这里
}

// 修改其调用方式
p.then((tip)=>{
    console.log(\'tip1\',tip) // tip1,测试
}).then((tip)=>{
    console.log(\'tip2\',tip) // tip2,测试
})

加入延时机制

首先我们吧上方代码的栗子中 setTimeout 去掉,在执行一下

//Promise应用
let p = new Promise(resolve => {
    console.log(\'同步执行1\');
    resolve(\'同步执行2\');
}).then(tip => {
    console.log(\'then1\', tip);
}).then(tip => {
    console.log(\'then2\', tip);
});

发现只打印了 同步执行1
JavaScript——Promise进阶
这显然是不允许的,Promises/A+规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此我们要加入一些处理,保证在resolve执行之前,then方法已经注册完所有的回调。我们可以这样改造下resolve函数

// 修改上方代码
resolve(value) {
    //看这里
    setTimeout(() => {
        this.callbacks.forEach(fn => fn(value));
    });
}
//通过`setTimeout`机制,将`resolve`中执行回调的逻辑放置到`JS`任务队列末尾,以保证在`resolve`执行时,`then`方法的回调函数已经注册完成.

JavaScript——Promise进阶

加入状态

为了解决上面提到的问题, 我们需要加入状态机制, 也就是大家熟知的pending, fulfilled, rejected。
Promises/A+ 规范中明确规定了, pending 可以转化为fulfilled或rejected并且只能转化一次。 也就是说如果pending转为fulfiled就不能再转化到rejected。 并且fulfilled 和 rejected状态只能pending转化而来, 两者之间不能互相转换。
JavaScript——Promise进阶

// 修改如下
class Promise {
    callbacks = [];
    state = \'pending\';//增加状态
    value = null;//保存结果
    constructor(fn) {
        fn(this.resolve.bind(this));
    }
    then(onFulfilled) {
        if (this.state === \'pending\') {
            //在resolve之前,跟之前逻辑一样,添加到callbacks中
            this.callbacks.push(onFulfilled);
        } else {
            //在resolve之后,直接执行回调,返回结果了
            onFulfilled(this.value);
        }
        return this;
    }
    resolve(value) {
        this.state = \'fulfilled\';//改变状态
        this.value = value;//保存结果
        this.callbacks.forEach(fn => fn(value));
    }
}

resolve 执行时, 会将状态设置为 fulfilled , 并把 value 的值存起来, 在此之后调用 then 添加的新回调都会立即执行, 直接返回保存的value值

有同学发现增加了状态的后代码把setTimeout去掉了,原因是:
resolve执行时,会将状态设置为fulfilled,在此之后调用then添加的新回调,都会立即执行

链式Promise

那么这里问题又来了,如果用户再then函数里面注册的仍然是一个Promise,该如何解决?比如下面

p()
.then(()=>{
    // 这里返回Promise
    return new Promise(function (resolve) {
         resolve(resolve);
    });
})
.then(function (res) {
    // res拿到上一个Promise.resolve的值
});

真正的链式调用
真正的链式Promise是指在当前Promise达到fulfilled状态后, 即开始进行下一个Promise(后邻Promise)。 那么我们如何衔接当前Promise和后邻Promise呢,这个是重点,修改较多,我附一段完整的代码

<!DOCTYPE html>
<html>
<head lang=\"en\">
    <meta charset=\"UTF-8\">
    <title>Promise</title>
</head>
<body>
<script type=\"text/javascript\">
    class Promise {
        callbacks = [];
        state = \'pending\';//增加状态
        value = null;//保存结果
        constructor(fn) {
            fn(this.resolve.bind(this));
        }
        then(onFulfilled) {
            return new Promise(resolve => {
                this.handle({
                    onFulfilled: onFulfilled || null,
                    resolve: resolve
                });
            });
        }
        handle(callback) {
            if (this.state === \'pending\') {
                this.callbacks.push(callback);
                return false;
            }
            //如果then中没有传递任何东西
            if (!callback.onFulfilled) {
                callback.resolve(this.value);
                return false;
            }
            var ret = callback.onFulfilled(this.value);
            callback.resolve(ret);
        }
        resolve(value) {
            if (value && (typeof value === \'object\' || typeof value === \'function\')) {
                var then = value.then;
                if (typeof then === \'function\') {
                    then.call(value, this.resolve.bind(this));
                    return;
                }
            }
            this.state = \'fulfilled\';//改变状态
            this.value = value;//保存结果
            this.callbacks.forEach(fn => fn(value));
        }
    }

    // then中返回Promise
    let p = new Promise(resolve => {
        console.log(\'同步执行1\');
        resolve(\'同步执行2\');
    }).then(tip => {
        console.log(tip);
        return new Promise((resolve)=>{
            resolve(\'同步执行3\')
        })
    }).then(tip => {
        console.log(tip);
    });
</script>
</body>
</html>

JavaScript——Promise进阶

  1. then方法中,创建并返回了新的Promise实例,这是串行Promise的基础,并且支持链式调用。
  2. handle方法是promise内部的方法。then方法传入的形参onFulfilled以及创建新Promise实例时传入的resolve均被push到当前promisecallbacks队列中,这是衔接当前promise和后邻promise的关键所在(这里一定要好好的分析下handle的作用)。
  3. resolve 方法中会先检查value是不是 Promise 对象, 如果是一个新的Promise, 那么先不改变当前 promise 的状态。

错误处理

在异常操作失败时,标记其状态为rejected, 并执行注册的失败回调

<!--完整代码-->
<!DOCTYPE html>
<html>
<head lang=\"en\">
    <meta charset=\"UTF-8\">
    <title>Promise</title>
</head>
<body>
<script type=\"text/javascript\">
    class Promise {
        callbacks = [];
        state = \'pending\';//增加状态
        value = null;//保存结果
        constructor(fn) {
            fn(this.resolve.bind(this), this.reject.bind(this));
        }
        then(onFulfilled, onRejected) {
            return new Promise((resolve, reject) => {
                this.handle({
                    onFulfilled: onFulfilled || null,
                    onRejected: onRejected || null,
                    resolve: resolve,
                    reject: reject
                });
            });
        }
        handle(callback) {
            if (this.state === \'pending\') {
                this.callbacks.push(callback);
                return;
            }

            let cb = this.state === \'fulfilled\' ? callback.onFulfilled : callback.onRejected;

            if (!cb) {//如果then中没有传递任何东西
                cb = this.state === \'fulfilled\' ? callback.resolve : callback.reject;
                cb(this.value);
                return;
            }

            // 这里处理,如果在执行成功回调、失败回调时代码出错怎么办,对于类似异常, 处理也很简单, 可以使用try-catch捕获错误, 然后将相应的promise状态设置为rejected状态
            let ret;
            try {
                ret = cb(this.value);
                cb = this.state === \'fulfilled\' ? callback.resolve : callback.reject;
            } catch (error) {
                ret = error;
                cb = callback.reject
            } finally {
                cb(ret);
            }

        }
        resolve(value) {
            if (value && (typeof value === \'object\' || typeof value === \'function\')) {
                var then = value.then;
                if (typeof then === \'function\') {
                    then.call(value, this.resolve.bind(this), this.reject.bind(this));
                    return;
                }
            }
            this.state = \'fulfilled\';//改变状态
            this.value = value;//保存结果
            this.callbacks.forEach(callback => this.handle(callback));
        }
        reject(error) {
            this.state = \'rejected\';
            this.value = error;
            this.callbacks.forEach(callback => this.handle(callback));
        }
    }

    //Promise应用
    let p = new Promise(resolve => {
        console.log(\'同步执行1\');
        resolve(\'同步执行2\');
    }).then(tip => {
        return new Promise((resolve,reject)=>{
            // 做个随机数控制resolve或者reject的调用
            if(parseInt(Math.random()*10) > 4){
                resolve(tip+\' 成功\')
            }else{
                reject(tip+\' 失败\')
            }
        })
    }).then(result => {
        console.log(result);
    }, error => {
        console.log(error);
    });
</script>
</body>
</html>

JavaScript——Promise进阶

总结

promise 里面的 then 函数仅仅是注册了后续需要执行的回调函数,同时返回一个新的Promise对象,以延续链式调用,真正的逻辑是在handle里面
对于内部 pending 、fulfilled 和 rejected 的状态转变,通过 handler 触发 resolve 和 reject 方法,然后更改state状态值

参考文章

MPromise
分层解析 Promise 的实现原理
Promise 原理解析 (渐进实例详解)

本文来自网络整理,转载请注明原出处:https://segmentfault.com/a/1190000021665258

展开阅读全文

发表评论

登录后才能评论