异步编程解决方案全集—promise、generator+co、async+await

一、前言

传统的解决代码单线程执行的方案是回调函数和事件。这是个解决问题的方案,但是会造成回调地狱。

异步编程是优化代码逻辑提高代码易读性的关键。

目前通用的异步编程方法有三种:

  1. Promise
  2. generator+co
  3. async+await

这三种方法我都经常在用,但是对它们的原理却一知半解。于是想炒个冷饭从头到尾理一遍,梳理一下它们之间的关系。

二、Promise

2.1 原理

Promise对象是一个构造函数,用来生成Promise实例。

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

Promise函数的两个参数分别是resolvereject。它们是Promise中定义的两个函数,在运行自定义函数时返回。

resolve函数将Promise对象的状态从 pending 变为resolved,reject将Promise对象的状态从 pending 变为rejected

Promise的原型链上定义了then方法,提供两个回调函数分别捕获resolve、reject返回的值。

2.2 静态方法
方法 描述
Promise.resolve(promise); 返回 promise(仅当 promise.constructor == Promise 时)
Promise.resolve(thenable); 从 thenable 中生成一个新 promise。thenable 是具有 then() 方法的类似于 promise 的对象。
Promise.resolve(obj); 在此情况下,生成一个 promise 并在执行时返回 obj。
Promise.reject(obj); 生成一个 promise 并在拒绝时返回 obj。为保持一致和调试之目的(例如堆叠追踪), obj 应为 instanceof Error。
Promise.all(array); 生成一个 promise,该 promise 在数组中各项执行时执行,在任意一项拒绝时拒绝。
Promise.race(array); 生成一个 Promise,该 Promise 在任意项执行时执行,或在任意项拒绝时拒绝,以最先发生的为准。

sample 1

let p1 = new Promise((resolve,reject)=>{
    console.log('hello')
    setTimeout(function () {
        reject('1212')
    },1000)
})

p1.then(data=> {
    console.log('success'+data)
},err=>{
    console.log('err'+err)
})

p1.then(data=> {
    console.log('success'+data)
},err=>{
    console.log('err'+err)
})
复制代码

terminal:

hello
err1212
err1212
复制代码

sample 1 中新建了一个Promise实例,定时1S后使用reject方法,将Promise实例的状态从pending变成rejected,触发then的err捕捉回调函数。

在sample 1 中调用then方法,并不会马上执行回调。是等待实例中状态改变后才会执行。这一点和发布订阅模式很类似。

sample 2

let fs = require('fs')

let event = {
    arr:[],
    result:[],
    on(fn){
        this.arr.push(fn)
    },
    emit(data){
        this.result.push(data)
        this.arr.forEach(fn=>fn(this.result))
    }
}

event.on(function (data) {
    if(data.length === 2){
        console.log(data)
    }
})

fs.readFile('1.txt','utf8',function (err,data) {
    event.emit(data)
})
fs.readFile('2.txt','utf8',function (err,data) {
    event.emit(data)
})
复制代码

smaple2 中将结果data放入暂存数组中,在执行接听函数的时候返回。

2.3 简写Promise源码

通过之前的例子和对发布订阅模式的理解,我们可以大概写出Promise实例的基本功能:

code 1:

function Promise(executor) {
    let self = this
    self.value = undefined
    self.reason = undefined
    self.status = 'pending'
    self.onResovedCallbacks = []
    self.onRejectedCallbacks = []
    function resolve(data) {
        if(self.status === 'pending'){
            self.value = data
            self.status = 'resolved'
            self.onResovedCallbacks.forEach(fn=>fn())
        }
    }
    function reject(reason) {
        if(self.status === 'pending') {
            self.reason = reason
            self.status = 'reject'
            self.onRejectedCallbacks.forEach(fn=>fn())
        }
    }
    //如果函数执行时发生异常
    try{
        executor(resolve,reject)
    }catch (e){
        reject(e)
    }
}

