从 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);
}
}
从 Vue 中 parseHTML 方法来看前端 html 词法分析的更多相关文章
- 022——VUE中remove()方法的使用:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Vue中methods(方法)、computed(计算属性)、watch(侦听器)的区别
1.computed和methods 共同点:computed能现实的methods也能实现: 不同点:computed是基于它的依赖进行缓存的.computed只有在它的相关依赖发生变化才会重新计算 ...
- 021——VUE中变异方法 push/unshift pop/shift
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 020——VUE中变异方法push的留言版实例讲解
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- Ace 在Vue中使用方法
var Vue = require('vue/dist/vue.common.js'); document.querySelector('body').append(document.createEl ...
- vue中makeMap方法的使用 (定义注册一些值 后期方便使用)
function makeMap ( str, expectsLowerCase ) { var map = Object.create(null); var list = str.split(',' ...
- [Vue 牛刀小试]:第十二章 - 使用 Vue Router 实现 Vue 中的前端路由控制
一.前言 前端路由是什么?如果你之前从事的是后端的工作,或者虽然有接触前端,但是并没有使用到单页面应用的话,这个概念对你来说还是会很陌生的.那么,为什么会在单页面应用中存在这么一个概念,以及,前端路由 ...
- Vue中的methods、watch、computed
看到这个标题就知道这篇文章接下来要讲的内容,我们在使用vue的时候methods.watch.computed这三个特性一定经常使用,因为它们是非常的有用,但是没有彻底的理解它们的区别和各自的使用场景 ...
- Vue中$nextTick的理解
Vue中$nextTick的理解 Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的 ...
随机推荐
- Python Xpath语法
Python Xpath语法 一.选取节点 常用的路劲表达式: 表达式 描述 实例 nodename 选取nodename节点的所有子节点 xpath('//div') 选取了div节点 ...
- Nginx 配置实例-配置高可用
Nginx 配置实例-配置高可用 1. 实现效果 2. 两台机器 nginx 的安装 2.1 192.168.25.120 中 nginx 的安装 2.1.1 安装 pcre 依赖 2.1.2 安装其 ...
- GO文件读写03---使用缓冲读写实现视频文件的拷贝
package main import ( "bufio" "fmt" "io" "os" ) /* ·使用缓冲读写实现 ...
- Python+Selenium学习笔记2 - 字符串
跟着网络课程学了几个小程序. 1.判断a字符串是否为b字符串的子串 1 # coding = utf-8 2 3 # 判断str_a字符串是否为str_b字符串的子串 4 5 str_a = &quo ...
- 调试动态加载的js
用浏览器无法调试异步加载页面里包含的js文件.简单的说就是在调试工具里面看不到异步加载页面里包含的js文件 最近在一个新的web项目中开发功能.这个项目的管理界面有一个特点,框架是固定的,不会刷新 ...
- FCN与U-Net语义分割算法
FCN与U-Net语义分割算法 图像语义分割(Semantic Segmentation)是图像处理和是机器视觉技术中关于图像理解的重要一环,也是 AI 领域中一个重要的分支.语义分割即是对图像中每一 ...
- 如何运行具有奇点的NGC深度学习容器
如何运行具有奇点的NGC深度学习容器 How to Run NGC Deep Learning Containers with Singularity 高性能计算机和人工智能的融合使新的科学突破成为可 ...
- Android系统编程入门系列之应用环境及开发环境介绍
作为移动端操作系统,目前最新的Android 11.0已经发展的比较完善了,现在也到了系统的整理一番的时间,接下来的系列文章将以Android开发者为中心,争取用归纳总结的态度对初级入门者所应 ...
- PyQt5开发实践(一、准备篇)
前言 近一年来我开发了不少PyQt小项目,因为之前没用过使用C++语言的Qt,所以可以算是从零基础开始边学边做的,这个过程中再一次体会到国内技术社区的匮乏-- 国内关于PyQt的资料说少不少,说多也不 ...
- Task01:初识数据库
本章主要讲解数据库安装和数据库基本介绍,考虑易用性及普及度,本课程采取mysql进行教学. 1.1 初识数据库 数据库是将大量数据保存起来,通过计算机加工而成的可以进行高效访问的数据集合.该数据集合称 ...