Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parser 如何处理 <div><p></div> 的 edge case? #2

Open
masterX89 opened this issue May 9, 2022 · 0 comments
Open

parser 如何处理 <div><p></div> 的 edge case? #2

masterX89 opened this issue May 9, 2022 · 0 comments
Labels
compiler-core Problems related to compiler-core edge cases edge cases

Comments

@masterX89
Copy link
Owner

masterX89 commented May 9, 2022

问题

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 从而死循环。

image-20220509102840921

死循环解决方案

分析下来主要是由于 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?

  1. 处理 parseChildren 的死循环问题
    • ❌严厉方案:判断是 EndTag 且与 parentTag 相同
    • ❌宽松方案:只判断是 EndTag
    • ✅折衷方案:判断是 EndTag 且与 祖先 Tag 相同
  2. parseElement 中处理 EndTag 前作判断
    • 判断消费后的 context.source 最前面的 tag 是否等于当前 element 的 tag

感想

实际开发中的 用户体验 同样重要,当用户没有以预期的方式使用时,需要从 设计 层面决定 Error 信息或者 Warning 信息是由底层浏览器抛给用户,还是由我们的产品抛给用户。

所以有时候一些条件的 缩放 平衡就很值得玩味了,happy path 有时候条件比较严厉,会导致用户的未按照预期使用的行为触发死循环等。因此可以适当放宽条件,并在合适的时机抛出 错误 让用户得知

@masterX89 masterX89 added compiler-core Problems related to compiler-core edge cases edge cases labels May 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler-core Problems related to compiler-core edge cases edge cases
Projects
None yet
Development

No branches or pull requests

1 participant