资讯专栏INFORMATION COLUMN

JavaScript深入浅出异步编程-promise原理

morgan / 2538人阅读

摘要:这样得到权力回调函数,当的异步代码执行完毕后,由来执行回调函数。而在平时的开发过程中,在异步编程中起到了几乎不可替代的作用。

其实Promise本身并不具备异步的能力,而之所以这里需要多带带开一篇说明其原理,是因为Promise异步编程的过程中是一个不可或缺的一环。原因下面细说。

在说promise之前,有必要先说下JS中的回调方式。比如下面:

function doSomethingAfterTime(time, something) {
    setTimeout(fun, time);
}

但是这样的回调方式有一个问题,可读性太差。另外当回调的层次多了以后,容易陷入回调地狱。举个例子:

function func1(cb){
    // do something
    cb();
}
function func2(cb){
    // do something
    cb();
}
function func3(cb){
    // do something
    cb();
}
// do
func1(function(){
    func2(function(){
        func3(function(){

        });
    });
});

这样的代码读起来简直就是折磨,晕死!

下面试着改进下代码,试着将回调函数封装起来。顺便剖析下promise的原理。

Promise的原理

先来一个最简单的。

function Promise(something){
    var callback = null;
    this.then = function(onCompelete){
        callback = onCompelete;
    };
    something(function (value){
        callback(value);
    });
}

下面是调用代码。

// 事件1
function func1(){
    return new Promise(function(resolve){
        // do something
        setTimeout(function(){
            console.log("func1");
            resolve();
        },1000);
    });
}

func1().then(function(){
    console.log("func1 complete");
});

上面对Promise的封装算是最简单的版本,只是模拟了Promise的调用方法,比如then,还有Promise的构造函数。但是这样的封装无法实现链式调用,链式调用的核心就是当调用某个方法的时候返回该对象本身或者该对象对应class的全新对象。而对于Promise的改造也很简单.

then方法返回Promise本身

Promoise需要支持多个callback

function Promise(something){
    var callbacks = [];
    this.then = function(onCompelete){
        callbacks.push(onCompelete);
        return this;
    };
    function resolve(value){
        callbacks.forEach(function(cb){
            cb(value);
        });
    }
    something(resolve);  
}

调用代码如下:

func1().then(function(){
    console.log("func1 complete");
}).then(function(){
    console.log("then2");
}).then(function(){
    console.log("then3");
});

现在的Promise执行上面的代码后能够得到正确的执行结果,但是有一个问题,如果我们想在then方法再调用一个返回promise的方法?比如这样:

// 事件2
function func2(){
    return new Promise(function(resolve){
        // do something
        setTimeout(function(){
            console.log("func2");
            resolve();
        },1000);
    });
}

func1().then(func2).then(function(){
    console.log("all complete");
});

输出如下:

func1
all complete
func2

你会发现虽然func2成功调用了,但是输出顺序乱了,我们期望的正确输出顺序应该是:

func1
func2
all complete

分析下问题出在哪里?问题就出在Promise中的callbacks,第一个then是在func1返回的Promise上调用的,而第二个then事实上还是在func1返回的Promise上调用的。然而我们希望的是,第二个then应该是在func2返回的Promise调用,这时候就需要考虑如何进一步改造Promise了。

对于then传入的onCompelete函数参数,它是不知道这个函数具体是否会返回Promise,只有调用了onCompelete方法才能知道具体返回的数据。但是onCompelete是回调函数,你无法直接在then中调用。因此需要考虑其他的方式。

如果then方法里面返回一个新的Promise对象呢?用这个新的Promise作为中间代理,比如这样:

function Promise(something){
    var callbacks = [];
    this.then = function(onCompelete){
        return new Promise(function (resolve) {
            callbacks.push({
                onCompelete: onCompelete,
                resolve: resolve
            });
        });
    };
    function resolve(value){
        callbacks.forEach(function(cb){
            var ret = cb.onCompelete(value);
            cb.resolve(ret);
        })
    }
    something(resolve);  
}

但是运行的时候你会发现输出顺序还是没变,还是有问题的。那么继续分析问题出在哪里?
通过调试发现,resolve传入的value有可能是promise对象,而我们已经在then方法里面返回了新的promise对象了,交由该对象作为代理了。因此resolve传入的value如果是promise对象的话,那么就需要把当前promiseresolve处理权交出去,交给传入的promise对象。相当于代理人把权力交还给实际应该处理的对象。可能有点绕,我再详细的描述下

func1返回的promisep1then返回的promisep2resolve传入的promise对象为p3,func2返回的promise对象为p4

上面一共提到4个promise对象。

说下描说下调用顺序。

首先由func1创建p1,然后调用then方法创建了p2,然后再次调用了then方法,由p2创建了p3p2p3都是由then创建的代理人。

这时候func1中的异步代码执行了,1秒过后由func1调用了p1resolve方法,并且将callbacks数组内的方法依次调用,然后由cb.onCompelete(value)方法间接得到func2返回的p4,接着调用p2resolve方法将p4传入。但是上面说了,p2只是个代理,应该把权力交还给p4来执行。这样p4得到权力--回调函数,当func2的异步代码执行完毕后,由p4来执行回调函数。

因此resolve方法需要进行如下改造。

function resolve(value) {
    // 交还权力,并且把resolve传过去
    if (value && (typeof value.then === "function")) {
        value.then.call(value, resolve);
        return;
    }
    callbacks.forEach(function (cb) {
        var ret = cb.onCompelete(value);
        cb.resolve(ret);
    });
}

上面的代码就是交权的代码。这样完全的Promise修改如下:

