• 从 Vue 中 parseHTML 方法来看前端 html 词法分析


    先前我们在 从 Vue parseHTML 所用正则来学习常用正则语法 这篇文章中分析了 parseHTML 方法用到的正则表达式,在这个基础上我们可以继续分析 parseHTML 方法。

    先来看该方法整体结构:

    function parseHTML(html, options) {
      // ...
      let index = 0;
      let last, lastTag;
      while (html) {
        // ...
      }
      parseEndTag();
    }
    

    从整体结构上说就是通过从头开始遍历 html 元素,直至遍历至末尾。最后再调用 parseEndTag 方法,解析 endtag

    再来看 while 中的逻辑:

    while (html) {
      last = html;
      if (!lastTag || !isPlainTextElement(lastTag)) {
        // ...
      } else {
        // ...
      }
      if (html === last) {
        // ...
        break;
      }
    }
    

    这里的 lastTag 用来表示上一个标签。isPlainTextElement 用来判断标签是否为 <script><style><textarea> 三者中其中一个。所以这里是为了判断当前标签是否包含在了以上标签之中。大多数时候我们的 Vue 应用 isPlainTextElement 的判断都会为 false。

    if (!lastTag || !isPlainTextElement(lastTag))

    lastTag 或 有 lastTag 但其不为 <script><style><textarea> 三者中其中一个。

    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) { /* ... */ }
    
      let text, rest, next
      if (textEnd >= 0) { /* ... */ }
      if (textEnd < 0) { /* ... */ }
      if (text) { /* ... */ }
      if (options.chars && text) { /* ... */ }
    

    if (textEnd === 0)

    if (textEnd === 0) {
      // 处理 comment、conditionalComment、doctype
      if (comment.test(html)) { /* ... */ }
      if (conditionalComment.test(html)) { /* ... */ }
    
      const doctypeMatch = html.match(doctype)
      if (doctypeMatch) { /* ... */ }
    
      // endTagMatch 匹配 html 中如 </div> 的字符串
      const endTagMatch = html.match(endTag)
      if (endTagMatch) {
        const curIndex = index
        advance(endTagMatch[0].length)
        // 找到 stack 中与 tagName 匹配的最近的 stackTag,并调用 options.end 将 endTag 转换为 AST
        parseEndTag(endTagMatch[1], curIndex, index)
        continue
      }
      // startTagMatch 保存了 startTag 的 tagName、attrs、start、end 等结果
      const startTagMatch = parseStartTag()
      if (startTagMatch) {
        // 分析 startTag 中属性,并调用 options.start 将 startTag 转换为 AST
        handleStartTag(startTagMatch)
        if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
          advance(1)
        }
        // 继续下一循环
        continue
      }
    }
    

    if (textEnd >= 0)

    // textEnd 记录了 `<` 的位置
    if (textEnd >= 0) {
      // rest 记录了 html 中从 `<` 到最末尾的字符串
      rest = html.slice(textEnd);
      while (
        !endTag.test(rest) && // 非 endTag: `</div>`
        !startTagOpen.test(rest) && // 非 startTagOpen: `<div `
        !comment.test(rest) && // 非 comment: `<!--`
        !conditionalComment.test(rest) // 非 conditionalComment: `<![`
      ) {
        // 下一个 `<` 的位置
        next = rest.indexOf("<", 1);
        if (next < 0) break;
        textEnd += next;
        rest = html.slice(textEnd);
      }
      // text 记录了从 html 字符串开头到 `<` 的字符串
      text = html.substring(0, textEnd);
    }
    

    剩余逻辑

    // 如 `<` 不存在
    if (textEnd < 0) {
      text = html;
    }
    
    // 将 index 后移 text 长度,html 做截取
    if (text) {
      advance(text.length);
    }
    
    // 调用 options.chars
    if (options.chars && text) {
      options.chars(text, index - text.length, index);
    }
    

    else

    通常不会进入该逻辑,暂不分析。

    附录

    parseEndTag

    function parseEndTag(tagName, start, end) {
      let pos, lowerCasedTagName;
      if (start == null) start = index;
      if (end == null) end = index;
    
      // pos 保存了 stack 中与 tagName 匹配的最近的标签
      if (tagName) {
        lowerCasedTagName = tagName.toLowerCase();
        for (pos = stack.length - 1; pos >= 0; pos--) {
          if (stack[pos].lowerCasedTag === lowerCasedTagName) {
            break;
          }
        }
      } else {
        // If no tag name is provided, clean shop
        pos = 0;
      }
    
      if (pos >= 0) {
        // Close all the open elements, up the stack
        for (let i = stack.length - 1; i >= pos; i--) {
          if (
            process.env.NODE_ENV !== "production" &&
            (i > pos || !tagName) &&
            options.warn
          ) {
            options.warn(`tag <${stack[i].tag}> has no matching end tag.`, {
              start: stack[i].start,
              end: stack[i].end,
            });
          }
          // 用 options.end 将 end 标签解析为 AST
          if (options.end) {
            options.end(stack[i].tag, start, end);
          }
        }
    
        // 移除在 stack 中匹配位置之后的标签
        stack.length = pos;
        lastTag = pos && stack[pos - 1].tag;
      } else if (lowerCasedTagName === "br") {
        if (options.start) {
          options.start(tagName, [], true, start, end);
        }
      } else if (lowerCasedTagName === "p") {
        if (options.start) {
          options.start(tagName, [], false, start, end);
        }
        if (options.end) {
          options.end(tagName, start, end);
        }
      }
    }
    

    parseStartTag

    用于解析 html 标签中 <div id="mydiv" class="myClass" style="color: #ff0000" > 部分,并将结果用 match 保存。

    function parseStartTag() {
      // startTagOpen 匹配如 `<div ` 的字符串
      const start = html.match(startTagOpen);
      if (start) {
        const match = {
          tagName: start[1],
          attrs: [],
          start: index,
        };
        advance(start[0].length);
        let end, attr;
        // startTagClose 匹配如 ` />` 或 ` >` 的字符串,dynamicArgAttribute: `v-bind:[attributeName]="url"`,attribute: `id="mydiv"`
        // 若往后匹配到 dynamicArgAttribute 或 attribute,且一直匹配不是 startTagClose,下面的 while 循环一直进行
        // 循环内将 attribute 等匹配结果用 match.attrs 保存起来
        while (
          !(end = html.match(startTagClose)) &&
          (attr = html.match(dynamicArgAttribute) || html.match(attribute))
        ) {
          attr.start = index;
          advance(attr[0].length);
          attr.end = index;
          match.attrs.push(attr);
        }
        // 到达 ` />` 的位置,将 end 用 match.end 保存
        if (end) {
          match.unarySlash = end[1];
          advance(end[0].length);
          match.end = index;
          return match;
        }
      }
    }
    

    advance

    将 html 字符串向后移动 n 位,得到从 n 到结尾的字符串

    function advance(n) {
      index += n;
      html = html.substring(n);
    }
    

    handleStartTag

    用于分析 startTag 中属性,并调用 options.start 将 startTag 转换为 AST

    function handleStartTag(match) {
      const tagName = match.tagName;
      const unarySlash = match.unarySlash;
    
      // expectHTML 来自于 baseOptions.expectHTML,初始值为 true,第一次会执行
      // 里面逻辑暂不分析
      if (expectHTML) {
        if (lastTag === "p" && isNonPhrasingTag(tagName)) {
          parseEndTag(lastTag);
        }
        if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
          parseEndTag(tagName);
        }
      }
    
      // unary 用来表示标签是否自闭合
      const unary = isUnaryTag(tagName) || !!unarySlash;
    
      // 下面一段用来将 match.attrs 放入 attrs 变量,供后续使用
      const l = match.attrs.length;
      const attrs = new Array(l);
      for (let i = 0; i < l; i++) {
        const args = match.attrs[i];
        const value = args[3] || args[4] || args[5] || "";
        const shouldDecodeNewlines =
          tagName === "a" && args[1] === "href"
            ? options.shouldDecodeNewlinesForHref
            : options.shouldDecodeNewlines;
        attrs[i] = {
          name: args[1],
          value: decodeAttr(value, shouldDecodeNewlines),
        };
        if (process.env.NODE_ENV !== "production" && options.outputSourceRange) {
          attrs[i].start = args.start + args[0].match(/^s*/).length;
          attrs[i].end = args.end;
        }
      }
    
      // 如果是非自闭合的标签,则将标签各个属性 push 进 stack,并将 tagName 赋给 lastTag
      if (!unary) {
        stack.push({
          tag: tagName,
          lowerCasedTag: tagName.toLowerCase(),
          attrs: attrs,
          start: match.start,
          end: match.end,
        });
        lastTag = tagName;
      }
    
      // options.start 用来将开始标签转换为 AST
      if (options.start) {
        options.start(tagName, attrs, unary, match.start, match.end);
      }
    }
    
  • 相关阅读:
    搭建环境遇到的几个问题
    webservice
    Eclipse 反编译 阅读class 文件
    设置navigationBar上面的item
    自定义的UITabbar上面的按钮的x坐标的计算方法
    UIToolbar自定义背景及按钮设置
    UITabBar实现自定义背景及UITabBarItem自定义图片和字体
    IOS APP圆形图片的实现
    如果AlertView输入框为空,则禁止点击确定按钮
    两种局部刷新UITableView的方法的使用条件
  • 原文地址:https://www.cnblogs.com/lilei94/p/15033246.html
Copyright © 2020-2023  润新知