当前位置: 首页 > news >正文

福建外贸网站建设国外手机html5网站

福建外贸网站建设,国外手机html5网站,网站制作公司去哪找,建设局主要管什么文章目录 1. 前言2. HTML解析器内部运行流程3. 如何解析不同的内容3.1 解析HTML注释3.2 解析条件注释3.3 解析DOCTYPE3.4 解析开始标签3.5 解析结束标签3.6 解析文本 4. 如何保证AST节点层级关系5. 回归源码5.1 HTML解析器源码5.2 parseEndTag函数源码 6. 总结 1. 前言 上篇文… 文章目录 1. 前言2. HTML解析器内部运行流程3. 如何解析不同的内容3.1 解析HTML注释3.2 解析条件注释3.3 解析DOCTYPE3.4 解析开始标签3.5 解析结束标签3.6 解析文本 4. 如何保证AST节点层级关系5. 回归源码5.1 HTML解析器源码5.2 parseEndTag函数源码 6. 总结 1. 前言 上篇文章中我们说到在模板解析阶段主线函数parse中根据要解析的内容不同会调用不同的解析器 而在三个不同的解析器中最主要的当属HTML解析器为什么这么说呢因为HTML解析器主要负责解析出模板字符串中有哪些内容然后根据不同的内容才能调用其他的解析器以及做相应的处理。那么本篇文章就来介绍一下HTML解析器是如何解析出模板字符串中包含的不同的内容的。 2. HTML解析器内部运行流程 在源码中HTML解析器就是parseHTML函数在模板解析主线函数parse中调用了该函数并传入两个参数代码如下 // 代码位置/src/complier/parser/index.js/*** Convert HTML string to AST.* 将HTML模板字符串转化为AST*/ export function parse(template, options) {// ...parseHTML(template, {warn,expectHTML: options.expectHTML,isUnaryTag: options.isUnaryTag,canBeLeftOpenTag: options.canBeLeftOpenTag,shouldDecodeNewlines: options.shouldDecodeNewlines,shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,shouldKeepComment: options.comments,// 当解析到开始标签时调用该函数start (tag, attrs, unary) {},// 当解析到结束标签时调用该函数end () {},// 当解析到文本时调用该函数chars (text) {},// 当解析到注释时调用该函数comment (text) {}})return root }从代码中我们可以看到调用parseHTML函数时为其传入的两个参数分别是 template:待转换的模板字符串options:转换时所需的选项 第一个参数是待转换的模板字符串无需多言重点看第二个参数第二个参数提供了一些解析HTML模板时的一些参数同时还定义了4个钩子函数。这4个钩子函数有什么作用呢我们说了模板编译阶段主线函数parse会将HTML模板字符串转化成AST而parseHTML是用来解析模板字符串的把模板字符串中不同的内容出来之后那么谁来把提取出来的内容生成对应的AST呢答案就是这4个钩子函数。 把这4个钩子函数作为参数传给解析器parseHTML当解析器解析出不同的内容时调用不同的钩子函数从而生成不同的AST。 当解析到开始标签时调用start函数生成元素类型的AST节点代码如下 // 当解析到标签的开始位置时触发start start (tag, attrs, unary) {let element createASTElement(tag, attrs, currentParent) }export function createASTElement (tag,attrs,parent) {return {type: 1,tag,attrsList: attrs,attrsMap: makeAttrsMap(attrs),parent,children: []} }从上面代码中我们可以看到start函数接收三个参数分别是标签名tag、标签属性attrs、标签是否自闭合unary。当调用该钩子函数时内部会调用createASTElement函数来创建元素类型的AST节点 当解析到结束标签时调用end函数 当解析到文本时调用chars函数生成文本类型的AST节点 // 当解析到标签的文本时触发chars chars (text) {if(text是带变量的动态文本){let element {type: 2,expression: res.expression,tokens: res.tokens,text}} else {let element {type: 3,text}} }当解析到标签的文本时触发chars钩子函数在该钩子函数内部首先会判断文本是不是一个带变量的动态文本如“hello ”。如果是动态文本则创建动态文本类型的AST节点如果不是动态文本则创建纯静态文本类型的AST节点。 当解析到注释时调用comment函数生成注释类型的AST节点 // 当解析到标签的注释时触发comment comment (text: string) {let element {type: 3,text,isComment: true} }当解析到标签的注释时触发comment钩子函数该钩子函数会创建一个注释类型的AST节点。 一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点最终完成将整个模板字符串转化成AST,这就是HTML解析器所要做的工作。 3. 如何解析不同的内容 要从模板字符串中解析出不同的内容那首先要知道模板字符串中都会包含哪些内容。那么通常我们所写的模板字符串中都会包含哪些内容呢经过整理通常模板内会包含如下内容 文本例如“难凉热血”HTML注释例如 条件注释例如 我是注释 DOCTYPE例如开始标签例如 结束标签例如 这几种内容都有其各自独有的特点也就是说我们要根据不同内容所具有的不同的的特点通过编写不同的正则表达式将这些内容从模板字符串中一一解析出来然后再把不同的内容做不同的处理。 下面我们就来分别看一下HTML解析器是如何从模板字符串中将以上不同种类的内容进行解析出来。 3.1 解析HTML注释 解析注释比较简单我们知道HTML注释是以!--开头以--结尾这两者中间的内容就是注释内容那么我们只需用正则判断待解析的模板字符串html是否以!--开头若是那就继续向后寻找--如果找到了OK注释就被解析出来了。代码如下 const comment /^!\--/ if (comment.test(html)) {// 若为注释则继续查找是否存在--const commentEnd html.indexOf(--)if (commentEnd 0) {// 若存在 --,继续判断options中是否保留注释if (options.shouldKeepComment) {// 若保留注释则把注释截取出来传给options.comment创建注释类型的AST节点options.comment(html.substring(4, commentEnd))}// 若不保留注释则将游标移动到--之后继续向后解析advance(commentEnd 3)continue} }在上面代码中如果模板字符串html符合注释开始的正则那么就继续向后查找是否存在--若存在则把html从第4位 处截取得到的内容就是注释的真实内容然后调用4个钩子函数中的comment函数将真实的注释内容传进去创建注释类型的AST节点。 上面代码中有一处值得注意的地方那就是我们平常在模板中可以在template/template标签上配置comments选项来决定在渲染模板时是否保留注释对应到上面代码中就是options.shouldKeepComment,如果用户配置了comments选项为true则shouldKeepComment为true则创建注释类型的AST节点如不保留注释则将游标移动到’–之后继续向后解析。 advance函数是用来移动解析游标的解析完一部分就把游标向后移动一部分确保不会重复解析其代码如下 function advance (n) {index n // index为解析游标html html.substring(n) }为了更加直观地说明 advance 的作用请看下图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 调用 advance 函数 advance(3)得到结果 从图中可以看到解析游标index最开始在模板字符串的位置0处当调用了advance(3)之后解析游标到了位置3处每次解析完一段内容就将游标向后移动一段接着再从解析游标往后解析这样就保证了解析过的内容不会被重复解析。 3.2 解析条件注释 解析条件注释也比较简单其原理跟解析注释相同都是先用正则判断是否是以条件注释特有的开头标识开始然后寻找其特有的结束标识若找到则说明是条件注释将其截取出来即可由于条件注释不存在于真正的DOM树中所以不需要调用钩子函数创建AST节点。代码如下 // 解析是否是条件注释 const conditionalComment /^!\[/ if (conditionalComment.test(html)) {// 若为条件注释则继续查找是否存在]const conditionalEnd html.indexOf(])if (conditionalEnd 0) {// 若存在 ],则从原本的html字符串中把条件注释截掉// 把剩下的内容重新赋给html继续向后匹配advance(conditionalEnd 2)continue} }3.3 解析DOCTYPE 解析DOCTYPE的原理同解析条件注释完全相同此处不再赘述代码如下 const doctype /^!DOCTYPE [^]/i // 解析是否是DOCTYPE const doctypeMatch html.match(doctype) if (doctypeMatch) {advance(doctypeMatch[0].length)continue }3.4 解析开始标签 相较于前三种内容的解析解析开始标签会稍微复杂一点但是万变不离其宗它的原理还是相通的都是使用正则去匹配提取。 首先使用开始标签的正则去匹配模板字符串看模板字符串是否具有开始标签的特征如下 /*** 匹配开始标签的正则*/ const ncname [a-zA-Z_][\\w\\-\\.]* const qnameCapture ((?:${ncname}\\:)?${ncname}) const startTagOpen new RegExp(^${qnameCapture})const start html.match(startTagOpen) if (start) {const match {tagName: start[1],attrs: [],start: index} }// 以开始标签开始的模板 div/div.match(startTagOpen) [div,div,index:0,input:div/div] // 以结束标签开始的模板 /divdiv/div.match(startTagOpen) null // 以文本开始的模板 我是文本/p.match(startTagOpen) null在上面代码中我们用不同类型的内容去匹配开始标签的正则发现只有div/div的字符串可以正确匹配并且返回一个数组。 在前文中我们说到当解析到开始标签时会调用4个钩子函数中的start函数而start函数需要传递3个参数分别是标签名tag、标签属性attrs、标签是否自闭合unary。标签名通过正则匹配的结果就可以拿到即上面代码中的start[1]而标签属性attrs以及标签是否自闭合unary需要进一步解析。 解析标签属性 我们知道标签属性一般是写在开始标签的标签名之后的如下 div classa idb/div另外我们在上面匹配是否是开始标签的正则中已经可以拿到开始标签的标签名即上面代码中的start[0]那么我们可以将这一部分先从模板字符串中截掉则剩下的部分如下 classa idb/div那么我们只需用剩下的这部分去匹配标签属性的正则就可以将标签属性提取出来了如下 const attribute /^\s*([^\s\/])(?:\s*()\s*(?:([^]*)|([^]*)|([^\s])))?/ let html classa idb/div let attr html.match(attribute) console.log(attr) // [classa, class, , a, undefined, undefined, index: 0, input: classa idb/div, groups: undefined]可以看到第一个标签属性classa已经被拿到了。另外标签属性有可能有多个也有可能没有如果没有的话那好办匹配标签属性的正则就会匹配失败标签属性就为空数组而如果标签属性有多个的话那就需要循环匹配了匹配出第一个标签属性后就把该属性截掉用剩下的字符串继续匹配直到不再满足正则为止代码如下 const attribute /^\s*([^\s\/])(?:\s*()\s*(?:([^]*)|([^]*)|([^\s])))?/ const startTagClose /^\s*(\/?)/ const match {tagName: start[1],attrs: [],start: index } while (!(end html.match(startTagClose)) (attr html.match(attribute))) {advance(attr[0].length)match.attrs.push(attr) }在上面代码的while循环中如果剩下的字符串不符合开始标签的结束特征startTagClose并且符合标签属性的特征的话那就说明还有未提取出的标签属性那就进入循环继续提取直到把所有标签属性都提取完毕。 所谓不符合开始标签的结束特征是指当前剩下的字符串不是以开始标签结束符开头的我们知道一个开始标签的结束符有可能是一个非自闭合标签也有可能是/自闭合标签如果剩下的字符串如/div以开始标签的结束符开头那么就表示标签属性已经被提取完毕了。 解析标签是否是自闭合 在HTML中有自闭合标签如img src/也有非自闭合标签如div/div这两种类型的标签在创建AST节点是处理方式是有区别的所以我们需要解析出当前标签是否是自闭合标签。 解析的方式很简单我们知道经过标签属性提取之后那么剩下的字符串无非就两种如下 !--非自闭合标签-- /div或 !--自闭合标签-- /所以我们可以用剩下的字符串去匹配开始标签结束符正则如下 const startTagClose /^\s*(\/?)/ let end html.match(startTagClose) /div.match(startTagClose) // [, , index: 0, input: /div, groups: undefined] /.match(startTagClose) // [/, /, index: 0, input: /div/div, groups: undefined]可以看到非自闭合标签匹配结果中的end[1]为而自闭合标签匹配结果中的end[1]为/。所以根据匹配结果的end[1]是否是我们即可判断出当前标签是否为自闭合标签源码如下 const startTagClose /^\s*(\/?)/ let end html.match(startTagClose) if (end) {match.unarySlash end[1]advance(end[0].length)match.end indexreturn match }经过以上两步开始标签就已经解析完毕了完整源码如下 const ncname [a-zA-Z_][\\w\\-\\.]* const qnameCapture ((?:${ncname}\\:)?${ncname}) const startTagOpen new RegExp(^${qnameCapture}) const startTagClose /^\s*(\/?)/function parseStartTag () {const start html.match(startTagOpen)// div/div.match(startTagOpen) [div,div,index:0,input:div/div]if (start) {const match {tagName: start[1],attrs: [],start: index}advance(start[0].length)let end, attr/*** div a1 b2 c3/div* 从div之后到开始标签的结束符号之前一直匹配属性attrs* 所有属性匹配完之后html字符串还剩下* 自闭合标签剩下/* 非自闭合标签剩下/div*/while (!(end html.match(startTagClose)) (attr html.match(attribute))) {advance(attr[0].length)match.attrs.push(attr)}/*** 这里判断了该标签是否为自闭合标签* 自闭合标签如:input typetext /* 非自闭合标签如:div/div* /div.match(startTagClose) [, , index: 0, input: /div, groups: undefined]* /div/div.match(startTagClose) [/, /, index: 0, input: /div/div, groups: undefined]* 因此我们可以通过end[1]是否是/来判断该标签是否是自闭合标签*/if (end) {match.unarySlash end[1]advance(end[0].length)match.end indexreturn match}} }通过源码可以看到调用parseStartTag函数如果模板字符串符合开始标签的特征则解析开始标签并将解析结果返回如果不符合开始标签的特征则返回undefined。 解析完毕后就可以用解析得到的结果去调用start钩子函数去创建元素型的AST节点了。 在源码中Vue并没有直接去调start钩子函数去创建AST节点而是调用了handleStartTag函数在该函数内部才去调的start钩子函数为什么要这样做呢这是因为虽然经过parseStartTag函数已经把创建AST节点必要信息提取出来了但是提取出来的标签属性数组还是需要处理一下下面我们就来看一下handleStartTag函数都做了些什么事。handleStartTag函数源码如下 function handleStartTag (match) {const tagName match.tagNameconst unarySlash match.unarySlashif (expectHTML) {// ...}const unary isUnaryTag(tagName) || !!unarySlashconst l match.attrs.lengthconst 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.shouldDecodeNewlinesattrs[i] {name: args[1],value: decodeAttr(value, shouldDecodeNewlines)}}if (!unary) {stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })lastTag tagName}if (options.start) {options.start(tagName, attrs, unary, match.start, match.end)}}handleStartTag函数用来对parseStartTag函数的解析结果进行进一步处理它接收parseStartTag函数的返回值作为参数。 handleStartTag函数的开始定义几个常量 const tagName match.tagName // 开始标签的标签名 const unarySlash match.unarySlash // 是否为自闭合标签的标志自闭合为,非自闭合为/ const unary isUnaryTag(tagName) || !!unarySlash // 布尔值标志是否为自闭合标签 const l match.attrs.length // match.attrs 数组的长度 const attrs new Array(l) // 一个与match.attrs数组长度相等的数组接下来是循环处理提取出来的标签属性数组match.attrs如下 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.shouldDecodeNewlinesattrs[i] {name: args[1],value: decodeAttr(value, shouldDecodeNewlines)} }上面代码中首先定义了 args常量它是解析出来的标签属性数组中的每一个属性对象即match.attrs 数组中每个元素对象。 它长这样 const args [classa, class, , a, undefined, undefined, index: 0, input: classa idb/div, groups: undefined]接着定义了value用于存储标签属性的属性值我们可以看到在代码中尝试取args的args[3]、args[4]、args[5]如果都取不到则给value复制为空 const value args[3] || args[4] || args[5] || 接着定义了shouldDecodeNewlines这个常量主要是做一些兼容性处理 如果 shouldDecodeNewlines 为 true意味着 Vue 在编译模板的时候要对属性值中的换行符或制表符做兼容处理。而shouldDecodeNewlinesForHref为true 意味着Vue在编译模板的时候要对a标签的 href属性值中的换行符或制表符做兼容处理。 const shouldDecodeNewlines tagName a args[1] href? options.shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesconst value args[3] || args[4] || args[5] || 最后将处理好的结果存入之前定义好的与match.attrs数组长度相等的attrs数组中如下 attrs[i] {name: args[1], // 标签属性的属性名如classvalue: decodeAttr(value, shouldDecodeNewlines) // 标签属性的属性值如class对应的a }最后如果该标签是非自闭合标签则将标签推入栈中关于栈这个概念后面会说到如下 if (!unary) {stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })lastTag tagName }如果该标签是自闭合标签现在就可以调用start钩子函数并传入处理好的参数来创建AST节点了如下 if (options.start) {options.start(tagName, attrs, unary, match.start, match.end) }以上就是开始标签的解析以及调用start钩子函数创建元素型的AST节点的所有过程。 3.5 解析结束标签 结束标签的解析要比解析开始标签容易多了因为它不需要解析什么属性只需要判断剩下的模板字符串是否符合结束标签的特征如果是就将结束标签名提取出来再调用4个钩子函数中的end函数就好了。 首先判断剩余的模板字符串是否符合结束标签的特征如下 const ncname [a-zA-Z_][\\w\\-\\.]* const qnameCapture ((?:${ncname}\\:)?${ncname}) const endTag new RegExp(^\\/${qnameCapture}[^]*) const endTagMatch html.match(endTag)/div.match(endTag) // [/div, div, index: 0, input: /div, groups: undefined] div.match(endTag) // null上面代码中如果模板字符串符合结束标签的特征则会获得匹配结果数组如果不合符则得到null。 接着再调用end钩子函数如下 if (endTagMatch) {const curIndex indexadvance(endTagMatch[0].length)parseEndTag(endTagMatch[1], curIndex, index)continue }在上面代码中没有直接去调用end函数而是调用了parseEndTag函数关于parseEndTag函数内部的作用我们后面会介绍到在这里你暂时可以理解为该函数内部就是去调用了end钩子函数。 3.6 解析文本 终于到了解析最后一种文本类型的内容了为什么要把解析文本类型放在最后一个介绍呢我们仔细想一下前面五种类型都是以开头的只有文本类型的内容不是以开头的所以我们在解析模板字符串的时候可以先判断一下字符串是不是以开头的如果是则继续判断是以上五种类型的具体哪一种而如果不是的话那它肯定就是文本了。解析文本也比较容易在解析模板字符串之前我们先查找一下第一个出现在什么位置如果第一个在第一个位置那么说明模板字符串是以其它5种类型开始的如果第一个不在第一个位置而在模板字符串中间某个位置那么说明模板字符串是以文本开头的那么从开头到第一个出现的位置就都是文本内容了如果在整个模板字符串里没有找到那说明整个模板字符串都是文本。这就是解析思路接下来我们对照源码来了解一下实际的解析过程源码如下 let textEnd html.indexOf() // 在第一个位置为其余5种类型 if (textEnd 0) {// ... } // 不在第一个位置文本开头 if (textEnd 0) {// 如果html字符串不是以开头,说明前面的都是纯文本无需处理// 那就把以后的内容拿出来赋给restrest html.slice(textEnd)while (!endTag.test(rest) !startTagOpen.test(rest) !comment.test(rest) !conditionalComment.test(rest)) {// in plain text, be forgiving and treat it as text/*** 用以后的内容rest去匹配endTag、startTagOpen、comment、conditionalComment* 如果都匹配不上表示是属于文本本身的内容*/// 在之后查找是否还有next rest.indexOf(, 1)// 如果没有了表示后面也是文本if (next 0) break// 如果还有表示是文本中的一个字符textEnd next// 那就把next之后的内容截出来继续下一轮循环匹配rest html.slice(textEnd)}// 是结束标签的开始 ,说明从开始到都是文本截取出来text html.substring(0, textEnd)advance(textEnd) } // 整个模板字符串里没有找到,说明整个模板字符串都是文本 if (textEnd 0) {text htmlhtml } // 把截取出来的text转化成textAST if (options.chars text) {options.chars(text) }源码的逻辑很清晰根据在不在第一个位置以及整个模板字符串里没有都分别进行了处理。 值得深究的是如果不在第一个位置而在模板字符串中间某个位置那么说明模板字符串是以文本开头的那么从开头到第一个出现的位置就都是文本内容了接着我们还要从第一个的位置继续向后判断因为还存在这样一种情况那就是如果文本里面本来就包含一个例如12/div。为了处理这种情况我们把从第一个的位置直到模板字符串结束都截取出来记作rest如下 let rest html.slice(textEnd)接着用rest去匹配以上5种类型的正则如果都匹配不上则表明这个是属于文本本身的内容如下 while (!endTag.test(rest) !startTagOpen.test(rest) !comment.test(rest) !conditionalComment.test(rest) ) {}如果都匹配不上则表明这个是属于文本本身的内容接着以这个的位置继续向后查找看是否还有如果没有了则表示后面的都是文本如果后面还有下一个那表明至少在这个到下一个中间的内容都是文本至于下一个以后的内容是什么则还需要重复以上的逻辑继续判断。代码如下 while (!endTag.test(rest) !startTagOpen.test(rest) !comment.test(rest) !conditionalComment.test(rest) ) {// in plain text, be forgiving and treat it as text/*** 用以后的内容rest去匹配endTag、startTagOpen、comment、conditionalComment* 如果都匹配不上表示是属于文本本身的内容*/// 在之后查找是否还有next rest.indexOf(, 1)// 如果没有了表示后面也是文本if (next 0) break// 如果还有表示是文本中的一个字符textEnd next// 那就把next之后的内容截出来继续下一轮循环匹配rest html.slice(textEnd) }最后截取文本内容text并调用4个钩子函数中的chars函数创建文本型的AST节点。 4. 如何保证AST节点层级关系 上一章节我们介绍了HTML解析器是如何解析各种不同类型的内容并且调用钩子函数创建不同类型的AST节点。此时你可能会有个疑问我们上面创建的AST节点都是单独创建且分散的而真正的DOM节点都是有层级关系的那如何来保证AST节点的层级关系与真正的DOM节点相同呢 关于这个问题Vue也注意到了。Vue在HTML解析器的开头定义了一个栈stack这个栈的作用就是用来维护AST节点层级的那么它是怎么维护的呢通过前文我们知道HTML解析器在从前向后解析模板字符串时每当遇到开始标签时就会调用start钩子函数那么在start钩子函数内部我们可以将解析得到的开始标签推入栈中而每当遇到结束标签时就会调用end钩子函数那么我们也可以在end钩子函数内部将解析得到的结束标签所对应的开始标签从栈中弹出。请看如下例子 加入有如下模板字符串 divpspan/span/p/div当解析到开始标签div时就把div推入栈中然后继续解析当解析到p时再把p推入栈中同理再把span推入栈中当解析到结束标签/span时此时栈顶的标签刚好是span的开始标签那么就用span的开始标签和结束标签构建AST节点并且从栈中把span的开始标签弹出那么此时栈中的栈顶标签p就是构建好的span的AST节点的父节点如下图 这样我们就找到了当前被构建节点的父节点。这只是栈的一个用途它还有另外一个用途我们再看如下模板字符串 divpspan/p/div按照上面的流程解析这个模板字符串时当解析到结束标签/p时此时栈顶的标签应该是p才对而现在是span那么就说明span标签没有被正确闭合此时控制台就会抛出警告‘tag has no matching end tag.’相信这个警告你一定不会陌生。这就是栈的第二个用途 检测模板字符串中是否有未正确闭合的标签。 OK有了这个栈的概念之后我们再回看上一章HTML解析器解析不同内容的代码。 5. 回归源码 5.1 HTML解析器源码 以上内容都了解了之后我们回归源码逐句分析HTML解析器parseHTML函数函数定义如下 function parseHTML(html, options) {var stack [];var expectHTML options.expectHTML;var isUnaryTag$$1 options.isUnaryTag || no;var canBeLeftOpenTag$$1 options.canBeLeftOpenTag || no;var index 0;var last, lastTag;// 开启一个 while 循环循环结束的条件是 html 为空即 html 被 parse 完毕while (html) {last html;// 确保即将 parse 的内容不是在纯文本标签里 (script,style,textarea)if (!lastTag || !isPlainTextElement(lastTag)) {let textEnd html.indexOf()/*** 如果html字符串是以开头,则有以下几种可能* 开始标签:div* 结束标签:/div* 注释:!-- 我是注释 --* 条件注释:!-- [if !IE] -- !-- [endif] --* DOCTYPE:!DOCTYPE html* 需要一一去匹配尝试*/if (textEnd 0) {// 解析是否是注释if (comment.test(html)) {}// 解析是否是条件注释if (conditionalComment.test(html)) {}// 解析是否是DOCTYPEconst doctypeMatch html.match(doctype)if (doctypeMatch) {}// 解析是否是结束标签const endTagMatch html.match(endTag)if (endTagMatch) {}// 匹配是否是开始标签const startTagMatch parseStartTag()if (startTagMatch) {}}// 如果html字符串不是以开头,则解析文本类型let text, rest, nextif (textEnd 0) {}// 如果在html字符串中没有找到表示这一段html字符串都是纯文本if (textEnd 0) {text htmlhtml }// 把截取出来的text转化成textASTif (options.chars text) {options.chars(text)}} else {// 父元素为script、style、textarea时其内部的内容全部当做纯文本处理}//将整个字符串作为文本对待if (html last) {options.chars options.chars(html);if (!stack.length options.warn) {options.warn((Mal-formatted tag at end of template: \ html \));}break}}// Clean up any remaining tagsparseEndTag();//parse 开始标签function parseStartTag() {}//处理 parseStartTag 的结果function handleStartTag(match) {}//parse 结束标签function parseEndTag(tagName, start, end) {} }上述代码中大致可分为三部分 定义的一些常量和变量while 循环解析过程中用到的辅助函数 我们一一来分析 首先定义了几个常量如下 const stack [] // 维护AST节点层级的栈 const expectHTML options.expectHTML const isUnaryTag options.isUnaryTag || no const canBeLeftOpenTag options.canBeLeftOpenTag || no //用来检测一个标签是否是可以省略闭合标签的非自闭合标签 let index 0 //解析游标标识当前从何处开始解析模板字符串 let last, // 存储剩余还未解析的模板字符串lastTag // 存储着位于 stack 栈顶的元素接着开启while 循环循环的终止条件是 模板字符串html为空即模板字符串被全部编译完毕。在每次while循环中 先把 html的值赋给变量 last如下 last html这样做的目的是如果经过上述所有处理逻辑处理过后html字符串没有任何变化即表示html字符串没有匹配上任何一条规则那么就把html字符串当作纯文本对待创建文本类型的AST节点并且如果抛出异常模板字符串中标签格式有误。如下 //将整个字符串作为文本对待 if (html last) {options.chars options.chars(html);if (!stack.length options.warn) {options.warn((Mal-formatted tag at end of template: \ html \));}break }接着我们继续看while循环体内的代码 while (html) {// 确保即将 parse 的内容不是在纯文本标签里 (script,style,textarea)if (!lastTag || !isPlainTextElement(lastTag)) {} else {// parse 的内容是在纯文本标签里 (script,style,textarea)} }在循环体内首先判断了待解析的html字符串是否在纯文本标签里如script,style,textarea因为在这三个标签里的内容肯定不会有HTML标签所以我们可直接当作文本处理判断条件如下 !lastTag || !isPlainTextElement(lastTag)前面我们说了lastTag为栈顶元素!lastTag即表示当前html字符串没有父节点而isPlainTextElement(lastTag) 是检测 lastTag 是否为是那三个纯文本标签之一是的话返回true不是返回fasle。 也就是说当前html字符串要么没有父节点要么父节点不是纯文本标签则接下来就可以依次解析那6种类型的内容了关于6种类型内容的处理方式前文已经逐个介绍过此处不再重复。 5.2 parseEndTag函数源码 接下来我们看一下之前在解析结束标签时遗留的parseEndTag函数该函数定义如下 function parseEndTag (tagName, start, end) {let pos, lowerCasedTagNameif (start null) start indexif (end null) end indexif (tagName) {lowerCasedTagName tagName.toLowerCase()}// Find the closest opened tag of the same typeif (tagName) {for (pos stack.length - 1; pos 0; pos--) {if (stack[pos].lowerCasedTag lowerCasedTagName) {break}}} else {// If no tag name is provided, clean shoppos 0}if (pos 0) {// Close all the open elements, up the stackfor (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.)}if (options.end) {options.end(stack[i].tag, start, end)}}// Remove the open elements from the stackstack.length poslastTag 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)}}} }该函数接收三个参数分别是结束标签名tagName、结束标签在html字符串中的起始和结束位置start和end。 这三个参数其实都是可选的根据传参的不同其功能也不同。 第一种是三个参数都传递用于处理普通的结束标签第二种是只传递tagName第三种是三个参数都不传递用于处理栈中剩余未处理的标签 如果tagName存在那么就从后往前遍历栈在栈中寻找与tagName相同的标签并记录其所在的位置pos如果tagName不存在则将pos置为0。如下 if (tagName) {for (pos stack.length - 1; pos 0; pos--) {if (stack[pos].lowerCasedTag lowerCasedTagName) {break}} } else {// If no tag name is provided, clean shoppos 0 }接着当pos0时开启一个for循环从栈顶位置从后向前遍历直到pos处如果发现stack栈中存在索引大于pos的元素那么该元素一定是缺少闭合标签的。这是因为在正常情况下stack栈的栈顶元素应该和当前的结束标签tagName 匹配也就是说正常的pos应该是栈顶位置后面不应该再有元素如果后面还有元素那么后面的元素就都缺少闭合标签 那么这个时候如果是在非生产环境会抛出警告告诉你缺少闭合标签。除此之外还会调用 options.end(stack[i].tag, start, end)立即将其闭合这是为了保证解析结果的正确性。 if (pos 0) {// Close all the open elements, up the stackfor (var i stack.length - 1; i pos; i--) {if (i pos || !tagName ) {options.warn((tag (stack[i].tag) has no matching end tag.));}if (options.end) {options.end(stack[i].tag, start, end);}}// Remove the open elements from the stackstack.length pos;lastTag pos stack[pos - 1].tag; }最后把pos位置以后的元素都从stack栈中弹出以及把lastTag更新为栈顶元素: stack.length pos; lastTag pos stack[pos - 1].tag;接着如果pos没有大于等于0即当 tagName 没有在 stack 栈中找到对应的开始标签时pos 为 -1 。那么此时再判断 tagName 是否为br 或p标签为什么要单独判断这两个标签呢这是因为在浏览器中如果我们写了如下HTML div/br/p /div浏览器会自动把/br标签解析为正常的 标签而对于/p浏览器则自动将其补全为p/p所以Vue为了与浏览器对这两个标签的行为保持一致故对这两个便签单独判断处理如下 if (lowerCasedTagName br) {if (options.start) {options.start(tagName, [], true, start, end) // 创建brAST节点} } // 补全p标签并创建AST节点 if (lowerCasedTagName p) {if (options.start) {options.start(tagName, [], false, start, end)}if (options.end) {options.end(tagName, start, end)} }以上就是对结束标签的解析与处理。 另外在while循环后面还有一行代码 parseEndTag()这行代码执行的时机是html last即html字符串中的标签格式有误时会跳出while循环此时就会执行这行代码这行代码是调用parseEndTag函数并不传递任何参数前面我们说过如果parseEndTag函数不传递任何参数是用于处理栈中剩余未处理的标签。这是因为如果不传递任何函数此时parseEndTag函数里的pos就为0那么pos0就会恒成立那么就会逐个警告缺少闭合标签并调用 options.end将其闭合。 6. 总结 本篇文章主要介绍了HTML解析器的工作流程以及工作原理文章比较长但是逻辑并不复杂。 首先介绍了HTML解析器的工作流程一句话概括就是一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点最终完成将整个模板字符串转化成AST。 接着介绍了HTML解析器是如何解析用户所写的模板字符串中各种类型的内容的把各种类型的解析方式都分别进行了介绍。 其次介绍了在解析器内维护了一个栈用来保证构建的AST节点层级与真正DOM层级一致。 了解了思想之后最后回归源码学习了源码中一些处理细节的地方。
http://www.dnsts.com.cn/news/52525.html

