Javascript 以 [同步代码] 的方式表达 [异步调用]

用作备忘, 本文详细描述了, Js中异步处理的演变, 每一步都是我手动编码验证过的.
从最原始的 callback, 到 Promise, 再到借助 Generator 最终实现了 以 [同步代码] 的方式表达 [异步调用].
因为目的比较单纯, 所以没有发散 Promise 和 Generator 更多的使用方法.

1. 最传统的用法是回调函数

function connectDatabase(connectString, callback){...}

connectDatabase 是一个异步调用, 不可能在执行函数后立刻返回数据库连接对象, 当真正连接对象创建时会回调一下callback函数, 由于连接可能创建失败, 所有当连接创建失败时也会调用callback函数, 按照约定, callback函数的第一个参数就是错误对象, 其他参数是返回值.

connectDatabase('host:localhost;user:root;password:123456', (err, conn)=>{ 
//这里才是connectDatabase异步函数有结果的地方, 能拿到错误信息, 和真正的返回值
})

如果业务场景很复杂, 使用callback会非常难以驾驭

2. 使用Promise

Promise是一个内置对象, 现代浏览器和Nodejs都已经做了内置支持, 使用方式为
connectDatabase('host:localhost;user:root;password:123456').then(conn=>{
    // 链接成功
}).catch(err=>{
    // 链接失败
})

3. 手动将 Callback 转 Promise

function connectDatabase(connectionString, callback) {
    setTimeout(() => {
        let err = null
        let conn = {}
        callback(err, conn)
    }, 100)
}   
// 手动转为Promisefunction connectDatabase_promise(connectionString) {
    return new Promise((resolve, reject) => {
        connectDatabase(connectionString, (err, conn) => {
            if (err) {
                reject(err)
            } else {
                resolve(conn)
            }
        })
    })
}   
// 调用
connectDatabase_promise("host:localhost;user:root;password:123456").then((conn)=>{
    console.log('连接成功', conn)
}).catch((err)=>{
    console.log('连接失败', err)
})

4. 实现promisify工具函数

// 函数 参数 callback 前有不止一个参数
function connectDatabase(connectionString, param1, param2, callback) {
    setTimeout(() => {
        console.log(connectionString, param1, param2)
        let err = null
        // err = 'error'            // 解开注释, 可以触发 Promise 的 reject
        let conn = {}
        // 注: 这里的写法在转Promise时是有问题的, 不支持多个参数, 只支持 两个参数(err, result), 
        //     因为 Promise 的 回调函数 resolve(result) 和 reject(err) 都只认一个参数
        callback(err, conn, 'result1', 'result2')
    }, 100)
}
// 没有参数
function getWeather(callback){
    setTimeout(()=>{
        let err = null
        // err = 'error'            // 解开注释, 可以触发 Promise 的 reject
        let result = '晴天'
        callback(err, result)
    })
}
function promisify(func) {
    return function () {        // 注: 这里不能使用()=>{}, 因为箭头函数里的arguments为上级函数的参数
        return new Promise((resolve, reject) => {
            // 注: 这里的 arguments 是再上一层函数的参数
            func(...arguments, (err, ...results) => {
                // 这里的写法有问题
                // 这里即使是 使用 ...results 也无法传递多个参数
                // 正确的做法是, reject 时, 额外信息都放在 err 对象中, resolve 时参数都压到数组中
                err ? reject(err, ...results) : resolve(...results)
            })
        })
    }
}
const connectDatabase_promise = promisify(connectDatabase)
const getWeather_promise = promisify(getWeather)
// 调用
connectDatabase_promise("host:localhost;user:root;password:123456", 1, 2).then((conn, result1, result2) => {
    console.log('连接成功', conn, result1, result2)         // result1 和 result2 并没有 传过来
    return getWeather_promise()                             // 关键点: 返回又一个 Promise 实现鱼刺调用. 一直 then 下去
}).then((result)=>{
    console.log('获取天气成功', result)
}).catch((err, result0, result1, result2) => {
    console.log('错误', err, result0, result1, result2)   // result0 result1 和 result2 并没有 传过来
})

5. 也可以使用es6-promisify库中提供的promisify工具函数, 测试后效果一致

npm install es6-promisify --save
const {promisify} = require("es6-promisify");

6. 使用Promise的resolve方法构造 Promise 对象

  • 参数是Promise:原样返回
  • 参数带有then方法:转换为Promise后立即执行then方法
  • 参数不带then方法、不是对象或没有参数:返回resolved状态的Promise
function connectDatabase(connectionString, callback) {
    setTimeout(() => {
        let err = null
        // err = 'error'            // 解开注释, 可以触发 Promise 的 reject
        let conn = {}
        callback(err, conn)
    }, 100)
}
const connectionString = "host:localhost;user:root;password:123456"
// 参数为 非 Promise, 非 thenable, 直接返回 resolved 状态的 Promise
Promise.resolve(connectionString).then(connectionString=>{
    console.log('before Promise.resolve called')
    // 参数为 thenable, 构造Promise, 并立即(其实并不是同步的)执行 then 方法
    let promise = Promise.resolve({then:(resolve, reject)=>{
        console.log('then method called')
        connectDatabase(connectionString, (err, conn)=>{
            err ? reject(err) : resolve(conn)
        })
    }})
    console.log('after Promise.resolve called')
    return promise
}).then(conn=>{
    console.log('连接成功', conn)
}).catch(err=>{
    console.log('错误', err)
})
console.log('code end')
输出:
code end
before Promise.resolve called
after Promise.resolve called
then method called
连接成功
Object {}

