Description
问题
parser 如何处理 <div><p></div>
的 edge case?
前言
如 HcySunYang 所说
当用户没有以预期的方式使用框架时,是否应该打印合适的警告信息从而提供更好的开发体验,让用户快速定位问题?
死循环分析
我们考虑 isEnd 的条件如下:
function isEnd(context: any, parentTag) {
// 结束标签
if (parentTag && context.source.startsWith(`</${parentTag}>`)) {
return true
}
// context.source 为空
return !context.source
}
很容易陷入如下的死循环中,因为此时的 parentTag
依然是 p
。而 </div>
进入 parseChildren
中,不以 {{
和 <[a-z]
开头会进入 parseText 从而死循环。
死循环解决方案
分析下来主要是由于 isEnd
的判断过于严厉,只和 parentTag
进行比较,如果不是理想的 happy path,那么就会陷入死循环。
那么尝试放宽判断条件,只判断是否是 结束标签 是否可行呢?
function isEnd(context: any, parentTag) {
// 结束标签
if (context.source.startsWith('</')) {
return true
}
// context.source 为空
return !context.source
}
那显然也是不行的,在正常情况下 <div><p></p><div>
就会提前退出循环
考虑这两种方案的特性:
-
严格的方案:判断是 结束标签 且等于
parentTag
-
宽松的方案:仅判断 结束标签
那么我们应该提出一个折衷的方案:
- 折衷的方案:判断是 结束标签 且在 祖先
Tag
中被找到
因此我们将 parseElement
中的 parentTag
修改为 ancestors
,数据类型是 stack,并且在 parseElement
保存所有的 祖先
function parseElement(context: any, ancestors): any {
// StartTag
const element = parseTag(context, TagType.Start)
// 入栈 element {tag,type}
ancestors.push(element)
element.children = parseChildren(context, ancestors)
// parseChildren 递归结束 出栈
ancestors.pop()
// EndTag
parseTag(context, TagType.End)
return element
}
最后将 isEnd
修改为:
function isEnd(context: any, ancestors) {
let s = context.source
// 判断为结束标签
if (s.startsWith('</')) {
// 和祖先标签进行对比
for (let i = ancestors.length - 1; i >= 0; i--) {
const tag = ancestors[i].tag
if (s.slice(2, 2 + tag.length) === tag) {
return true
}
}
}
// context.source 为空
return !s
}
错误信息
错误信息应该在 parseElement
中处理 endTag
之前,对 endTag
进行合法性验证
function parseElement(context: any, ancestors): any {
// StartTag
const element = parseTag(context, TagType.Start)
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
// EndTag
// 判断消费后的 context.source 最前面的 tag 是否等于当前 element 的 tag
if (context.source.slice(2, 2 + element.tag.length) === element.tag) {
parseTag(context, TagType.End)
} else {
throw new Error(`缺少结束标签: ${element.tag}`)
}
return element
}
当然后续可以将判断条件和 isEnd
进行抽离方法的重构,不在本篇的讨论中了
总结
vue3 中的 parser 如何处理 <div><p></div>
的 edge case?
- 处理
parseChildren
的死循环问题- ❌严厉方案:判断是 EndTag 且与
parentTag
相同 - ❌宽松方案:只判断是 EndTag
- ✅折衷方案:判断是 EndTag 且与 祖先 Tag 相同
- ❌严厉方案:判断是 EndTag 且与
- 在
parseElement
中处理EndTag
前作判断- 判断消费后的 context.source 最前面的 tag 是否等于当前 element 的 tag
感想
实际开发中的 用户体验 同样重要,当用户没有以预期的方式使用时,需要从 设计 层面决定 Error 信息或者 Warning 信息是由底层浏览器抛给用户,还是由我们的产品抛给用户。
所以有时候一些条件的 缩放 平衡就很值得玩味了,happy path 有时候条件比较严厉,会导致用户的未按照预期使用的行为触发死循环等。因此可以适当放宽条件,并在合适的时机抛出 错误 让用户得知