使用Promises的奇怪的无限递归行为

时间:2017-01-08 20:52:46

标签: javascript node.js recursion promise bluebird

我创建了一个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>

2 个答案:

答案 0 :(得分:0)

    obj.validate.bind(ctx)

评估异常函数对象,其this值设置为ctx。它仍然是一个功能对象。

然后出现

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

obj.validate设置为一个函数,该函数返回一个promise,在其构造期间同步调用其解析器函数obj.validate.bind(ctx)(在ES6中也称为“executor function”),它返回一个promise其结构同步调用obj.validate.bind(ctx)的对象,等等无限广告或JavaScript引擎会引发错误。

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

使用bind的另一个问题:

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

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

<小时/> 编辑:

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

发布时间:

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

会覆盖每个validate条目的validations属性。此属性曾经是在开始时声明的命名函数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的{​​{1}}会引用fn.bind(ctx)的更新版本,而不是原始版本。

另请注意,解析器函数必须调用其第一个函数参数(fn)来同步解析新的promise。忽略旋转变压器功能的返回值。

答案 1 :(得分:0)

executeValidations()要求validate()返回一个promise,因此最好返回一个promise。在验证过程中出现问题时拒绝承诺很有用,但验证测试失败是验证过程的正常部分,而不是错误。

import java.awt.BorderLayout;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JComboBox;
import javax.swing.JFrame;

class Item {
  int intValue;
  String strValue;

  public Item(int intValue, String strValue) {
    this.intValue = intValue;
    this.strValue = strValue;
  }

  public String toString() {
    return intValue + " - " + strValue;
  }
}

public class TestCombo {
  private static JComboBox<Item> cb;
  public static void main(String[]args) {
    JFrame f = new JFrame();
    f.setSize(640,400);
    cb = new JComboBox<>();
    cb.addItem(new Item(1, "one"));
    cb.addItem(new Item(2, "two"));
    cb.addItem(new Item(3, "three"));
    f.getContentPane().setLayout(new BorderLayout());
    f.getContentPane().add(cb, BorderLayout.NORTH);
    f.setVisible(true);

    selectItemByString("three");
  }

  private static void selectItemByString(String s) {
    for (int i=0; i<cb.getItemCount(); i++) {
      if (cb.getItemAt(i).strValue.equals(s)) {
        cb.setSelectedIndex(i);
        break;
      }
    }
    return;
  }
}

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

// 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')
        })
    })
}

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

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))
}