[英]Node.js — Maximum call stack size exceeded, even with process.nextTick()
我正在尝试编写用于“可链接” Express.js验证的模块:
const validatePost = (req, res, next) => {
validator.validate(req.body)
.expect('name.first')
.present('The parameter is required')
.string('The parameter must be a string')
.go(next);
};
router.post('/', validatePost, (req, res, next) => {
return res.send('Validated!');
});
validator.validate
的代码(为简便起见简化):
const validate = (data) => {
let validation;
const expect = (key) => {
validation.key = key;
// Here I get the actual value, but for testing purposes of .present() and
// .string() chainable methods it returns a random value from a string,
// not string and an undefined
validation.value = [ 'foo', 123, void 0 ][Math.floor(Math.random() * 3)];
return validation;
};
const present = (message) => {
if (typeof validation.value === 'undefined') {
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
return validation;
};
const string = (message) => {
if (typeof validation.value !== 'string') {
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
return validation;
};
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
// I even wrap async callbacks in process.nextTick()
process.nextTick(() => next(error));
}
process.nextTick(next);
};
validation = {
valid: true,
data: data,
errors: [],
expect: expect,
present: present,
string: string,
go: go
};
return validation;
};
该代码可用于短链,并返回适当的错误对象。 但是,如果我链接了很多方法,请说:
const validatePost = (req, res, next) => {
validator.validate(req.body)
.expect('name.first')
.present('The parameter is required')
.string('The parameter must be a string')
.expect('name.first') // Same for testing
.present('The parameter is required')
.string('The parameter must be a string')
// [...] 2000 times
.go(next);
};
Node.js引发RangeError: Maximum call stack size exceeded
。 请注意,我将异步回调.go(next)
包装在process.nextTick()
。
我没有太多时间去看这个,但是我确实注意到了一个很大的问题。 您有一个单分支if语句,当!validator.valid
为true
时,该语句导致next
调用两次 。 通常,单分支if
语句具有代码味道。
这可能不是您遇到堆栈溢出的原因,但这可能是罪魁祸首。
(代码更改以粗体显示 )
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
process.nextTick(() => next(error));
}
else {
process.nextTick(next);
}
};
有些人用return
用欺骗if
太。 这也可以,但是很烂
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
process.nextTick(() => next(error));
return; // so that the next line doesn't get called too
}
process.nextTick(next);
};
我认为这样go
更好地表达整个go
函数...
const go = (next) => {
// `!` is hard to reason about
// place the easiest-to-understand, most-likely-to-happen case first
if (validation.valid) {
process.nextTick(next)
}
// very clear if/else branching
// there are two possible outcomes and one block of code for each
else {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
// no need to create a closure here
process.nextTick(() => next(error));
process.nextTick(next, error);
}
};
其他备注
您的代码中也有其他单分支if
语句
const present = (message) => {
if (typeof validation.value === 'undefined') {
// this branch only performs mutations and doesn't return anything
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
// there is no `else` branch ...
return validation;
};
这种说法不那么令人反感,但我仍然认为,一旦您对if
语句始终包含else
表示赞赏,就很难推理了。 考虑强制两个分支的三元运算符( ?:
:)。 还可以考虑使用诸如Scheme之类的语言,其中在使用if
时始终需要True和False分支。
这就是我写你present
函数的方式
const present = (message) => {
if (validation.value === undefined) {
// True branch returns
return Object.assign(validation, {
valid: false,
errors: [...validation.errors, { key: validation.key, message }]
})
}
else {
// False branch returns
return validation
}
};
这是一个自以为是的话,但我认为这是值得考虑的。 当您必须返回此代码并稍后阅读时,您将感谢我。 当然,一旦您的代码采用这种格式,就可以加深理解,以消除很多语法上的重复
const present = message =>
validation.value === undefined
? Object.assign(validation, {
valid: false,
errors: [...validation.errors, { key: validation.key, message }]
})
: validation
好处
return
有效地迫使您在函数中使用单个表达式–这意味着您不能(轻松)使函数过于复杂 if
没有返回值,则使用三元表达式与隐式return效果很好 true
和 false
分支,以便您始终处理谓词的两个结果 是的,没有什么可以阻止您使用()
将多个表达式组合为一个表达式,但是重点并不是要把每个函数都简化为一个表达式–当它起作用时,它是一种理想且好用的方法。 如果任何时候您认为可读性受到影响,则可以诉诸if (...) { return ... } else { return ... }
以获取熟悉和友好的语法/样式。
方法链溢出
validate({ name: { last: 'foo' }})
// Duplicate this line ~2000 times for error
.expect('name.first').present().string()
.go(console.log);
您根本无法在一个表达式中链接那么多方法。
在隔离的测试中 ,我们表明这与递归或process.nextTick
无关process.nextTick
class X {
foo () {
return this
}
}
let x = new X()
x.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
...
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
// RangeError: Maximum call stack size exceeded
在OSX上使用64位Chrome,在发生堆栈溢出之前,方法链接限制为6253 。 这可能因实施而异。
横向思考
方法链DSL似乎是为数据指定验证属性的好方法。 在给定的验证表达式中,您不太可能需要链接几十行,因此您不必太担心限制。
除此之外,完全不同的解决方案可能会更好。 立即想到的一个示例是JSON模式 。 与其使用代码编写验证,不如使用数据声明式编写。
这是一个快速的JSON模式示例
{
"title": "Example Schema",
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
}
},
"required": ["firstName", "lastName"]
}
实际上,架构的大小没有限制,因此这应该适合解决您的问题。
其他优点
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.