相关文章:

  • 吉林整站优化免费照片的网站模板免费下载
  • 网站排行榜前十名品牌运营
  • WordPress输入密码可见seo外链怎么做能看到效果
  • 域名查询网站烟台建设集团招聘信息网站
  • 莞城网站推广wordpress模板 免费下载
  • 小学做试卷的网站wordpress 页面标题
  • o2o与网站建设中英企业网站源码
  • 深圳高端网站设计如何微信小程序注册
  • 网页设计师的主要职责网站页面的优化
  • 宁波网站设计服务收费价格福州网站如何制作
  • 新浪博客怎么做网站中国万网域名注册价格
  • 罗湖区网站建设做网站管理好吗
  • 贵港网站制作临安区规划建设局网站
  • 自己设计手机的网站重庆网页设计培训
  • 班级网站自助建设功能wordpress的编辑器插件
  • 百度熊掌号 wordpress上海关键词优化推荐
  • 如何构建自己的网站如何加强省市网站建设
  • 怎么做电子商务的网站用cms织梦做网站图文教程
  • 商标设计软件生成器谷歌seo排名工具
  • 菜谱网站开发系统wordpress的字体
  • 腾讯云wordpress教程视频鞍山做网站优化公司
  • 写作兼职网站wordpress的导航源码
  • 手机端网站开发源码wordpress注册跳过邮箱验证码
  • 怎么挑选网站主机搜索引擎优化步骤
  • 做个外贸网站大概多少钱如何在图片上添加文字做网站
  • 网站建设数据安全分析中文域名值得注册吗
  • 为什么网站数量减少网站开发整体流程图
  • 做网站公司做网站公司沪佳装修贵吗
  • 怎么在网站添加关键词铜陵网站建设价格
  • 2024免费网站推广yandex搜索入口