Promises 奇怪的无限递归行为

Strange infinite recursion behavior with Promises

我创建了一个 NodeJS 程序(使用 Bluebird 作为 Promise 库)来处理一些类似于下面代码片段的验证,但是如果我 运行 该脚本会抛出以下错误:

Unhandled rejection RangeError: Maximum call stack size exceeded

显然,在我使用 .bind(ctx)

的验证函数重新分配时,它正在执行一些递归函数调用

我解决这个问题的方法是将 Promise 工厂分配给 obj._validate 而不是重新分配 obj.validate 并在需要的地方使用 _validate(ctx)

但我仍然不明白为什么会发生该错误。有人可以给我解释一下吗?

// Example validation function
function validate(pass, fail) {
  const ctx = this
  Promise.resolve(ctx.value) // Simulate some async validation
    .then((value) => {
      if (value === 'pass') pass()
      if (value == 'fail') fail('Validation failed!')
    })
}

let validations = [
  {name: 'foo', validate: validate},
  {name: 'bar', validate: validate},
  {name: 'baz', validate: validate},
  {name: 'qux', validate: validate}
]

// Reassigning validate functions to a promise factory
// to handle async validation
validations.forEach(obj => {
  obj.validate = (ctx) => { // ctx used as context to validation
    return new Promise(obj.validate.bind(ctx))
  }
})

function executeValidations(receivedValues, validations) {
  receivedValues.forEach((obj, i) => {
    validations[i].validate(obj) // obj becomes the context to validate
      .then(() => console.log('Validation on', obj.name, 'passed'))
      .catch(e => console.error('Validation error on', obj.name, ':', e))
  })
}

let receivedValues1 = [
  {name: 'foo', value: 'pass'},
  {name: 'bar', value: 'fail'},
  {name: 'baz', value: 'fail'},
  {name: 'qux', value: 'pass'},
]

executeValidations(receivedValues1, validations)

let receivedValues2 = [
  {name: 'foo', value: 'pass'},
  {name: 'bar', value: 'pass'},
  {name: 'baz', value: 'fail'},
  {name: 'qux', value: 'fail'},
]

executeValidations(receivedValues2, validations)
<script src="//cdn.jsdelivr.net/bluebird/3.4.7/bluebird.js"></script>

编辑:我认为这是问题的简短版本

function fn(res, rej) { return this.foo }

fn = function(ctx) { return new Promise(fn.bind(ctx))}

const ctx = {foo: 'bar'}
fn(ctx)
  .then(console.log)
<script src="//cdn.jsdelivr.net/bluebird/3.4.7/bluebird.js"></script>

    obj.validate.bind(ctx)

求值为 this 值设置为 ctx 的奇异函数对象。它在很大程度上仍然是一个函数对象。

看来

    obj.validate = (ctx) => { // ctx used as context to validation
    return new Promise(obj.validate.bind(ctx))

obj.validate 设置为一个函数,该函数 returns 在其构造期间 同步 调用其解析器函数 obj.validate.bind(ctx) 的承诺(a.k.a. "executor function" in ES6) 其中 returns 其构造同步调用 obj.validate.bind(ctx) 的 promise 对象,依此类推无限或 JavaScript 引擎抛出错误。

因此,调用 obj.validate 第一次启动由解析器函数生成承诺的无限循环。

bind 用法的进一步问题:

箭头函数在声明时绑定它们的词法 this 值。语法上 Function.prototype.bind 可以应用于箭头函数 但不会更改箭头函数看到的 this 值!

因此,如果方法是使用箭头函数定义的,obj.validate.bind(ctx) 永远不会更新在 obj.validate 中看到的 this 值。


编辑:

最大的问题可能是覆盖执行操作的函数的值:

已发布:

    validations.forEach(obj => {
      obj.validate = (ctx) => { // ctx used as context to validation
        return new Promise(obj.validate.bind(ctx))
      }

覆盖每个 validations 条目的 validate 属性。这个属性曾经是开头声明的命名函数validate,现在不是了。

在短版中,

    function fn(res, rej) { return this.foo }

    fn = function(ctx) { return new Promise(fn.bind(ctx))}

    const ctx = {foo: 'bar'}
    fn(ctx)

fn = function... 覆盖 fn 的命名函数声明。这意味着当稍后调用fn时,fn.bind(ctx)fn指的是fn的更新版本,而不是原来的

另请注意,解析器函数必须调用其第一个函数参数 (resolve) 以同步解析新的承诺。 Return 解析器函数的值被忽略。

executeValidations() 期望 validate() return 一个承诺,所以最好 return 一个承诺。当验证过程中出现问题时拒绝承诺是有用的,但验证测试失败是验证过程的正常部分,而不是错误。

// Example validation function
function validate(ctx) {
    return new Promise((resolve, reject) => {
        // Perform validation asynchronously to fake some async operation
        process.nextTick(() => {
            // Passing validations resolve with undefined result
            if (ctx.value === 'pass') resolve()
            // Failing validations resolve with an error object
            if (ctx.value == 'fail') resolve({
                name: ctx.name,
                error: 'Validation failed!'
            })
            // Something went wrong
            reject('Error during validation')
        })
    })
}

现在 executeValidations() 可以将验证映射到错误列表

function executeValidations(receivedValues, validations) {
    // Call validate for each received value, wait for the promises to resolve, then filter out any undefined (i.e. success) results
    return Promise.all(receivedValues.map( obj => validate(obj)))    
            .then(results => results.filter(o => o !== undefined))
}

如果没有错误则验证成功...

executeValidations(receivedValues1, validations)
.then (errors => {
    if (!errors.length)
        console.log('Validations passed')
    else
        errors.forEach(error => console.error(error))
})

executeValidations(receivedValues2, validations)
.then (errors => {
    if (!errors.length)
        console.log('Validations passed')
    else
        errors.forEach(error => console.error(error))
})