Promise.prototype.then = function (onFulfilled,onRejected) {
    let self = this
    if(self.status === 'pending'){
        self.onResovedCallbacks.push(()=>{
            onFulfilled(self.value)
        })
        self.onRejectedCallbacks.push(()=>{
            onRejected(self.reason)
        })
    }else if(self.status === 'resolved'){
        onFulfilled(self.value)
    }else if(self.status === 'reject'){
        onRejected(self.reason)
    }

}

module.exports = Promise
复制代码
  • 函数内部变量
    • status:储存Promise的状态
    • onResovedCallbacks:储存Promise pending状态下成功回调函数
    • onRejectedCallbacks:储存Promise pending状态下失败回调函数
    • resolve函数
    • reject函数
  • Promise.prototype.then
    • 根据实例状态执行响应的回调
    • status == pending使用发布订阅模式储存回调函数。
2.4 Promise用法简述
  1. 如果一个promise执行完后,返回的还是一个Promise对象,会把这个promise的执行结果,传递给下一个then中。
let fs = require('fs')

function read(filePath,encoding) {
    return new Promise((resolve,reject)=>{
        fs.readFile(filePath,encoding,(err,data)=> {
            if(err) reject(err)
            resolve(data)
        })
    })
}

read('1.txt','utf8').then(
    f1=>read(f1,'utf8') // 1
).then(
    data=> console.log('resolved:',comments)
    err=> console.log('rejected: ',err)
)
复制代码
  1. 如果then中返回的不是promise,是一个普通值,会将这个普通值作为下一个then的返回结果。
 ......
 
read('1.txt','utf8').then(
    f1=>read(f1,'utf8')
).then(
    return 123 //2
).then(
    data=> console.log('resolved:',comments)
    err=> console.log('rejected: ',err)
)
复制代码
  1. 如果当前then中失败了会走下一个then的失败。
 ......
 
read('1.txt','utf8').then(
    f1=>read(f1,'utf8')
).then(
    return 123
).then(
    throw new Error('出错') //3
).then(
    data=> console.log('resolved:',comments)
    err=> console.log('rejected: ',err)
)
复制代码
  1. 如果返回的是undefined不管当前是失败还是成功,都会走下一次成功。
  2. catch是错误没有处理的情况下会走。
  3. then中可以不写。
 ......
 
read('1.txt','utf8').then(
    f1=>read(f1,'utf8')
).then(
    return 123
).then(
    throw new Error('出错') 
).then() //6
 .then(
    data=> console.log('resolved:',comments)
    err=> console.log('rejected: ',err)
)
复制代码

这些用法中最重要的是promise的then链式调用。 可以大致猜到,旧Promise的then方法返回的是一个新的Promise对象。

参考Promises/A+规范,可以完善手写的Promise源码使其支持promise的静态方法和调用规则。

code 2:

function Promise(executor) {
    let self = this
    self.value = undefined
    self.reason = undefined
    self.status = 'pending'
    self.onResovedCallbacks = []
    self.onRejectedCallbacks = []
    function resolve(value) {
        if (self.status === 'pending') {
            self.value = value
            self.status = 'resolved'
            self.onResovedCallbacks.forEach(fn=>fn())
        }
    }

    function reject(reason) {
        if (self.status === 'pending') {
            self.reason = reason
            self.status = 'rejected'
            self.onRejectedCallbacks.forEach(fn=>fn())
        }
    }

    //如果函数执行时发生异常
    try {
        executor(resolve, reject)
    } catch (e) {
        reject(e)
    }
}


