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

歡迎訪問 生活随笔!

生活随笔

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

vue

Vue parse之 从template到astElement 源码详解

發布時間:2023/12/20 vue 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Vue parse之 从template到astElement 源码详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前奏

在緊張的一個星期的整理,筆者的前端小組每個人都整理了一篇文章,筆者整理了Vue編譯模版到虛擬樹的思想這一篇幅。建議讀者看到這篇之前,先點擊這里預習一下整個流程的思想和思路。

本文介紹的是Vue編譯中的parse部分的源碼分析,也就是從template 到 astElemnt的解析到程。

正文

從筆者的 Vue編譯思想詳解一文中,我們已經知道編譯個四個流程分別為parse、optimize、code generate、render。具體細節這里不做贅述,附上之前的一張圖。

本文則旨在從思想落實到源代碼分析,當然只是針對parse這一部分的。

一、 源碼結構。

筆者先列出我們在看源碼之前,需要先預習的一些概念和準備。

準備

1.正則

parse的最終目標是生成具有眾多舒心的astElement,而這些屬性有很多則摘自標簽的一些屬性。 如 div上的v-for、v-if、v-bind等等,最終都會變成astElement的節點屬性。 這里先給個例子:

<div v-for="(item,index) in options" :key="item.id"></div> 到

{alias: "item"attrsList: [],attrsMap: {"v-for": "(item,index) in options", :key: "item.id"},children: (2) [{…}, {…}],end: 139,for: "options",iterator1: "index",key: "item.id",parent: {type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …},plain: false,rawAttrsMap: {v-for: {…}, :key: {…}},start: 15,tag: "div",type: 1, } 復制代碼

可以看到v-for的屬性已經被解析和從摘除出來,存在于astElement的多個屬性上面了。而摘除的這個功能就是出自于正則強大的力量。下面先列出一些重要的正則預熱。

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 重要1 const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 重要二 const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*` const qnameCapture = `((?:${ncname}\\:)?${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) const startTagClose = /^\s*(\/?)>/ const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) const doctype = /^<!DOCTYPE [^>]+>/i // #7298: escape - to avoid being pased as HTML comment when inlined in page const comment = /^<!\--/ const conditionalComment = /^<!\[/export const onRE = /^@|^v-on:/ export const dirRE = process.env.VBIND_PROP_SHORTHAND? /^v-|^@|^:|^\./: /^v-|^@|^:/ export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const stripParensRE = /^\(|\)$/g // 在v-for中去除 括號用的。 const dynamicArgRE = /^\[.*\]$/ // 判斷是否為動態屬性const argRE = /:(.*)$/ // 配置 :xxx export const bindRE = /^:|^\.|^v-bind:/ // 匹配bind的數據,如果在組件上會放入prop里面 否則放在attr里面。 const propBindRE = /^\./ const modifierRE = /\.[^.\]]+(?=[^\]]*$)/gconst slotRE = /^v-slot(:|$)|^#/const lineBreakRE = /[\r\n]/ const whitespaceRE = /\s+/gconst invalidAttributeRE = /[\s"'<>\/=]/ 復制代碼

正則基礎不太好的同學可以先學兩篇正則基礎文章,特別詳細:

  • 輕松入門正則表達式
  • 正則一條龍

并且附帶上兩個網站,供大家學習正則。

  • 正則測試
  • 正則圖解

一次性看到這么多正則是不是有點頭暈目眩。不要慌,這里給大家詳細講解下比較復雜多正則。

1)獲取屬性多正則

attribute 和 dynamicArgAttribute 分別獲取普通屬性和動態屬性的正則表達式。 普通屬性大家一定十分屬性了,這里對動態屬性做下解釋。

動態屬性,就是key值可能會發生變動對屬性,vue對寫法如 v-bind:[attrName]="attrVal" 相當于控制你想要傳遞對參數。

我們先對attribute做一個詳細對講解:

const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"])"+|'([^'])'+|([^\s"'=<>`]+)))?/ 一共分為五個分組:

  • 1.([^\s"'<>/=]+) 不匹配 空格、<、>、/、= 等符號。 因為我們配置對是屬性。
  • 2.\s*(=)\s* 這個是 匹配 = 號,當然了空格頁一并匹配了。
  • 3."([^"])" 、'([^'])' 、([^\s"'=<>`]+) . 這三個則分別匹配三種情況 "val" 、'val' 、val。

這樣的話應該比較清晰了,我們來概括下:

attribute匹配的一共是三種情況, name="xxx" name='xxx' name=xxx。能夠保證屬性的所有情況都能包含進來。 需要注意的是正則處理后的數組的格式是:

['name','=','val','',''] 或者 ['name','=','','val',''] 或者 ['name','=','','','val'] 復制代碼

正則的圖:

而關于dynamicArgAttribute, 則是大同小異:

主要是多了\[[^=]+\][^\s"'<>\/=]* 也就是 [name] 或者 [name]key 這類情況,附上正則詳解圖:

2)標簽處理正則

標簽主要包含開始標簽 (如<div>)和結束標簽(如</div>),正則分別為以下兩個:

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*` const qnameCapture = `((?:${ncname}\\:)?${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) const startTagClose = /^\s*(\/?)>/ const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) 復制代碼

能夠看到標簽的匹配是以qnameCapture為基礎的,那么這玩意又是啥呢? 其實qname就是類似于xml:xxx的這類帶冒號的標簽,所以startTagOpen是匹配<div或<xml:xxx的標簽。 endTag匹配的是如</div>或</xml:xxx>的標簽

3)處理vue的標簽
export const onRE = /^@|^v-on:/ 處理綁定事件的正則 export const dirRE = process.env.VBIND_PROP_SHORTHAND? /^v-|^@|^:|^\./ // v- | @click | :name | .stop 指令匹配: /^v-|^@|^:/ 復制代碼

for 標簽比較重要,匹配也稍微復雜點,這里做個詳解:

export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ 復制代碼

首先申明這里的正則是依賴于attribute正則的,我們會拿到v-for里面的內容,舉個例子v-for="item in options",我們最終會處理成一個map的形式,大致如下:

const element = {attrMap: {'v-for':'item in options',...} } 復制代碼

先看forAliasRE的分組,一共兩個分組分別([\s\S]?)和([\s\S]) 會分別匹配 item 和 options。 但是 in或of之前內容可能是比較復雜的,如(value,key) 或者(item,index)等,這個時候就是forIteratorRE開始起作用了。 它一共兩個分組都是([^,}]]*),其實就是拿到alias的最后兩個參數,大家都知道對于Object我們是可以這么做的:

<div v-for="(value,key,index)"> 復制代碼

而數組則是為了獲取key 和index的。最終會放在astElement的iterator1 和 iterator2。

好了關于正則就說這么多了,具體的情況還是得自己去看看源碼的。

2.源碼結構

依然是在開始講源碼前,先大致介紹下源碼的結構。先貼個代碼出來

function parse() {模塊一:初始化需要的方法模塊二: 初始化所有標記模塊三: 開始識別并創建 astElement 樹。 } 復制代碼

模塊一大致是:

platformIsPreTag = options.isPreTag || no //判斷是否為 pre 標簽platformMustUseProp = options.mustUseProp || no // 判斷某個屬性是否是某個標簽的必要屬性,如selected 對于optionplatformGetTagNamespace = options.getTagNamespace || no // 判斷是否為 svg or math標簽 對函數const isReservedTag = options.isReservedTag || no // 判斷是否為該平臺對標簽,目前vue源碼只有 web 和weex兩個平臺。maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) //是否可能為組件transforms = pluckModuleFunction(options.modules, 'transformNode') // 數組,成員是方法, 用途是摘取 staticStyle styleBinding staticClass classBindingpreTransforms = pluckModuleFunction(options.modules, 'preTransformNode') // ??postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') // ??delimiters = options.delimiters // express標志function closeElement() {...} // 處理astElement對結尾函數function trimEndingWhitespace() {...} // 處理尾部空格function checkRootConstraints() {...} // 檢查root標簽對合格性 復制代碼

模塊二大致為:

const stack = [] // 配合使用的棧 主要目的是為了完成樹狀結構。let root // 根節點記錄,樹頂let currentParent // 當前父節點let inVPre = false // 標記是否在v-pre節點 當中let inPre = false // 是否在pre標簽當中let warned = false 復制代碼

模塊三大致為:

parseHTML(template,options) 復制代碼

options 是關鍵,包括很多平臺配置和 傳入的四個處理方法。大致如下:

options = {warn,expectHTML: options.expectHTML, // 是否期望和瀏覽器器保證一致。isUnaryTag: options.isUnaryTag, // 是否為一元標簽的判斷函數canBeLeftOpenTag: options.canBeLeftOpenTag, // 可以直接進行閉合的標簽shouldDecodeNewlines: options.shouldDecodeNewlines,shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,shouldKeepComment: options.comments,outputSourceRange: options.outputSourceRange,// 這里分開,上面是平臺配置、下面是處理函數。start, (1)end, (2)chars, (3)commend (4) } 復制代碼

筆者之前的parse思想,已經介紹過兩個處理函數start和end了,一個是創建astElement另一個是建立父子關系,其中細節會在下文中,詳細介紹,這也是本文的重點。切記這四個函數至關重要,下面會用代號講解。

二、各模塊重點功能。

Vue的html解析并非一步到位,先來介紹一些重點的函數功能

1.parseHTML函數功能。

(1)解析開始標簽和 處理屬性,生成初始化match。代碼如下:

/*** 創建match數據結構* 初始化的狀態* 只有* tagName* attrs* attrs自己是個數組 也就是 正則達到的效果。。* start* end*/function parseStartTag () {const start = html.match(startTagOpen)if (start) {const match = { // 匹配startTag的數據結構tagName: start[1],attrs: [],start: index}advance(start[0].length)let end, attr// 取屬性值while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {attr.start = indexadvance(attr[0].length)attr.end = indexmatch.attrs.push(attr)}if (end) {match.unarySlash = end[1] // 是否為 一元標記 直接閉合advance(end[0].length)match.end = indexreturn match}}} 復制代碼

parseStartTag的目標是比較原始的,獲得類似于

const match = { // 匹配startTag的數據結構tagName: 'div',attrs: [{ 'id="xxx"','id','=','xxx' },...],start: index,end: xxx} 復制代碼

match大致可以概括為獲取標簽、屬性和位置信息。并將此傳遞給下個函數。

(2)handleStartTag處理parseStartTag傳遞過來的match。

// parseStartTag 拿到的是 matchfunction handleStartTag (match) {const tagName = match.tagNameconst unarySlash = match.unarySlashif (expectHTML) { // 是否期望和瀏覽器的解析保持一致。if (lastTag === 'p' && isNonPhrasingTag(tagName)) {parseEndTag(lastTag)}if (canBeLeftOpenTag(tagName) && lastTag === tagName) {parseEndTag(tagName)}}const unary = isUnaryTag(tagName) || !!unarySlash // 一元判斷const l = match.attrs.lengthconst attrs = new Array(l)for (let i = 0; i < l; i++) { // 將attrs的 數組模式變成 { name:'xx',value:'xxx' }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 (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {attrs[i].start = args.start + args[0].match(/^\s*/).lengthattrs[i].end = args.end}}if (!unary) { // 非一元標簽處理方式stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })lastTag = tagName}if (options.start) {options.start(tagName, attrs, unary, match.start, match.end)}}復制代碼

handleStartTag的本身效果其實非常簡單直接,就是吧match的attrs重新處理,因為之前是數組結構,在這里他們將所有的數組式attr變成一個對象,流程大致如下:

從這樣:

attrs: [{ 'id="xxx"','id','=','xxx' },... ], 復制代碼

變成這樣:

attrs: [{name='id',value='xxx' },... ], 復制代碼

那么其實還有些特殊處理expectHTML 和 一元標簽。

expectHTML 是為了處理一些異常情況。如 p標簽的內部出現div等等、瀏覽器會特殊處理的情況,而Vue會盡量和瀏覽器保持一致。具體參考 p標簽標準。

最后handleStartTag會調用 從parse傳遞的start(1)函數來做處理,start函數會在下文中有詳細的講解。

(3) parseEndTag

parseEndTag本身的功能特別簡單就是直接調用options傳遞進來的end函數,但是我們觀看源碼的時候會發現源碼還蠻長的。

function parseEndTag (tagName, start, end) {let pos, lowerCasedTagNameif (start == null) start = indexif (end == null) end = index// Find the closest opened tag of the same typeif (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 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.`,{ start: stack[i].start, end: stack[i].end })}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)}}} }復制代碼

看起來還蠻長的,其實主要都是去執行options.end, Vue的源碼有很多的代碼量都是在處理特殊情況,所以看起來很臃腫。這個函數的特殊情況主要有兩種:

  • 1.編寫者失誤,有標簽沒有閉合。會直接一次性和檢測的閉合標簽一起進入options.end。 如:
<div><span><p></div> 復制代碼

在處理div的標簽時,根據pos的位置,將pos之前的所有標簽和匹配到的標簽都會一起遍歷的去執行end函數。

  • p標簽和br標簽

可能會遇到</p> 和 </br>標簽 這個時候 p標簽會走跟瀏覽器自動補全效果,先start再end。 而br則是一元標簽,直接進入end效果。

2.start、end、comment、chars四大函數。

1)start函數

start函數非常長。這里截取重點部分

start() {...let element: ASTElement = createASTElement(tag, attrs, currentParent)...if (!inVPre) {processPre(element)if (element.pre) {inVPre = true}}if (platformIsPreTag(element.tag)) {inPre = true}if (inVPre) {processRawAttrs(element)} else if (!element.processed) {// structural directivesprocessFor(element)processIf(element)processOnce(element)}if (!root) {root = elementif (process.env.NODE_ENV !== 'production') {checkRootConstraints(root)}}if (!unary) {currentParent = elementstack.push(element)} else {closeElement(element)} } 復制代碼
  • 1).創建astElement節點。

結構如下:

{type: 1,tag,attrsList: attrs,attrsMap: makeAttrsMap(attrs),rawAttrsMap: {},parent,children: []} 復制代碼
  • 2)處理屬性 當然在這里只是處理部分屬性,且分為兩種情況:

    (1)pre模式 直接摘取所有屬性

    (2)普通模式 分別處理processFor(element) 、processIf(element) 、 processOnce(element)。

2)end函數

end函數非常短

end (tag, start, end) {const element = stack[stack.length - 1]// pop stackstack.length -= 1currentParent = stack[stack.length - 1]if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {element.end = end}closeElement(element)}, 復制代碼

end函數第一件事就是取出當前棧的父元素賦值給currentParent,然后執行closeElement,為的就是能夠創建完整的樹節點關系。 所以closeElement才是end函數的重點。

下面詳細解釋下closeElement

function closeElement (element) {trimEndingWhitespace(element) // 去除 未部對空格元素if (!inVPre && !element.processed) {element = processElement(element, options) // 處理Vue相關對一些屬性關系}// tree managementif (!stack.length && element !== root) {// allow root elements with v-if, v-else-if and v-elseif (root.if && (element.elseif || element.else)) {if (process.env.NODE_ENV !== 'production') {checkRootConstraints(element)}addIfCondition(root, { // 處理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.`,{ start: element.start })}}if (currentParent && !element.forbidden) {if (element.elseif || element.else) { // 處理 elseif else 塊級processIfConditions(element, currentParent)} else {if (element.slotScope) { // 處理slot, 將生成的各個slot的astElement 用對象展示出來。// scoped slot// keep it in the children list so that v-else(-if) conditions can// find it as the prev node.const name = element.slotTarget || '"default"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element}currentParent.children.push(element)element.parent = currentParent}}// final children cleanup// filter out scoped slotselement.children = element.children.filter(c => !(c: any).slotScope)// remove trailing whitespace node againtrimEndingWhitespace(element)// check pre stateif (element.pre) {inVPre = false}if (platformIsPreTag(element.tag)) {inPre = false}// apply post-transformsfor (let i = 0; i < postTransforms.length; i++) {postTransforms[i](element, options)}} 復制代碼

主要是做了五個操作:

  • 1.processElement。

processElement是closeElement非常重要的一個處理函數。先把代碼貼出來。

export function processElement (element: ASTElement,options: CompilerOptions ) {processKey(element)// determine whether this is a plain element after// removing structural attributeselement.plain = (!element.key &&!element.scopedSlots &&!element.attrsList.length)processRef(element)processSlotContent(element)processSlotOutlet(element)processComponent(element)for (let i = 0; i < transforms.length; i++) {element = transforms[i](element, options) || element}processAttrs(element)return element } 復制代碼

可以看到主要是processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs和最后一個遍歷的執行的transforms。

processSlotContent是處理展示在組件內部的slot,但是在這個地方只是簡單的將給el添加兩個屬性作用域插槽的slotScope和 slotTarget,也就是目標slot。 processSlotOutlet,則是簡單的摘取 slot元素上面的name,并賦值給slotName。 processComponent 并不是處理component,而是摘取動態組件的is屬性。 processAttrs是獲取所有的屬性和動態屬性。

transforms是處理class和style的函數數組。這里不做贅述了。

  • 2.添加elseif 或else的block。

最終生成的的ifConditions塊級的格式大致為:

[{exp:'showToast',block: castElement1},{exp:'showOther',block: castElement2},{exp: undefined,block: castElement3} ] 復制代碼

這里會將條件展示處理成一個數組,exp存放所有的展示條件,如果是else 則為undefined。

  • 3.處理slot,將各個slot對號入座到一個對象scopedSlots。

processElement完成的slotTarget的賦值,這里則是將所有的slot創建的astElement以對象的形式賦值給currentParent的scopedSlots。以便后期組件內部實例話的時候可以方便去使用vm.slot的初始化。

  • 4.處理樹到父子關系,element.parent = currentParent。
  • 5.postTransforms

不做具體介紹了,感興趣的同學自己去研究下吧。

3)chars函數

chars(){...const children = currentParent.children...if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {child = {type: 2,expression: res.expression,tokens: res.tokens,text}} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {child = {type: 3,text}} } 復制代碼

chars主要處理兩中文本情況,靜態文本和表達式,舉個例子:

<div>name</div> 復制代碼

name就是靜態文本,創建的type為3.

<div>{{name}}</div> 復制代碼

而在這個里面name則是表達式,創建的節點type為2。

做個總結就是:普通tag的type為1,純文本type為2,表達式type為3。

4)comment函數比較簡單

comment (text: string, start, end) {// adding anyting as a sibling to the root node is forbidden// comments should still be allowed, but ignoredif (currentParent) {const child: ASTText = {type: 3,text,isComment: true}if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {child.start = startchild.end = end}currentParent.children.push(child)}} 復制代碼

也是純文本,只是節點加上了一個isComment:true的標志。

三、整體流程總結。

普通標簽處理流程描述

  • 1.識別開始標簽,生成匹配結構match。
const match = { // 匹配startTag的數據結構tagName: 'div',attrs: [{ 'id="xxx"','id','=','xxx' },...],start: index,end: xxx} 復制代碼
  • 2.處理attrs,將數組處理成 {name:'xxx',value:'xxx'}
  • 3.生成astElement,處理for,if和once的標簽。
  • 4.識別結束標簽,將沒有閉合標簽的元素一起處理。
  • 5.建立父子關系,最后再對astElement做所有跟Vue 屬性相關對處理。slot、component等等。

文本或表達式的處理流程描述。

  • 1、截取符號<之前的字符串,這里一定是所有的匹配規則都沒有匹配上,只可能是文本了。
  • 2、使用chars函數處理該字符串。
  • 3、判斷字符串是否含有delimiters,默認也就是${},有的話創建type為2的節點,否則type為3.

注釋流程描述

  • 1、匹配注釋符號。
  • 2、 使用comment函數處理。
  • 3、直接創建type為3的節點。

完結感言

時間倉促,希望多多支持。

轉載于:https://juejin.im/post/5d01b954f265da1bbf69172e

總結

以上是生活随笔為你收集整理的Vue parse之 从template到astElement 源码详解的全部內容,希望文章能夠幫你解決所遇到的問題。

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