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

先来看该方法整体结构:

  1. function parseHTML(html, options) {
  2. // ...
  3. let index = 0;
  4. let last, lastTag;
  5. while (html) {
  6. // ...
  7. }
  8. parseEndTag();
  9. }

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

再来看 while 中的逻辑:

  1. while (html) {
  2. last = html;
  3. if (!lastTag || !isPlainTextElement(lastTag)) {
  4. // ...
  5. } else {
  6. // ...
  7. }
  8. if (html === last) {
  9. // ...
  10. break;
  11. }
  12. }

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

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

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

  1. if (!lastTag || !isPlainTextElement(lastTag)) {
  2. let textEnd = html.indexOf('<')
  3. if (textEnd === 0) { /* ... */ }
  4. let text, rest, next
  5. if (textEnd >= 0) { /* ... */ }
  6. if (textEnd < 0) { /* ... */ }
  7. if (text) { /* ... */ }
  8. if (options.chars && text) { /* ... */ }

if (textEnd === 0)

  1. if (textEnd === 0) {
  2. // 处理 comment、conditionalComment、doctype
  3. if (comment.test(html)) { /* ... */ }
  4. if (conditionalComment.test(html)) { /* ... */ }
  5. const doctypeMatch = html.match(doctype)
  6. if (doctypeMatch) { /* ... */ }
  7. // endTagMatch 匹配 html 中如 </div> 的字符串
  8. const endTagMatch = html.match(endTag)
  9. if (endTagMatch) {
  10. const curIndex = index
  11. advance(endTagMatch[0].length)
  12. // 找到 stack 中与 tagName 匹配的最近的 stackTag,并调用 options.end 将 endTag 转换为 AST
  13. parseEndTag(endTagMatch[1], curIndex, index)
  14. continue
  15. }
  16. // startTagMatch 保存了 startTag 的 tagName、attrs、start、end 等结果
  17. const startTagMatch = parseStartTag()
  18. if (startTagMatch) {
  19. // 分析 startTag 中属性,并调用 options.start 将 startTag 转换为 AST
  20. handleStartTag(startTagMatch)
  21. if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
  22. advance(1)
  23. }
  24. // 继续下一循环
  25. continue
  26. }
  27. }

if (textEnd >= 0)

  1. // textEnd 记录了 `<` 的位置
  2. if (textEnd >= 0) {
  3. // rest 记录了 html 中从 `<` 到最末尾的字符串
  4. rest = html.slice(textEnd);
  5. while (
  6. !endTag.test(rest) && // 非 endTag: `</div>`
  7. !startTagOpen.test(rest) && // 非 startTagOpen: `<div `
  8. !comment.test(rest) && // 非 comment: `<!--`
  9. !conditionalComment.test(rest) // 非 conditionalComment: `<![`
  10. ) {
  11. // 下一个 `<` 的位置
  12. next = rest.indexOf("<", 1);
  13. if (next < 0) break;
  14. textEnd += next;
  15. rest = html.slice(textEnd);
  16. }
  17. // text 记录了从 html 字符串开头到 `<` 的字符串
  18. text = html.substring(0, textEnd);
  19. }

剩余逻辑

  1. // 如 `<` 不存在
  2. if (textEnd < 0) {
  3. text = html;
  4. }
  5. // 将 index 后移 text 长度,html 做截取
  6. if (text) {
  7. advance(text.length);
  8. }
  9. // 调用 options.chars
  10. if (options.chars && text) {
  11. options.chars(text, index - text.length, index);
  12. }

else

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

附录

