Skip to content

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

Open
@masterX89

Description

@masterX89

问题

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 有时候条件比较严厉,会导致用户的未按照预期使用的行为触发死循环等。因此可以适当放宽条件,并在合适的时机抛出 错误 让用户得知

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions