promise异步发展史

异步介绍

异步简单来说就是做一件事时,做到一半可能需要等待系统或服务处理之后才会得到响应和结果,此时可以转去做另一件事,等到获得响应之后在去执行剩下一半的事情。反之同步就是一直等到响应然后接着做事,中间不会跳去做别的事。

异步发展史可以简单归纳为

  • callback
  • Promise
  • generator + co
  • async + await

JS 中最基础的异步调用方式是callback,它将回调函数callback传给异步API,由浏览器或 Node 在异步完成后,通知JS引擎调用callback。对于简单的异步操作,用 callback 实现,是够用的。但随着负责交互页面和 Node 出现,callback方案的弊端开始浮现出来。 Promise 规范孕育而生,并被纳入 ES6 的规范中。后来 ES7 又在 Promise 的基础上将 async,await函数纳入标准。此为 JavaScript 异步发展史。

同步与异步

通常,代码是由上往下依次执行的。如果有多个任务,就必需排队,前一个任务完成,后一个任务才会执行。这种执行模式称之为:同步(synchronous)。新手容易把计算机用语中的同步,和日常用语中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在计算机中,同步指的是任务从上往下依次执行的模式。比如:

A();
B();
C();
复制代码

在这段代码中,A、B、C是三个不同的函数,每个函数都是一个不相关的任务。在同步模式,计算机会先执行A任务,再执行B任务,最后执行C任务。在大部分情况,同步模式都没问题。但是如果 B 任务是一个耗时很长的网络请求,而 C 任务恰好是展现新页面,就会导致网页卡顿。

更好解决方案是,将B任务分成两个部分。一部分立即执行网络请求的任务,另一部分在请求回来后的执行任务。这种一部分立即执行,另一部分在未来执行的模式称为异步。

A();
// 发送请求
ajax('url', function B(){
  // 在未来某个时刻执行
});
C();
// 执行顺序 A=>C=>B

复制代码

实际上,JS 引擎并没有直接处理网络请求的任务,它只是调用了浏览器的网络请求接口,由浏览器发送网络请求并监听返回的数据。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。

1.callback

未来执行的函数通常也叫 callback。使用 callback 的异步模式,解决了阻塞的问题,但是也带来了一些其他问题。在最开始,我们的函数是从上往下书写的,也是从上往下执行的,这种“线性”模式,非常符合我们的思维习惯,但是现在却被 callback 打断了!在上面一段代码中,现在它跳过 B 任务先执行了 C任务!这种异步“非线性”的代码会比同步“线性”的代码,更难阅读,因此也更容易滋生 BUG。

试着判断下面这段代码的执行顺序,你会对“非线性”代码比“线性”代码更难以阅读,体会更深。

A();
ajax('url1', function() {
    B();
    ajax('url2',function(){
        C();
    });
    D();
});
E();
// A=>E=>B=>D=>C
复制代码

这段代码中,从上往下执行的顺序被 Callback 打乱了。我们的阅读代码视线是A => B => C => D => E,但是执行顺序却是A => E => B => D => C,这就是非线性代码带来的糟糕之处。

通过将ajax后面执行的任务提前,可以更容易看懂代码的执行顺序。虽然代码因为嵌套看起来不美观,但现在的执行顺序却是从上到下的“线性”方式。这种技巧在写多重嵌套的代码时,是非常有用的。

A();
E();
ajax('url1', function() {
    B();
    D();
    ajax('url2',function(){
        C();
    });
});
// A=>E=>B=>D=>C
复制代码

上一段代码只有处理了成功回调,并没处理异常回调。接下来,把异常处理回调加上,再来讨论代码“线性”执行的问题。

A();

ajax('url1', function() {
    B();
    ajax('url2',function(){
        C();
    }, function() {
        D();
    });
}, functon() {
   E();
});
// A=>E=>B=>D=>C
复制代码

加上异常处理回调后,url1的成功回调函数 B 和异常回调函数 E,被分开了。这种“非线性”的情况又出现了。

在 node 中,为了解决的异常回调导致的“非线性”的问题,制定了错误优先的策略。node 中 callback 的第一个参数,专门用于判断是否发生异常。