parseEndTag

  1. function parseEndTag(tagName, start, end) {
  2. let pos, lowerCasedTagName;
  3. if (start == null) start = index;
  4. if (end == null) end = index;
  5. // pos 保存了 stack 中与 tagName 匹配的最近的标签
  6. if (tagName) {
  7. lowerCasedTagName = tagName.toLowerCase();
  8. for (pos = stack.length - 1; pos >= 0; pos--) {
  9. if (stack[pos].lowerCasedTag === lowerCasedTagName) {
  10. break;
  11. }
  12. }
  13. } else {
  14. // If no tag name is provided, clean shop
  15. pos = 0;
  16. }
  17. if (pos >= 0) {
  18. // Close all the open elements, up the stack
  19. for (let i = stack.length - 1; i >= pos; i--) {
  20. if (
  21. process.env.NODE_ENV !== "production" &&
  22. (i > pos || !tagName) &&
  23. options.warn
  24. ) {
  25. options.warn(`tag <${stack[i].tag}> has no matching end tag.`, {
  26. start: stack[i].start,
  27. end: stack[i].end,
  28. });
  29. }
  30. // 用 options.end 将 end 标签解析为 AST
  31. if (options.end) {
  32. options.end(stack[i].tag, start, end);
  33. }
  34. }
  35. // 移除在 stack 中匹配位置之后的标签
  36. stack.length = pos;
  37. lastTag = pos && stack[pos - 1].tag;
  38. } else if (lowerCasedTagName === "br") {
  39. if (options.start) {
  40. options.start(tagName, [], true, start, end);
  41. }
  42. } else if (lowerCasedTagName === "p") {
  43. if (options.start) {
  44. options.start(tagName, [], false, start, end);
  45. }
  46. if (options.end) {
  47. options.end(tagName, start, end);
  48. }
  49. }
  50. }

parseStartTag

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

  1. function parseStartTag() {
  2. // startTagOpen 匹配如 `<div ` 的字符串
  3. const start = html.match(startTagOpen);
  4. if (start) {
  5. const match = {
  6. tagName: start[1],
  7. attrs: [],
  8. start: index,
  9. };
  10. advance(start[0].length);
  11. let end, attr;
  12. // startTagClose 匹配如 ` />` 或 ` >` 的字符串,dynamicArgAttribute: `v-bind:[attributeName]="url"`,attribute: `id="mydiv"`
  13. // 若往后匹配到 dynamicArgAttribute 或 attribute,且一直匹配不是 startTagClose,下面的 while 循环一直进行
  14. // 循环内将 attribute 等匹配结果用 match.attrs 保存起来
  15. while (
  16. !(end = html.match(startTagClose)) &&
  17. (attr = html.match(dynamicArgAttribute) || html.match(attribute))
  18. ) {
  19. attr.start = index;
  20. advance(attr[0].length);
  21. attr.end = index;
  22. match.attrs.push(attr);
  23. }
  24. // 到达 ` />` 的位置,将 end 用 match.end 保存
  25. if (end) {
  26. match.unarySlash = end[1];
  27. advance(end[0].length);
  28. match.end = index;
  29. return match;
  30. }
  31. }
  32. }

advance

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

  1. function advance(n) {
  2. index += n;
  3. html = html.substring(n);
  4. }

handleStartTag

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

  1. function handleStartTag(match) {
  2. const tagName = match.tagName;
  3. const unarySlash = match.unarySlash;
  4. // expectHTML 来自于 baseOptions.expectHTML,初始值为 true,第一次会执行
  5. // 里面逻辑暂不分析
  6. if (expectHTML) {
  7. if (lastTag === "p" && isNonPhrasingTag(tagName)) {
  8. parseEndTag(lastTag);
  9. }
  10. if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
  11. parseEndTag(tagName);
  12. }
  13. }
  14. // unary 用来表示标签是否自闭合
  15. const unary = isUnaryTag(tagName) || !!unarySlash;
  16. // 下面一段用来将 match.attrs 放入 attrs 变量,供后续使用
  17. const l = match.attrs.length;
  18. const attrs = new Array(l);
  19. for (let i = 0; i < l; i++) {
  20. const args = match.attrs[i];
  21. const value = args[3] || args[4] || args[5] || "";
  22. const shouldDecodeNewlines =
  23. tagName === "a" && args[1] === "href"
  24. ? options.shouldDecodeNewlinesForHref
  25. : options.shouldDecodeNewlines;
  26. attrs[i] = {
  27. name: args[1],
  28. value: decodeAttr(value, shouldDecodeNewlines),
  29. };
  30. if (process.env.NODE_ENV !== "production" && options.outputSourceRange) {
  31. attrs[i].start = args.start + args[0].match(/^\s*/).length;
  32. attrs[i].end = args.end;
  33. }
  34. }
  35. // 如果是非自闭合的标签,则将标签各个属性 push 进 stack,并将 tagName 赋给 lastTag
  36. if (!unary) {
  37. stack.push({
  38. tag: tagName,
  39. lowerCasedTag: tagName.toLowerCase(),
  40. attrs: attrs,
  41. start: match.start,
  42. end: match.end,
  43. });
  44. lastTag = tagName;
  45. }
  46. // options.start 用来将开始标签转换为 AST
  47. if (options.start) {
  48. options.start(tagName, attrs, unary, match.start, match.end);
  49. }
  50. }