function resolvePromise(promise2, x, resolve, reject) {
    //If promise and x refer to the same object, reject promise with a TypeError as the reason.
    if (promise2 === x) {
        return reject(new TypeError('chaining cycle'))
    }
    let called
    //2.3.3.Otherwise, if x is an object or function,
    if (x !== null && (typeof x == 'object' || typeof x === 'function')) {
        try {
            let then = x.then
            //2.3.3.3.If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where:
            //2.3.3.3.3.If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
            if (typeof then === 'function') {
                then.call(x, y=> {
                    if (called) return;
                    called = true;
                    //递归直到解析成普通值为止
                    //2.3.3.1.If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
                    resolvePromise(promise2, y, resolve, reject)
                }, err=> {
                    if (called) return;
                    called = true;
                    reject(err)
                })
            } else {
                resolve(x)
            }
        } catch (e) {
            if (called) return;
            called = true;
            //2.3.3.3.If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
            reject(e)
        }
    } else {
        //If x is not an object or function, fulfill promise with x.
        resolve(x)
    }
}
//then调用的时候 都是异步调用 (原生的then的成功或者失败 是一个微任务)
Promise.prototype.then = function (onFulfilled, onRejected) {
    //成功和失败的函数 是可选参数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val=>val;
    onRejected = typeof onRejected === 'function' ? onRejected : (e)=> {throw e};
    let self = this
    let promise2;
    promise2 = new Promise((resolve, reject)=> {
        if (self.status === 'resolved') {
            setTimeout(()=> {
                try {
                    let x = onFulfilled(self.value)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            }, 0)
        } else if (self.status === 'rejected') {
            setTimeout(()=> {
                try {
                    let x = onRejected(self.reason)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            }, 0)
        } else if (self.status === 'pending') {
            self.onResovedCallbacks.push(()=> {
                setTimeout(()=> {
                    try {
                        let x = onFulfilled(self.value)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        //当执行成功回调的时候,可能会出现异常,那就用这个异常作为promise2的错误结果
                        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 (规范要求)

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 (onReject) {
    return this.then(null,onReject)
}

Promise.defer = Promise.deferred = function () {
    let dfd = {}
    dfd.promise = new Promise((resolve, reject)=> {
        dfd.resolve = resolve
        dfd.reject = reject
    })
    return dfd;
}

module.exports = Promise
复制代码
  1. 为了支持then的链式调用,Promise.then.prototype中返回一个新的Promise对象
return p2 = new Promise()
复制代码

2.增加resolvePromise方法,处理旧Promise的回调函数的结果x,根据x的类型,分别调用新promise对象的resolve/reject方法。

  • 是普通值用resolve方法返回
  • 是函数或者对象就继续用resolvePromise方法迭代(解决回调函数是Promise对象)
  • 出错就用reject方法返回

三、bluebird

1: NodeJS 中的 fs.readFile 方法的基本使用方式

const fs = require('fs'),path = require('path');
 
fs.readFile(path.join(__dirname, '1.txt'), 'utf-8', (err, data) => {
 if (err) {
   console.error(err);
 } else {
   console.log(data);
 }
});
复制代码

2:使用Promise封装

let fs = require('fs')

function read(filePath, encoding) {
    return new Promise((resolve, reject)=> {
        fs.readFile(filePath, encoding, (err, data)=> {
            if (err) reject(err)
            resolve(data)
        })
    })
}

read('1.txt', 'utf8').then( data=> data)
复制代码

把fs.readFile方法用Promise封装一下就能使用Promise api。但是每次手动封装比较麻烦,bluebird可以帮我们简化这个步骤。

3:在 NodeJS 环境中,通过 const bluebird = require('bluebird') 就可以开始使用 Bluebird 提供的 Promise 对象。

Promise.promisify 将单个方法转换成Promise对象。

const bluebird = require('bluebird') 
let read = bluebird.promisify(fs.readFile)
read('1.txt', 'utf-8').then(data=> {
    console.log('data promisify', data)
})
复制代码

使用bluebird.promisify方法,就能将fs.readFile直接封装成一个promise对象,它的原理很简单,return new Promise 是它的核心:

function promisify(fn) {
    return function () {
        return new Promise((resolve, reject)=> {
            fn(...arguments, function (err, data) {
                if (err) reject(err)
                resolve(data)
            })
        })
    }
}
复制代码

4.使用 Promise.promisifyAll 把一个对象的所有方法都自动转换成使用 Promise。

const bluebird = require('bluebird'),
  fs = require('fs'),
  path = require('path');
Promise.promisifyAll(fs);
 
fs.readFileAsync(path.join(__dirname, 'sample.txt'), 'utf-8')
  .then(data => console.log(data))
  .catch(err => console.error(err));
复制代码

promisifyAll核心是遍历对象,生成些新创建方法的名称在已有方法的名称后加上"Async"后缀。

function promisifyAll(obj) {
    Object.keys(obj).forEach(key=>{
        if(typeof obj[key] === 'function'){
            obj[key+'Async'] = promisify(obj[key])
        }
    })
}
复制代码

四、generator+co

4.1 简介

generator函数最大的特点是可以用yield暂停执行,为了区别普通函数在函数名前加*号。

function *say() {
    let a = yield "test1"
    let b = yield "test2"
}

let it = say();

console.log(1, it.next()) //1 { value: 'test1', done: false }
console.log(2, it.next()) //2 { value: 'test2', done: false }
console.log(3, it.next()) //3 { value: undefined, done: true }
复制代码

执行say()方法返回的是指针对象,不会返回函数执行结果。it 就是iterator 迭代器

需要调用指针对象的next()方法,让函数指针不断移动并返回一个对象。({value:xxx,done:xxx})

value是yield后面的值,done表示函数是否执行完成。

我们可以用generator函数实现结果的产出,但是也需要它支持输入。

generator函数的运行顺序如下:

使用it.next()执行函数,结果并不会返回给定义的变量a。next方法可以接受参数,这是向 Generator 函数体内输入数据。 第二个next的时候传入参数,就能被变量a接收到。

terminal 返回:

1 { value: 'test1', done: false }
aaa
2 { value: 'test2', done: false }
bbb
3 { value: undefined, done: true }
复制代码

4.2 使用

example:使用generator异步执行函数,使函数的返回作为下一个函数的入参执行。

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

function *r() {
    let r1 = yield read('1.txt', 'utf-8')
    console.log('r1',r1); // r1 2.txt
    let r2 = yield read(r1, 'utf-8')
    console.log('r2',r2); // r2 3.txt
    let r3 = yield read(r2, 'utf-8')
    console.log('r3',r3); // r3 hello
    return r3
}
复制代码

拿读取文件的例子:使用bluebird将fs.readFile变成promise对象,将读取到的文件内容作为入参传入下一个要执行的函数。

突然发现,要拿到结果会是个复杂的过程,但还是硬着头皮下下去:

const it_r = r()
it_r.next().value.then(d1=>{
    return it_r.next(d1).value
}).then(d2=>{
    return it_r.next(d2).value
}).then(d3=>{
    return it_r.next(d3).value
}).then(data=>{
    console.log(data) // hello
})
复制代码

it.next().value 返回的是一个promise,使用then方法,拿到它成功回调的值,并传入下一个next。

这样能成功拿到我们要的值,但是太麻烦了。于是就有了generator+co的组合!

安装:

$ npm install co
复制代码

使用:

co(r()).then(data=> {
    console.log(data)
})
复制代码

co会迭代执行it.next()方法,直到done的布尔值为true就返回generator函数的运行结果。

大致执行代码如下:

function co(it) {
    return new Promise((resolve, reject)=> {
        function next(data) {
            let {value, done} = it.next(data)
            if(done){
                resolve(value)
            }else{
                value.then(data=> {
                    next(data)
                },reject)
            }
        }
        next()
    })
}
复制代码

五、async+await

async 函数是Generator 函数的语法糖。

比Generator函数用起来简单

  1. 可以让代码像同步
  2. 可以try+catch
  3. 可以使用promise api
async function r() {
   try{
        let r1 = await read('1.txt','utf8')
        let r2 = await read(r1,'utf8')
        let r3 = await read(r2,'utf8')
        return r3
    }catch(e){
        console.log('e',e)
    }
}

r().then(data=> {
    console.log(data)
},err=>{
    console.log('err',err)
})
复制代码

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。遇到await就会先返回,等待函数执行。

参考

猜你喜欢

转载自juejin.im/post/5bc7d9bef265da0af879a293