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