zoukankan      html  css  js  c++  java
  • 从 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);
      }
    }
    
  • 相关阅读:
    Selenium简单测试页面加载速度的性能(Page loading performance)
    Selenium Page object Pattern usage
    Selenium如何支持测试Windows application
    UI Automation的两个成熟的框架(QTP 和Selenium)
    分享自己针对Automation做的两个成熟的框架(QTP 和Selenium)
    敏捷开发中的测试金字塔(转)
    Selenium 的基础框架类
    selenium2 run in Jenkins GUI testing not visible or browser not open but run in background浏览器后台运行不可见
    eclipse与SVN 结合(删除SVN中已经上传的问题)
    配置Jenkins的slave节点的详细步骤适合windows等其他平台
  • 原文地址:https://www.cnblogs.com/lilei94/p/15033246.html
Copyright © 2011-2022 走看看