A();
get('url1', function (error) {
    if(error){
        E();
    } else {
        B();
        get('url2', function (error){
            if(error){
                D(); 
            } else {
                C();
            }
        });
    }
});
复制代码

到此,callback 引起的“非线性”问题基本得到解决。遗憾的是,使用 callback 嵌套,一层层if else和回调函数,一旦嵌套层数多起来,阅读起来不是很方便。此外,callback 一旦出现异常,只能在当前回调函数内部处理异常。

2.promise

callback虽然帮我们解决了异步问题,但是它仍有一些不足。首先:代码嵌套多,结构复杂难以阅读和维护;其次:就是无法合并两个或多个异步的结果。比如下面的例子:

fs.readFile('./2.promise/1.txt', 'utf8', function(err,data) { 
});
fs.readFile('./2.promise/2.txt', 'utf8', function(err, data) { 
    console.log(data);
});
复制代码

Promise的引入就解决了以上这些问题,首先来看下Promise的简单用法:

promise定义:

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理,让开发者不用再关注于时序和底层的结果。Promise的状态具有不受外界影响和不可逆两个特点,与译后的“承诺”这个词有着相似的特点。

promise的简单用法:

let p = new Promise(function(resolve, reject) {
    resolve(100);
});

p.then(function(data) {
    console.log('data', data);
}, function(err) {
    console.log('err', err);
});

复制代码

可以看到Promis通过then的链式调用解决了嵌套回调的问题,在用法上Promise的构造函数会接受一个executor函数,这个函数带有两个参数resolve和reject,两个参数背后其实就是两个函数,而通过Promise构造函数创建出来的对象会保存一个status属性,resolve会做的事就是将这个属性从初始化的pending转为resolved,而reject则是转为rejected,同时两个函数都可以接受一个参数,作为之后then中回调函数的参数传入,那么在then方法中我们可以看到它接收两个参数,第一个就是成功resolved之后会调用的回调函数,第二个就是rejected的回调函数。

这里注意的是,只要状态转为resolved或rejected之中的其中一个,那么当前promise对象就不能再转变状态了。之后不管调resolve还是reject都会被忽略。

另外,上面所说Promise是可以支持链式调用的,所以then是可以多次调用的,但是因为刚刚所说状态不可转变的问题,所以链式调用每次then返回的不是当前的Promise对象而是一个新的Promise对象,那么第2次then的状态又是怎么决定的呢,第一次then中无论是成功的回调还是失败的回调只要返回了结果就会走下一个then中的成功,如果有错误走下一个then的失败。

promise的扩展方法

Promise.all
const p = Promise.all([p1, p2, p3]);
复制代码

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理(Promise.all方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是 Promise实例)

p的状态由p1、p2、p3决定,分成两种情况. (1)只有p1、p2、p3的状态都变成fullfilled,p的状态才会变成fullfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数. (2)只要p1、p2、p3之中有一个被reject,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race
const p = Promise.race([p1, p2, p3]);
复制代码

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。 Promise.race方法的参数与Promise.all方法一样,如果不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise实例,再进一步处理。

Promise.resolve

将现有对象转为Promise对象,该实例的状态为resolved.

Promise.reject

将现有对象转为Promise对象,该实例的状态为rejected

Promise.prototype.catch

catch方法其实就是.then(null, rejection)的别名

promise库

1. Q库

可以通过npm install q安装使用

Q.fcall(function() {
    return 100;
}).then(function(data) {
    console.log(data);
})
复制代码

这里的fcall其实就类似于Promis.resolve方法

function read(url) {
    return new Promise(function(resolve, reject) {
        require('fs').readFile(url, 'utf8', function(err, data) {
            if (err) reject(err);
            resolve(data);
        });
    });
}

let Q = require('q');
Q.all([read('./2.promise/1.txt'), read('./2.promise/2.txt')]).then(function ([a1, a2]) {
    console.log(a1, a2);
});
复制代码

同样它也有类似Promise.all这样的方法

let Q = require('q');
function read(url) {
    let defer = Q.defer();
    require('fs').readFile(url, 'utf8', function(err, data) {
        if (err) defer.reject(err);
        defer.resolve(data);
    })
    return defer.promise;
}
read('./2.promise/1.txt').then(function(data) {
    console.log(data);
});

