日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

Vue源码阅读(12):解析器

發布時間:2024/3/26 vue 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Vue源码阅读(12):解析器 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

今天聊聊解析器,解析器的作用是將程序員編寫的模板字符串解析成抽象語法樹,抽象語法樹可以理解成模板字符串的對象表示形式,其本質并沒有什么神奇的,只不過是 JS 中最為常見的對象字面量。

通過抽象語法樹,Vue 可以以一種統一的格式來表示不同編碼風格的模板字符串,這種統一是接下來進行優化器和代碼生成器處理的基礎。接下來,我們看一個簡單模板字符串解析成的抽象語法樹是什么樣的。

new Vue({template: `<div class="container"><h1>我是靜態文本</h1><h1>名字:{{name}}</h1></div>` })

解析成的抽象語法樹如下所示:

let ast = {attrsList: [],attrsMap: {class: "container"},children:[{attrsList: [],attrsMap: {},children: [{static: true, text: "我是靜態文本", type: 3}],plain: true,static: true,staticInFor: false,staticRoot: false,tag: "h1",type: 1},{attrsList: [],attrsMap: {},children: [{type: 2, expression: ""名字:"+_s(name)", text: "名字:{{name}}", static: false}],plain: true,static: false,staticRoot: false,tag: "h1",type: 1}],parent: undefined,plain: false,static: false,staticClass: ""container"",staticRoot: false,tag: "div",type: 1 }

可以看到,抽象語法樹只是 JS 中普通的對象字面量,所以,大家要以平常心看待它。

接下來,開始看解析器的源碼實現。

1,src/compiler/index.js ==> function baseCompile(){}

