原生es6实现 promisify

1,155 阅读2分钟

promisify化mysql模块的connection.query函数. 在学习mysql模块的时候需要大量使用connection.query, 在回调里拿查询结果的方式非常不优雅, node的原生util库提供了一个promisify方法将普通函数Promise化, 非常方便, 但发现有一个缺点, 只能拿到回调的第一二个参数, 于是尝试自己使用es6实现 promisify, 并优化这个地方

准备 - 创建mysql连接

// 创建连接
const connection = mysql.createConnection({
    user: 'root',
    password: 'password',
})
connection.connect()

回调写法

  connection.query('show databases;', (err, results, fields) => {
    if(err) {
      console.error('Query Fail!')
      throw err
    } else {
      console.log('Query Success!')
      console.table(results)
      console.log(fields)
    }
  })

成功拿到结果, OK 可以继续了

Query Success!
┌─────────┬──────────────────────┐
│ (index) │       Database       │
├─────────┼──────────────────────┤
│    0    │ 'information_schema' │
│    1    │       'mysql'        │
│    2    │ 'performance_schema' │
│    3    │        'sys'         │
│    4    │        'test'        │
└─────────┴──────────────────────┘

实现promisify

/**
 * Promise化一个带回调的普通函数
 * 回调函数的第一个参数必须是Error
 * @param {Funciton} fn function to call
 * @return 返回一个Promise的函数, 即Promise化
 * 在返回的Promise里面处理fn的回调函数
 */
function promisify(fn) {
  return function(...args) {
    let _args = args.slice()// 拷贝一份参数, 原则上不应该修改传入参数的值
    return new Promise( (resolve, reject) => {// 这里必须使用箭头函数, 为了获得真正的上下文
      function cb(err, ...results) { // 处理回调结果
        if (err) {
          reject(err)
        } else {
        // 这里util.promisify只会传err之后的第一个参数, 相当于resolve(result[0])
        // 给resolve传递了err之外的其他所有参数, 解决缺陷!
          resolve(results)
        }
      }
      _args.push(cb)
      fn.apply(this, _args)
      // fn.call(this, ..._args) // 也可以call
    })
  }
}

使用promisify

async function testPromisify() {
  const queryAsync = promisify(connection.query)
  const [results, fields] = await queryAsync('show databases;') // 抛出异常
  console.log('Query Success!')
  console.table(results)
  console.log(fields)
}

发现抛出异常

[unhandledRejection] TypeError: Cannot read property 'typeCast' of undefined
    at query (E:\mygit\exercise\node_modules\mysql\lib\Connection.js:201:34)
    at Promise (E:\mygit\exercise\server\promisify.js:22:10)
    at new Promise (<anonymous>)
    at E:\mygit\exercise\server\promisify.js:13:12
    at testPromisify (E:\mygit\exercise\server\promisify.js:48:35)
    at Object.<anonymous> (E:\mygit\exercise\server\promisify.js:72:1)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)

查看mysql源码 E:\mygit\exercise\node_modules\mysql\lib\Connection.js:201:34, 发现原来是上下文不对

Connection.prototype.query = function query(sql, values, cb) {
  var query = Connection.createQuery(sql, values, cb);
  query._connection = this;

  console.log('this === global', this === global); // true
  console.log('this.config', this.config); // undefined
  if (!(typeof sql === 'object' && 'typeCast' in sql)) {
    query.typeCast = this.config.typeCast; // 这里的this, 打印了一下发现指向global
  }
  // ...
};

为什么会这样呢? 因为... query函数Promise化后失去了宿主环境(context)

// query 的 this 指向 connection
  connection.query('show databases;', (err, results, fields) => {  ... });
// queryAsync 的 this 指向 global
  const queryAsync = promisify(connection.query)
  const [results, fields] = await queryAsync('show databases;')

解决方法很简单: 绑定context

  const queryAsync = promisify(connection.query)
  const [results, fields] = await queryAsync.call(connection, 'show databases;')
  // or
  connection.queryAsync = promisify(connection.query)
  const [results, fields] = await connection.queryAsync('show databases;')

总结

最终代码为

function promisify(fn) {
  return function(...args) {
    let _args = args.slice()
    return new Promise( (resolve, reject) => {
      function cb(err, ...results) {
        err ? reject(err) : resolve(results)
      }
      _args.push(cb)
      fn.apply(this, _args)
    })
  }
}
const connection = mysql.createConnection({
    user: 'root',
    password: 'password',
})
connection.connect()
async function testPromisify() {
  connection.queryAsync = promisify(connection.query)
  const [results, fields] = await connection.queryAsync('show databases;')
  console.log('Query Success!')
  console.table(results)
  console.log(fields)
}
// 因为 await并未处理异常, 处理一下全局异常
process.on('uncaughtException', (err) => {
  console.log('[uncaughtException]', err)
})
process.on('unhandledRejection', (err) => {
  console.log('[unhandledRejection]', err)
})

非常好, 满意!