复制代码

同样也可以使用defer这样一个语法糖(关于defer会在之后的Promise实现中解释)

2.bluebird库

可以通过npm install bluebird安装

核心方法 promisify和promisifyAll

let fs = require('fs');
let bluebird = require('bluebird');

let read = bluebird.promisify(fs.readFile);
bluebird.promisifyAll(fs);
fs.readFileAsync('./2.promise/1.txt', 'utf8').then(function(data) {
    console.log(data);
});

复制代码

所做的就是将传入的方法全部转成返回是Promise对象的新函数

背后实现原理其实也很简单

function promisify(fn) { // promise化 将回调函数在内部进行处理
    return function (...args) {
        return new Promise(function (resolve, reject) {
            fn(...args, function (err, data) {
                if (err) reject(err);
                resolve(data);
            })
        })
    }
}
function promisifyAll(obj) {
    Object.keys(obj).forEach(key => { // es5将对象转化成数组的方法
        if (typeof obj[key] === 'function') {
            obj[key + 'Async'] = promisify(obj[key])
        }
    })
}

复制代码

3. Generator

简单案例

/ genrator函数要用* 来比标识,yield(暂停 产出)
// 它会将函数分割出好多个部分,调用一次next就会继续向下执行
// 返回结果是一个迭代器 迭代器有一个next方法
// yield后面跟着的是value的值
// yield等号前面的是我们当前调用next传进来的值
// 第一次next传值是无效的
function* read() {
    console.log(1);
    let a = yield 'zf';
    console.log(a);
    let b = yield 9;
    console.log(b);
    return b;
}

let it = read();
console.log(it.next('213')); // {value:'zf',done:false}
console.log(it.next('100')); // {value:9,done:false}
console.log(it.next('200')); // {value:200,done:true}
console.log(it.next('200')); // {value:200,done:true}

复制代码

generator与Promise的搭配使用例子如下

let bluebird = require('bluebird');
let fs = require('fs');

let read = bluebird.promisify(fs.readFile);

function* r() {
    let content1 = yield read('./2.promise/1.txt', 'utf8');
    let content2 = yield read(content1, 'utf8');
    return content2;
}
let it = r();
it.next().value.then(function(data) { // 2.txt
    it.next(data).value.then(function(data) {
        console.log(it.next(data).value);
    });
})

复制代码

当然我们可以看到调用时还是得这样不停的嵌套去获取值,所以这里需要引入另一个叫co的库,那么使用方法就可以变成如下:

let co = require('co');
let bluebird = require('bluebird');
let fs = require('fs');

let read = bluebird.promisify(fs.readFile);

function* r() {
    let content1 = yield read('./2.promise/1.txt', 'utf8');
    let content2 = yield read(content1, 'utf8');
    return content2;
}
co(r()).then(function(data) {
    console.log(data)
})

复制代码

co源码实现

function co(it) {
    return new Promise(function(resolve, reject) {
        function next(d) {
            let { value, done } = it.next(d);
            if (!done) {
                value.then(function (data) { // 2,txt
                    next(data)
                }, reject)
            } else {
                resolve(value);
            }
        }
        next();
    });
}

复制代码

4. async+await

async+await就是目前为至,异步的最佳解决方案,它同时解决了

  • 回调地狱
  • 并发执行异步,在同一时刻同步返回结果Promise.all
  • 返回值的问题
  • 可以实现代码的try/catch;示例代码:

示例代码:

let bluebird = require('bluebird');
let fs = require('fs');
let read = bluebird.promisify(fs.readFile);

// 用async来修饰函数,aysnc需要配await, await只能接promise
// async和await(语法糖) === co + generator
async function r() {
    try{
        let content1 = await read('./2.promise/100.txt', 'utf8');
        let content2 = await read(content1, 'utf8');
        return content2;
    } catch(e) { // 如果出错会catch
        console.log('err', e)
    }
}

// async函数返回的是promise
r().then(function(data) {
    console.log('flag', data);
}, function(err) {
    console.log(err);
})

复制代码

转载于:https://juejin.im/post/5b1dcd9e6fb9a01e4072b7a3

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值