export const createCompiler = createCompilerCreator(// 真正執行編譯功能的函數,分為三步走:(1)解析器 ==>(2)優化器 ==>(3)代碼生成器function baseCompile (template: string,options: CompilerOptions): CompiledResult {// 1,解析器。將模板字符串轉換成抽象語法樹const ast = parse(template.trim(), options)// 2,優化器。遍歷抽象語法樹,標記靜態節點,// 因為靜態節點是不會變化的,所以重新渲染視圖的時候,能夠直接跳過靜態節點,提升效率。optimize(ast, options)// 3,代碼生成器。使用抽象語法樹生成渲染函數字符串const code = generate(ast, options)return {ast,render: code.render,staticRenderFns: code.staticRenderFns}} )

實現模板編譯功能的方法是 baseCompile,其內部調用了三個函數,分別對應:解析器、優化器、代碼生成器。

2,src/compiler/parser/index.js ==> function parse(){}

/*** Convert HTML string to AST.*/ export function parse (template: string,options: CompilerOptions ): ASTElement | void {// 解析 options 中的配置,并將配置項賦值給變量 //platformIsPreTag = options.isPreTag || noplatformMustUseProp = options.mustUseProp || noplatformGetTagNamespace = options.getTagNamespace || notransforms = pluckModuleFunction(options.modules, 'transformNode')preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')delimiters = options.delimiters// 解析過程中用到的變量 //// 節點棧,用于維護父子關系const stack = []// 保存抽象語法樹的變量,也是抽象語法樹的根節點let root// 當前處理節點的父節點let currentParentconst preserveWhitespace = options.preserveWhitespace !== falselet inVPre = falselet inPre = falselet warned = false// 輔助函數 //function warnOnce (msg) {}function endPre (element) {}// 調用 parseHTML 開始解析模板字符串parseHTML(template, {warn,expectHTML: options.expectHTML,isUnaryTag: options.isUnaryTag,canBeLeftOpenTag: options.canBeLeftOpenTag,shouldDecodeNewlines: options.shouldDecodeNewlines,shouldKeepComment: options.comments,// 下面的回調函數用于 AST 節點的生成和整個抽象語法樹父子 AST 節點的維護// 針對開始標簽start (tag, attrs, unary) {},// 針對結束標簽end () {},// 針對文本內容chars (text: string) {},// 針對評論節點comment (text: string) {}})// AST type 解釋// 1:元素節點// 2:含有表達式的文本節點// 3:純文本節點return root }// 回調函數使用的工具函數 // function processPre (el) {} function processRawAttrs (el) {} export function processElement (element: ASTElement, options: CompilerOptions) {} function processKey (el) {} function processRef (el) {} export function processFor (el: ASTElement) {} function processIf (el) {} function processIfConditions (el, parent) {} function findPrevElement (children: Array<any>): ASTElement | void {} export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {} function processOnce (el) {} function processSlot (el) {} function processComponent (el) {} function processAttrs (el) {} function checkInFor (el: ASTElement): boolean {} function parseModifiers (name: string): Object | void {} function makeAttrsMap (attrs: Array<Object>): Object {} function isTextTag (el): boolean {} function isForbiddenTag (el): boolean {} function guardIESVGBug (attrs) {} function checkForAliasModel (el, value) {}

我們在上文說過,解析器內部細分了很多小的解析器,各自處理對應的工作,其中作為主線的是 HTML 解析器(對應上面?parseHTML 函數調用),整個解析器的處理過程就是 HTML 解析器不斷的用正則表達式處理模板字符串的過程,每處理完一小段模板字符串,就會將其從模板字符串中截取掉,直到模板字符串被解析成空字符串(""),解析器的工作也就完成了。

在 HTML 解析器解析到指定的節點時,會將解析的信息作為參數執行回調函數(上面代碼中的 start、end、chars、comment),這些回調函數負責生成 AST 節點和維護 AST 節點父子關系。

接下來,開始看 parseHTML 函數的內容。

3,src/compiler/parser/html-parser.js ==> function parseHTML(){}

parseHTML 函數內容很復雜,但是思路卻很清晰,就是使用 while(html) 不斷的循環處理模板字符串,解析的方式是使用正則表達式處理模板字符串,每處理一小段模板字符串,就會調用對應的回調函數,在回調函數中進行 AST 節點的生成和 AST 樹的維護,這一小段模板字符串處理完成后,就會將其從模板字符串中截取掉,直至截取成空字符串(""),接下來看看 parseHTML 的代碼,先搞清除總體邏輯。

export function parseHTML (html, options) {const stack = []let index = 0// last 變量用于記錄 html 字符串上一次解析之前的狀態let last, lastTag// 解析 html 的過程,就是不斷的截取和解析的過程,直至 html 字符串被解析完// 所以在這里,使用 while (html) 不斷的遍歷 html 字符串while (html) {last = html// !lastTag:針對首次進入解析的狀態// !isPlainTextElement(lastTag):上一個處理的標簽不是 script、style、textareaif (!lastTag || !isPlainTextElement(lastTag)) {// 獲取當前的 html 中首個 '<' 的下標位置let textEnd = html.indexOf('<')// 如果 < 的下標是 0 的話,說明當前 html 字符串的開頭是一個標簽if (textEnd === 0) {接下來判斷這個開頭的標簽是什么類型的標簽// 判斷是不是注釋標簽if (comment.test(html)) {}// 判斷開頭的標簽是不是 <![if !IE]>,如果是的話,就什么都不用做,直接截取跳過即可 if (conditionalComment.test(html)) {}// 判斷是不是 DOCTYPE 節點,如果是的話,也是直接截取掉并跳過const doctypeMatch = html.match(doctype)if (doctypeMatch) {advance(doctypeMatch[0].length)continue}接下來就是比較重點的開始標簽和結束標簽的判斷和處理// 對結束標簽進行匹配和處理const endTagMatch = html.match(endTag)if (endTagMatch) {// 截取掉匹配的結束標簽advance(endTagMatch[0].length)// 調用 parseEndTag 輔助函數對該結束標簽進行處理parseEndTag(endTagMatch[1], curIndex, index)continue}// 對開始標簽進行匹配和處理// parseStartTag 函數能夠返回解析后的開始標簽的信息const startTagMatch = parseStartTag()if (startTagMatch) {// 如果當前 html 的開頭的確是開始標簽的話,則調用 handleStartTag 進行額外的處理handleStartTag(startTagMatch)continue}}// 這一部分邏輯是處理標簽內文本內容的let text, rest, nextif (textEnd >= 0) {// 獲取當前的 html 字符串除最前面的文本內容剩下的部分rest = 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 textnext = rest.indexOf('<', 1)if (next < 0) breaktextEnd += nextrest = html.slice(textEnd)}// 用于處理文本字符串中有 '<' 符號的情況 //// 截取出當前需要處理的文本節點text = html.substring(0, textEnd)// 從 html 中截取掉文本節點advance(textEnd)}// 處理找不到 '<' 的情況,說明已經沒有待處理的標簽了,// 將 html 置為 '',外面的 while(html) 下次循環就會結束if (textEnd < 0) {text = htmlhtml = ''}if (options.chars && text) {// 調用 options 中的 chars 回調函數,進行文本節點的處理options.chars(text)}} else {// 下面的代碼針對 上一個處理的 tag 是 script、style、textarea 的情況let endTagLength = 0const stackedTag = lastTag.toLowerCase()const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))const rest = html.replace(reStackedTag, function (all, text, endTag) {endTagLength = endTag.lengthif (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {text = text.replace(/<!--([\s\S]*?)-->/g, '$1').replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')}if (shouldIgnoreFirstNewline(stackedTag, text)) {text = text.slice(1)}if (options.chars) {options.chars(text)}return ''})index += html.length - rest.lengthhtml = restparseEndTag(stackedTag, index - endTagLength, index)}if (html === last) {options.chars && options.chars(html)if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {options.warn(`Mal-formatted tag at end of template: "${html}"`)}break}}// 用于截取html字符串的工具函數function advance (n) {}// 解析開始標簽的工具函數function parseStartTag () {}// 進一步處理開始標簽,并會調用 options.start 回調函數function handleStartTag (match) {}// 解析結束標簽的工具函數function parseEndTag (tagName, start, end) {} }

3-1,while (html) {}

借助 while 不斷地循環處理 html 字符串,每處理一小段,就會將其從 html 中截取掉,直至 html 被截成空字符串(""),解析也就完成了。

3-2,if (!lastTag || !isPlainTextElement(lastTag)) {} else {}

isPlainTextElement(lastTag) 用于判斷當前處理節點的父節點是不是?script、style、textarea,script、style、textarea 類型節點的子節點需要特殊處理,在這里用 if else 將處理邏輯分開。

3-3,if (!lastTag || !isPlainTextElement(lastTag)) {}

如果當前處理節點的父節點不是?script、style、textarea 的話,代碼流程邏輯如下:

// 獲取當前的 html 中首個 '<' 的下標位置 let textEnd = html.indexOf('<')// 如果 < 的下標是 0 的話,說明當前 html 字符串的開頭是一個標簽 if (textEnd === 0) {// 在這里,判斷具體是什么類型的標簽// 1,判斷是不是注釋標簽if (comment.test(html)) {}// 2,判斷標簽是不是 <![if !IE]>if (conditionalComment.test(html)) {const conditionalEnd = html.indexOf(']>')if (conditionalEnd >= 0) {// 如果是 <![if !IE]> 標簽的話,就什么都不用做,直接截取掉并跳過advance(conditionalEnd + 2)continue}}// 3,判斷是不是 DOCTYPE 節點,如果是的話,也是直接截取掉并跳過const doctypeMatch = html.match(doctype)if (doctypeMatch) {}// 4,判斷是不是結束標簽,如果是的話,會進行解析和處理const endTagMatch = html.match(endTag)if (endTagMatch) {// 截取掉匹配的結束標簽advance(endTagMatch[0].length)// 對該結束標簽進行處理parseEndTag(endTagMatch[1], curIndex, index)}// 5,解析判斷是不是開始標簽,如果是的話,則會進行進一步的處理const startTagMatch = parseStartTag()if (startTagMatch) {// 如果當前 html 的開頭的確是開始標簽的話,則調用 handleStartTag 進行處理handleStartTag(startTagMatch)continue} }let text, rest, next // 處理類似于這種模板字符串: "我是小明</h1></div>",textEnd 大于 0,textEnd 之前的內容都是當前應當處理的文本節點 if (textEnd >= 0) {rest = html.slice(textEnd)text = html.substring(0, textEnd)advance(textEnd) }// 文本節點的處理 if (options.chars && text) {// 調用 options 中的 chars 回調函數,創建該文本的 AST 節點options.chars(text) }

3-4,棧是如何維護節點父子關系的

假設我們有如下的模板字符串。

<div class="container"><h1>我是文本1</h1><h2>我是文本2</h2> </div>

當解析?div 的開始標簽的時候,我們向棧 push 這個 div 對應的 AST 節點。

<h1>我是文本1</h1><h2>我是文本2</h2> </div>

當解析 h1 的開始標簽的時候,我們向棧 push 這個 h1 對應的 AST 節點,當 push h1 對應 AST 節點的時候,程序能夠發現棧的頂端有一個 div 的 AST 節點,這就說明,當前的 h1 是 div 的子節點。

我是文本1</h1><h2>我是文本2</h2> </div>

?然后解析 "我是文本1" 這個文本節點,創建對應的 AST 節點,程序發現棧頂是一個 h1 AST 節點,所以這個文本節點是?h1 節點的子節點。

</h1><h2>我是文本2</h2> </div>

接下來解析 h1 結束標簽,程序發現棧頂是一個 h1 的 AST 節點,會進行出棧操作。

<h2>我是文本2</h2> </div>

接下來處理 h2 標簽,處理流程和上面的 h1 標簽是一樣的,這里就不贅述了。

處理到最后,模板字符串所有的內容都處理完了,棧也成了空棧。

總結:

  • 解析到開始標簽,就會入棧;
  • 解析到結束標簽,就會出棧;
  • 棧頂的 AST 節點是當前處理 AST 節點的父節點;
  • 3-5,function advance (n) {}

    該方法的作用是截取掉已經處理的模板字符串,參數是要截取字符串的長度。

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

    例如:有如下的模板字符串:

    let html = '<h1>我是文本</h1>'

    執行 advance(4) 之后,html 變成了。

    let html = '我是文本</h1>'

    3-6,function parseStartTag () {}

    用于解析開始標簽,我們直接看例子,假如有如下的開始標簽:

    <div class="container" style="margin-top: 30px;">

    其最終將會被解析成如下的對象。

    {attrs: [[" class="container"", "class", "=", "container", undefined, undefined],[" style="margin-top: 30px;"", "style", "=", "margin-top: 30px;", undefined, undefined]],end: 49,start: 0,tagName: "div",unarySlash: "" }

    3-7,function handleStartTag?(match) {}

    該函數的參數是 parseStartTag 函數的返回值,也就是上面被解析成的對象。

    該函數的作用是:將上面的 attrs 轉換成另外一種格式,判斷標簽是不是自閉和的標簽,然后調用 options.start(tagName, attrs, unary, match.start, match.end) 回調函數。

    function handleStartTag (match) {const tagName = match.tagNameconst unarySlash = match.unarySlash判斷是不是自閉和的標簽const unary = isUnaryTag(tagName) || !!unarySlash/ 遍歷處理標簽的 attrs,轉換成另外一種格式const l = match.attrs.lengthconst attrs = new Array(l)// 遍歷處理標簽的 attrsfor (let i = 0; i < l; i++) {const args = match.attrs[i]// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {if (args[3] === '') { delete args[3] }if (args[4] === '') { delete args[4] }if (args[5] === '') { delete args[5] }}const value = args[3] || args[4] || args[5] || ''attrs[i] = {name: args[1],value: decodeAttr(value,options.shouldDecodeNewlines)}}// 如果當前標簽不是自閉和標簽的話,需要將當前標簽的信息對象 push 到棧數組中。棧數組用于處理 html 中標簽的父子關系if (!unary) {stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })lastTag = tagName}if (options.start) {// 調用 options 中的 start 回調函數,生成該開始標簽的 ASToptions.start(tagName, attrs, unary, match.start, match.end)} }

    attrs 會被轉換成如下的格式:

    [{name: "class", value: "container"},{name: "style", value: "margin-top: 30px;"} ]

    3-8,function parseEndTag (tagName, start, end) {}

    parseEndTag 函數的作用是:

  • 維護 stack 棧數據(我們上面說了,結束標簽會進行退棧操作)
  • 根據不同的情況,調用 options.start()、options.end() 回調函數
  • 源碼解釋都是注釋中,這里就不贅述了。

    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) {// stack 棧從上往下找,尋找與 lowerCasedTagName 相同的標簽的下標// 一般情況下,相同的元素都是在棧頂,但這是DOM嵌套規范的情況下,// 有時候,不規范的嵌套,例如:<div><span></div>,在處理 </div> 的時候,與其對應的標簽就不在棧頂for (pos = stack.length - 1; pos >= 0; pos--) {if (stack[pos].lowerCasedTag === lowerCasedTagName) {break}}} else {// If no tag name is provided, clean shop// 此處對應 tagName 是 undefined 的情況,在這里不做討論pos = 0}// 如果 pos > 0,說明在棧中找到了與 lowerCasedTagName 相同的標簽if (pos >= 0) {// 從棧頂往棧底遍歷,直到當前處理標簽對應開始標簽的位置(pos)for (let i = stack.length - 1; i >= pos; i--) {// 用于處理類似于下面這種情況// <div><h1>Hello</h1>,h1 沒有閉合標簽,打印出警告。if (process.env.NODE_ENV !== 'production' &&(i > pos || !tagName) &&options.warn) {// 打印警告options.warn(`tag <${stack[i].tag}> has no matching end tag.`)}// <div><h1>Hello</h1>,當處理 h1 閉合標簽的時候,棧中有兩個元素// 棧頂// -------// h1// div// -------// 棧底// 即使模板字符串中沒有 h1 的閉合標簽,在這里也會為其執行 end 回調函數,// 為 h1 執行 end 回調函數之后,也會為 div 執行 end 回調函數// 關于這一點,大家可以做個測試,在 Vue 的模板中寫一個沒有閉合標簽的元素,// Vue 會發出警告,而且會為其添加閉合元素,添加閉合元素的源碼級別實現就在這里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') {// 針對處理這種模板字符串:<div></br></div>if (options.start) {options.start(tagName, [], true, start, end)}} else if (lowerCasedTagName === 'p') {// 針對處理這種模板字符串:<div></p></div>,會為 p 結束標簽增加對應的 <p> 開始標簽// 真實的 DOM 會變成這樣:<div><p></p></div>if (options.start) {options.start(tagName, [], false, start, end)}if (options.end) {options.end(tagName, start, end)}} }

    好了,parseHTML 的內容到這里就講完了,接下來說說用于生成 AST 節點和維護 AST 層級關系的回調函數(start、end、chars、comment)。

    4,講解回調函數

    回調函數定義在:src/compiler/parser/index.js ==> function parse(){}

    export function parse (template: string,options: CompilerOptions ): ASTElement | void {let root// 調用 parseHTML 開始解析模板字符串parseHTML(template, {warn,expectHTML: options.expectHTML,isUnaryTag: options.isUnaryTag,canBeLeftOpenTag: options.canBeLeftOpenTag,shouldDecodeNewlines: options.shouldDecodeNewlines,shouldKeepComment: options.comments,下面的回調函數用于 AST 元素的生成和 AST 樹結構的維護// 針對開始標簽的回調函數start (tag, attrs, unary) {},// 針對結束標簽的回調函數end () {},// 針對文本內容的回調函數chars (text: string) {},// 針對評論節點的回調函數comment (text: string) {}})return root }

    4-1,start (tag, attrs, unary) {}

    start 回調函數的作用是:

  • 創建標簽 AST 節點;
  • 進一步解析 AST 節點,增加更多的信息;
  • 維護 AST 樹結構;
  • 首先說第一點:創建標簽 AST 節點。

    let element: ASTElement = createASTElement(tag, attrs, currentParent)

    調用 createASTElement 方法生成 AST 節點,createASTElement 方法的源碼如下。

    export function createASTElement (tag: string,attrs: Array<Attr>,parent: ASTElement | void ): ASTElement {return {type: 1,tag,attrsList: attrs,attrsMap: makeAttrsMap(attrs),parent,children: []} }

    創建 AST 節點的源碼很簡單,根據傳遞進來的參數,構建 AST 對象即可。

    接下來說第二點:進一步解析 AST 節點,增加更多的信息。

    if (!inVPre) {processPre(element)if (element.pre) {inVPre = true} } if (platformIsPreTag(element.tag)) {inPre = true } if (inVPre) {processRawAttrs(element) } else if (!element.processed) {// structural directives// 處理 v-forprocessFor(element)// 處理 v-ifprocessIf(element)// 處理 v-onceprocessOnce(element)// element-scope stuffprocessElement(element, options) }

    例如上面的 processIf(element),就是用來進一步處理 v-if 的。

    假設有如下的模板字符串:

    <div class="container"><h1 v-if="isShow">文本信息</h1> </div>

    h1 標簽對應的 AST 節點剛創建時如下所示。

    {attrsList: [{name: "v-if", value: "isShow"}],attrsMap: {v-if: "isShow"},children: [],tag: "h1",type: 1 }

    我們可以看到其中的 v-if 是作為 attr 存在的,這需要進行進一步的解析。

    processIf (el) 的源碼如下所示。

    function processIf (el) {const exp = getAndRemoveAttr(el, 'v-if')if (exp) {el.if = expaddIfCondition(el, {exp: exp,block: el})} else {if (getAndRemoveAttr(el, 'v-else') != null) {el.else = true}const elseif = getAndRemoveAttr(el, 'v-else-if')if (elseif) {el.elseif = elseif}} }

    經過?processIf 處理的 AST 節點如下所示。

    {attrsList: [],attrsMap: {v-if: "isShow"},children: [],if: "isShow",ifConditions: [{exp: "isShow"}],tag: "h1",type: 1 }

    可以看到,多了 if 和 ifConditions 屬性。

    最后一點:維護 AST 樹結構。

    主要代碼如下所示,解釋都在注釋中:

    // tree management if (!root) {// 如果 root 為 undefined 的話,說明當前處理的就是根節點// 所以將 element 直接賦值給 rootroot = element } else if (!stack.length) {// 處理模板存在多個根節點的情況// 如果存在 root 節點,并且 stack 棧數組為空的話,說明模板存在多個根節點// 多個根節點的話,如果根節點上面有 v-if, v-else-if and v-else 來確保某一個特定時刻,只有一個根節點的話,// 也是可以被允許的。而如果沒有 v-if, v-else-if and v-else 的話,則會打印出警告if (root.if && (element.elseif || element.else)) {addIfCondition(root, {exp: element.elseif,block: element})} else if (process.env.NODE_ENV !== 'production') {warnOnce(`Component template should contain exactly one root element. ` +`If you are using v-if on multiple elements, ` +`use v-else-if to chain them instead.`)} }if (currentParent && !element.forbidden) {// 維護 AST 樹的父子關系currentParent.children.push(element)element.parent = currentParent }// 更新 currentParent 和 stack currentParent = element stack.push(element)

    4-2,end?() {}

    end () {// remove trailing whitespaceconst element = stack[stack.length - 1]const lastNode = element.children[element.children.length - 1]if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {element.children.pop()}// pop stackstack.length -= 1currentParent = stack[stack.length - 1] },

    第一段用于處理標簽內全是全是空格的情況,例如如下的模板字符串。

    <div class="container"><h1> </h1> </div>

    當處理到 </h1> 結束標簽的時候,就會進行第一段代碼的優化處理,處理后的效果如下所示:

    <div class="container"><h1></h1> </div>

    后面兩行代碼就很簡單了,對 stack 做出棧操作以及更新 currentParent

    4-3,chars?(text: string) {}

    chars (text: string) {if (!currentParent) {// 如果當前沒有 currentParent 的話,說明有兩種情況:// (1) 組件的 template 是一個純文本// (2) 當前的文本寫在標簽的外面// 這兩種情況都是不被允許的if (process.env.NODE_ENV !== 'production') {// 針對情況(1)if (text === template) {warnOnce('Component template requires a root element, rather than just text.')// 針對情況(2)} else if ((text = text.trim())) {warnOnce(`text "${text}" outside root element will be ignored.`)}}return}// 獲取到父元素的 children 屬性const children = currentParent.childrentext = inPre || text.trim()? isTextTag(currentParent) ? text : decodeHTMLCached(text)// only preserve whitespace if its not right after a starting tag: preserveWhitespace && children.length ? ' ' : ''if (text) {let expression// 調用 parseText 對 text 進行解析。解析插值、過濾器等等特性 <span>{{name | nameFilter}}</span>if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {// 將當前文本的 AST 節點 push 到 children 數組中children.push({type: 2,expression,text})} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {// 處理 text 是純文本的情況children.push({type: 3,text})}} },

    首先第一段判斷文本的使用是否規范,不能出現模板字符串全是文本或者文本在根元素外面的情況,如果出現這兩種錯誤的話,則會在開發環境下打印出警告。

    然后嘗試對文本進行解析,如果解析成功的話,說明文本不是純文本,例如 "名字:{{name}}",此時會創建 type 為 2 的文本 AST 節點,并將該節點 push 到 currentParent.children 數組中。

    如果解析失敗的話,說明是純文本節點,此時會創建 type 為 3 的文本 AST 節點,并將該節點 push 到 currentParent.children 數組中。

    4-4,comment?(text: string) {}

    comment (text: string) {// 注釋 AST 和純文本 AST 很像,唯一的不同是有一個 isComment 屬性,并且屬性值為 truecurrentParent.children.push({type: 3,text,isComment: true}) }

    comment 很簡單,創建注釋對應的 AST 節點,并 push 到?currentParent.children 數組中即可。

    5,總結

    解析器如果看具體細節的話,很復雜,因為解析器需要處理和考慮的東西很多。但是,如果我們拋開這些細節,先看整體流程的話,解析器的工作流程是很清晰的,并沒有多難,無非就是在 HTML 解析器中不斷地遍歷解析模板字符串,解析的方法是利用正則表達式,解析完成之后,調用對應的回調函數,在回調函數中進行抽象語法樹節點的構建和整個樹結構的維護,一小段模板字符串處理完成后,就將其從模板字符串中截取出來。就這樣,不斷地循環,不斷地解析,不斷地觸發回調函數,直到模板字符串變成空的字符串,解析器的工作也就完成了。

    在這里,說一個小建議,大家可以先寫一個簡單的模板字符串,然后利用 debugger 調試解析器部分的源碼,把相關的源碼走一遍之后,就能夠理解解析器整體的工作流程了。如果想了解 Vue 某個特性是如何解析的話,就在上面簡單的模板字符串上添加上這個特性(例如 v-if),再 debugger 一遍。千萬不要死讀源碼,也不要追求一遍就將所有特性的解析細節都搞清楚,一定要由簡到難,一步一步來。

    好了,解析器就講到這里,接下來講優化器的工作原理。

    總結

    以上是生活随笔為你收集整理的Vue源码阅读(12):解析器的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。