从 Vue 中 parseHTML 方法来看前端 html 词法分析的更多相关文章

  1. 022——VUE中remove()方法的使用:

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  2. Vue中methods(方法)、computed(计算属性)、watch(侦听器)的区别

    1.computed和methods 共同点:computed能现实的methods也能实现: 不同点:computed是基于它的依赖进行缓存的.computed只有在它的相关依赖发生变化才会重新计算 ...

  3. 021——VUE中变异方法 push/unshift pop/shift

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. 020——VUE中变异方法push的留言版实例讲解

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  5. Ace 在Vue中使用方法

    var Vue = require('vue/dist/vue.common.js'); document.querySelector('body').append(document.createEl ...

  6. vue中makeMap方法的使用 (定义注册一些值 后期方便使用)

    function makeMap ( str, expectsLowerCase ) { var map = Object.create(null); var list = str.split(',' ...

  7. [Vue 牛刀小试]:第十二章 - 使用 Vue Router 实现 Vue 中的前端路由控制

    一.前言 前端路由是什么?如果你之前从事的是后端的工作,或者虽然有接触前端,但是并没有使用到单页面应用的话,这个概念对你来说还是会很陌生的.那么,为什么会在单页面应用中存在这么一个概念,以及,前端路由 ...

  8. Vue中的methods、watch、computed

    看到这个标题就知道这篇文章接下来要讲的内容,我们在使用vue的时候methods.watch.computed这三个特性一定经常使用,因为它们是非常的有用,但是没有彻底的理解它们的区别和各自的使用场景 ...

  9. Vue中$nextTick的理解

    Vue中$nextTick的理解 Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的 ...

随机推荐

  1. Go语言标准库log介绍

    Go语言标准库log介绍 无论是软件开发的调试阶段还是软件上线之后的运行阶段,日志一直都是非常重要的一个环节,我们也应该养成在程序中记录日志的好习惯. log Go语言内置的log包实现了简单的日志服 ...

  2. elasticsearch_dsl 操作

    import elasticsearch from elasticsearch_dsl import Search, MultiSearch # Search-执行一个搜索,MultiSearch-同 ...

  3. Step By Step(Lua基础知识)

    Step By Step(Lua基础知识) 一.基础知识:    1. 第一个程序和函数:    在目前这个学习阶段,运行Lua程序最好的方式就是通过Lua自带的解释器程序,如:    /> l ...

  4. 重新整理 .net core 实践篇—————3种配置验证[十四]

    前言 简单整理一些配置的验证. 正文 配置的验证大概分为3类: 直接注册验证函数 实现IValidteOptions 使用Microsoft.Extensions.Options.DataAnnota ...

  5. React-setState的那些事儿

    关于setState,使用过react的人应该再熟悉不过了,在hooks还不那么普及的时候,除了使用函数式组件,我们使用最多的应该就是类创建react的组件了,而在类组件中我们通常会使用state来管 ...

  6. CVPR2019论文解读:单眼提升2D检测到6D姿势和度量形状

    CVPR2019论文解读:单眼提升2D检测到6D姿势和度量形状 ROI-10D: Monocular Lifting of 2D Detection to 6D Pose and Metric Sha ...

  7. TVM自动调度器

    TVM自动调度器 随着模型大小,算子多样性和硬件异构性的不断增长,优化深度神经网络的执行速度非常困难.从计算的角度来看,深度神经网络只是张量计算的一层又一层.这些张量计算(例如matmul和conv2 ...

  8. GEMM与AutoKernel算子优化

    GEMM与AutoKernel算子优化 随着AI技术的快速发展,深度学习在各个领域得到了广泛应用.深度学习模型能否成功在终端落地应用,满足产品需求,一个关键的指标就是神经网络模型的推理性能.一大波算法 ...

  9. 机器学习PAL产品优势

    机器学习PAL产品优势 PAI支持丰富的机器学习算法.一站式的机器学习体验.主流的机器学习框架及可视化的建模方式.本文介绍PAI的产品优势. 丰富的机器学习算法 PAI的算法都经过阿里巴巴集团大规模业 ...

  10. TensorFlowMNIST数据集逻辑回归处理

    TensorFlow逻辑回归处理MNIST数据集 本节基于回归学习对 MNIST 数据集进行处理,但将添加一些 TensorBoard 总结以便更好地理解 MNIST 数据集. MNIST由https ...