7. Generator, 由 js 内核支持的新的语言特性

function* gen() {
    yield 1             // 函数协程挂起, 等待下次 next 的调用
    yield 2             // 函数协程挂起, 等待下次 next 的调用
    return 3            // 遇到 return 协程结束
}
let itor = gen()
console.log(itor.next().value)      // next().done 为 false, 输出 1
console.log(itor.next().value)      // next().done 为 false, 输出 2
console.log(itor.next().value)      // next().done 为 true, 输出 3

8. Generator 对象的 next 方法可以有参数, 它将作为 yield 表达式的结果

function* gen() {
    let a = yield 1
    console.log('a:', a)        // a = 100
    let b = yield 2 + a
    console.log('b:', b)        // b = 3389
    return b * 10
}
let itor = gen()
let c = itor.next().value
console.log('c:', c)                // c = 1
let d = itor.next(c * 100).value;
console.log('d:', d)                // d = 102
let e = itor.next(3389).value;
console.log('e:', e)                // e = 33890

9. 尝试结合Generator和Promise使用, 能达到异步代码同步编写的效果

const { promisify } = require("es6-promisify");

// 异步处理 data - 1
function getData1(data, callback) {
    setTimeout(() => {
        callback(null, data - 1)
    }, 1)
}

// 异步处理 data + 2
function getData2(data, callback) {
    setTimeout(() => {
        let err = null
        // err = 'err2'     // 解除注释 可以看到catch方法被调用
        callback(err, 2 + data)
    }, 1)
}

// 异步处理 data * 3
function getData3(data, callback) {
    setTimeout(() => {
        callback(null, 3 * data)
    }, 1)
}

// 这是一个 Generator 的执行器, 可以不断的执行一个Generator直到donetrue
function myCo(gen) {
    // 创建 Generator
    let itor = gen()

    // 关键: 用于保存 全局 的 Promiseresolvereject 方法
    let co_resolve, co_reject

    // 执行 next
    const executeNext = function (param) {

        // 调用 next, 外部的 yield 代码变为非阻塞, 但目前还没有返回值, 需要下次next时给出
        // next的返回值为 { done: bool, value: Promise }
        let ret = itor.next(param)

        // 判断 是否 done
        if (ret.done) {
            // 如果 done, 则立刻结束, 标记全局 Primoseresolved
            // 这时 外部调用 myCo 的返回值的 then 方法将会获得参数(执行的结果), 并执行
            co_resolve(ret.value)
        } else {
            // 还没有 done
            // 因为 ret.value 是一个Promise, 所有要运行它, 就要调用 then...catch
            ret.value.then(result => {
                // 如果 Promise 执行成功, 则继续 调用 next
                // 主要 要将结果告知外部, 使用next的参数, 给出 yield 的返回值
                executeNext(result)
            }).catch(err => {
                // 如果失败, 就提前结束, 标记全局 Primose 为 rejected
                // 这时 外部调用 myCo 的返回值的 catch 方法 将会 获得错误信息, 并执行其 catch 中的代码
                co_reject(err)
            })
        }
    }

    // 这里很关键
    // myCo 函数显然不是同一个同步完成的函数, 需要获得最终的结果或者错误, 必须依靠 Promise
    // 所有这里返回了一个 Promise 使得外部能 then...catch 结果
    // Promise.resolve 的好处是能立即(非同步)执行then方法
    return Promise.resolve({
        then: (resolve, reject) => {
            // 关键点: 
            // 将这个最顶层的 resolve 和 reject 保存在 myCo闭包全局变量中, 能使 任意一级 executeNext 有机会能 返回结果 或是 抛出错误
            co_resolve = resolve
            co_reject = reject
            // 执行 next
            // 注: 这里的没有给参数, 是因为第一次next不需要参数
            executeNext()
        }
    })
}

// 最终效果
myCo(function* () {
    // promisify(getData1) 的结果是 一个 Promise Creator
    // promisify(getData1)(2) 的结果 是 一个 Promise
    // yield promisify(getData1)(2) 会等待 generator 调用 next
    // next 的返回值 中就包含了 刚刚的 Promise, 注: next 返回 { done: bool, value: obj }
    let a = yield promisify(getData1)(2)
    console.log('a:', a)
    let b = yield promisify(getData2)(a)
    console.log('b:', b)
    let c = yield promisify(getData3)(b)
    console.log('c:', c)
    return c + 1000 

    // 也可以更加嚣张的鱼刺调用, 注: yield 语句的范围最好用 括号明确 框定
    // return (yield promisify(getData3)(yield promisify(getData2)(yield promisify(getData1)(2)))) + 1000
}).then(result => {
    console.log('result:', result)
}).catch((err) => {
    console.log('error:', err)
})
console.log('end')

10. 也可以使用npm-co工具类实现, 效果一致, co的写法更加严谨

  • 考虑到了参数是Generator 还是 Generator 函数调用的返回值
  • 更合理的使用了Promise作封装
  • 更严谨的错误处理, 比如对 next 调用作了 try…catch包装
 npm install co --save
 const co = require('co')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值