function Promise(something) {
    var callbacks = [];
    this.then = function (onCompelete) {
        return new Promise(function (resolve) {
            callbacks.push({
                onCompelete: onCompelete,
                resolve: resolve
            });
        });
    };
    function resolve(value) {
        if (value && (typeof value.then === "function")) {
            value.then.call(value, resolve);
            return;
        }
        callbacks.forEach(function (cb) {
            var ret = cb.onCompelete(value);
            cb.resolve(ret);
        });
    }
    something(resolve);
}

这样修改过后,再执行如下代码:

func1().then(func2).then(function () {
    console.log("all complete");
});

现在就能得到正确的执行结果了。

至此,一个简单的Promise定义完了。这时候有一个问题,如果调用then方法之前resolve已经被执行了怎么办呢,岂不是永远都得不到回调了?比如这样:

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

你会发现then里面的回调就不会执行了。其实这时候只需要做一个小小的改动就行了。改造如下:

function Promise(something) {
    var callbacks = [];
    this.then = function (onCompelete) {
        return new Promise(function (resolve) {
            callbacks.push({
                onCompelete: onCompelete,
                resolve: resolve
            });
        });
    };
    function resolve(value) {
        if (value && (typeof value.then === "function")) {
            value.then.call(value, resolve);
            return;
        }
        setTimeout(function(){
            callbacks.forEach(function (cb) {
                var ret = cb.onCompelete(value);
                cb.resolve(ret);
            });
        },0);
    }
    something(resolve);
}

你会发现,这里只是在resolve方法里面,将执行的回调放入setTimeout中,并且timeout设为0。这里稍微说下原理

在第一篇中提到setTimeout类似定时器,JS内容在执行setTimeout的回调函数的时候使用线程调度的方式将回调函数调度到JS线程执行。但凡涉及到线程调度那么肯定需要等待JS线程空闲的时候才能调度过来。这时候将timeout设为0,相当于改变了代码执行顺序。

在实际的开发过程中,上面的Promise代码还是缺少了一个功能,那就是状态管理,比如:pendingfulfilledrejected。下面的代码继续加入状态管理的代码,先添加pendingfulfilled的状态:

function Promise(something) {
    var callbacks = [];
    var state = 0;//0:pending,1:fulfilled
    var resultValue = null;
    this.then = function (onCompelete) {
        return new Promise(function (resolve) {
            handleCallBack({
                onCompelete: onCompelete,
                resolve: resolve
            });
        });
    };
    function handleCallBack(callback){
        switch(state){
            case 0:{
                callbacks.push(callback);
                break;
            }
            case 1:{
                var ret = callback.onCompelete(resultValue);
                callback.resolve(ret);
                break;
            }
            default:{
                break;
            }
        }
    }
    function resolve(value) {
        if (value && (typeof value.then === "function")) {
            value.then.call(value, resolve);
            return;
        }
        state = 1;
        resultValue = value;
        setTimeout(function(){
            callbacks.forEach(function (cb) {
                handleCallBack(cb);
            });
        },0);
    }
    something(resolve);
}

下面再继续加入reject功能。

function Promise(something) {
    var callbacks = [];
    var state = 0;//0:pending,1:fulfilled 2:reject
    var resultValue = null;
    this.then = function (onCompelete, onReject) {
        return new Promise(function (resolve) {
            handleCallBack({
                onCompelete: onCompelete,
                resolve: resolve,
                reject: onReject
            });
        });
    };
    function handleCallBack(callback) {
        switch (state) {
            case 0: {
                callbacks.push(callback);
                break;
            }
            case 1: {
                var ret = callback.onCompelete(resultValue);
                callback.resolve(ret);
                break;
            }
            case 2: {
                if(callback.reject){
                    var ret = callback.reject(resultValue);
                }
                callback.resolve(ret);
                break;
            }
            default: {
                break;
            }
        }
    }
    function reject(error) {
        state = 2;
        resultValue = error;
        setTimeout(function () {
            callbacks.forEach(function (cb) {
                handleCallBack(cb);
            });
        }, 0);
    }
    function resolve(value) {
        if (value && (typeof value.then === "function")) {
            value.then.call(value, resolve);
            return;
        }
        state = 1;
        resultValue = value;
        setTimeout(function () {
            callbacks.forEach(function (cb) {
                handleCallBack(cb);
            });
        }, 0);
    }
    something(resolve,reject);
}

OK,通过上面一步一步对Promise进行修改,基本上是把Promise的功能完善了。

从这个上面一步一步剖析Promise原理的过程中,我们发现,Promise本身并不提供异步功能,Promise只是对函数的回调功能进行了封装,甚至可以理解为Promise就是一个回调代理。但是正是有了这个回调代理,使得我们的回调方式发生了彻底的改变,甚至直接影响了项目的架构设计。而在平时的开发过程中,Promise在异步编程中起到了几乎不可替代的作用。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106711.html

相关文章

  • ES6-7

    摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...

    mudiyouyou 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • JavaScript 工作原理之四-事件循环及异步编程的出现和 5 种更好的 async/await

    摘要:函数会在之后的某个时刻触发事件定时器。事件循环中的这样一次遍历被称为一个。执行完毕并出栈。当定时器过期,宿主环境会把回调函数添加至事件循环队列中,然后,在未来的某个取出并执行该事件。 原文请查阅这里,略有改动。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原理的第四章。 现在,我们将会通过回顾单线程环境下编程的弊端及如何克服这些困难以创建令人惊叹...

    maochunguang 评论0 收藏0
  • JS笔记

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。异步编程入门的全称是前端经典面试题从输入到页面加载发生了什么这是一篇开发的科普类文章,涉及到优化等多个方面。 TypeScript 入门教程 从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript。 网络基础知识之 HTTP 协议 详细介绍 HTT...

    rottengeek 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<