<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
vue template模板編譯的過程經過parse()生成ast(抽象語法樹),optimize對靜態節點優化,generate()生成render字串
之後呼叫new Watcher()函數,用來監聽資料的變化,render 函數就是資料監聽的回撥所呼叫的,其結果便是重新生成 vnode。
當這個 render 函數位符串在第一次 mount、或者繫結的資料更新的時候,都會被呼叫,生成 Vnode。
如果是資料的更新,那麼 Vnode 會與資料改變之前的 Vnode 做 diff,對內容做改動之後,就會更新到 我們真正的 DOM
在瞭解 parse 的過程之前,我們需要了解 AST,AST 的全稱是 Abstract Syntax Tree,也就是所謂抽象語法樹,用來表示程式碼的資料結構。
在Vue中我把它理解為巢狀的、攜帶標籤名、屬性和父子關係的 JS 物件,以樹來表現 DOM 結構。
vue中的ast型別有以下3種
ASTElement = { // AST標籤元素 type: 1; tag: string; attrsList: Array<{ name: string; value: any }>; attrsMap: { [key: string]: any }; parent: ASTElement | void; children: Array<ASTNode> ... } ASTExpression = { // AST表示式 {{ }} type: 2; expression: string; text: string; tokens: Array<string | Object>; static?: boolean; }; ASTText = { // AST文字 type: 3; text: string; static?: boolean; isComment?: boolean; };
通過children欄位來形成一種層層巢狀的樹狀結構。vue中定義了許多正則(判斷標籤開始、結束、屬性、vue指令、文字),通過對html內容進行遞迴正則匹配,對滿足條件的字串進行擷取。把字串型別的html轉換位AST結構
parse函數的作用就是把字串型的template轉化為AST結構
如,假設我們有一個元素
texttext,在 parse 完之後會變成如下的結構並返回:
ele1 = { type: 1, tag: "div", attrsList: [{name: "id", value: "test"}], attrsMap: {id: "test"}, parent: undefined, children: [{ type: 3, text: 'texttext' } ], plain: true, attrs: [{name: "id", value: "'test'"}] }
那麼它具體是怎麼解析、擷取的呢?
舉個例子
<div> <p>我是{{name}}</p> </div>
他的擷取過程,主要如下
// 初始 <div> <p>我是{{name}}</p> </div> // 第一次擷取剩餘(包括空格) <p>我是{{name}}</p> </div> // 第二次擷取 <p>我是{{name}}</p> </div> // 第三次擷取 我是{{name}}</p> </div> // 第四次擷取 </p> </div> // </div> // </div>
那麼,他的擷取規則是什麼呢?
vue中擷取規則主要是通過判斷模板中html.indexof(’<’)的值,來確定我們是要擷取標籤還是文字.
若等於0
若等於0,則進行正則匹配看是否為開始標籤、結束標籤、註釋、條件註釋、doctype中的一種。若是開始標籤,則擷取對應的開始標籤,並定義ast的基本結構,並且解析標籤上帶的屬性(attrs, tagName)、指令等等。
當然,這裡的attrs也是通過正則匹配出來的,具體做法就是通過匹配標籤上對應的屬性,然後把他push到attrs裡。
匹配時候的正規表示式如下。
const attribute = /^s*([^s"'<>/=]+)(?:s*(=)s*(?:"([^"]*)"+|'([^']*)'+|([^s"'=<>`]+)))?/ const ncname = '[a-zA-Z_][\w\-\.]*' const qnameCapture = `((?:${ncname}\:)?${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) const startTagClose = /^s*(/?)>/ const endTag = new RegExp(`^<\/${qnameCapture}[^>]*>`) const doctype = /^<!DOCTYPE [^>]+>/i const comment = /^<!--/ const conditionalComment = /^<![/
關於stack
stack裡的最後一項,永遠是當前正在解析的元素的parentNode。
通過stack解析器會把當前解析的元素和stack裡的最後一個元素建立父子關係。即把當前節點push到stack的最後一個節點的children裡,同時將它自身的parent設為stack的最後一個節點。
當然,因為我們的標籤中存在一種自閉和的標籤(如input),這種型別的標籤沒有子元素,所以不會push到stack中。
等於0或大於0
若等於0且不滿足以上五種條件或大於0,則表示它是文字或表示式。
let textEnd = html.indexOf('<') let text, rest, next if (textEnd >= 0) { rest = html.slice(textEnd) // 剩餘部分的 HTML 不符合標籤的格式那肯定就是文字 // 並且還是以 < 開頭的文字 while ( !endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest) ) { // < in plain text, be forgiving and treat it as text next = rest.indexOf('<', 1) if (next < 0) break textEnd += next rest = html.slice(textEnd) } text = html.substring(0, textEnd) html = html.substring(0, textEnd) }
關於文字的擷取
文字一般分為2種
如果文字中含有表示式,則需要對文字中的變數進行解析
const expression = parseText(text, delimiters) // 對變數解析 {{name}} => _s(name) children.push({ type: 2, expression, text }) // 上例中解析過後形成如下的結構 { expression: "_s(name)", text: "我是{{name}}", type: 2 }
現在我們再來看最開始的例子
<div> <p>我是{{name}}</p> </div>
1.首先第一次判斷<的位置,等於0,且可以匹配上開始標籤,則擷取這個標籤。
// 第一次擷取後剩餘 <p>我是{{name}}</p> </div>
2.繼續判斷<的位置,大於0(因為有空格),判斷為文字,擷取這個文字
// 第二次擷取後剩餘 <p>我是{{name}}</p> </div>
3.繼續判斷<位置,等於0,且為開始標籤,擷取這一部分,並且維護stack,把當前的解析的元素的parnet置為stack中的最後一項,並且在stack的最後一項的children裡push當前解析的元素
// 這裡有個判斷,因為非自閉和標籤才會有children,所以非自閉標籤才往stack裡push if (!unary) { currentParent = element stack.push(element) } // 設立父子關係 currentParent.children.push(element) element.parent = currentParent // 此時stack [divAst,pAst] // 第三次擷取後剩餘 我是{{name}}</p> </div>
4.繼續判斷<的位置,大於0,判斷剩餘部分是否屬於標籤的一種,這裡剩餘部分可以匹配結束標籤,則表明為文字
// 第四次擷取後剩餘 </p> </div>
5.繼續判斷<的位置,等於0,且匹配為結束標籤,此時會再stack裡尋找滿足tagName和當前標籤名相同的最後一項,把它之後項的全部刪除。
// 此時stack [divAst] // 第五次擷取剩餘 </div>
6.繼續通過以上方式擷取,直到全部擷取完畢。
簡單來說,template的parse過程,其實就是不斷的擷取字串並解析它們的過程。
在此過程中,如果擷取到非閉合標籤就push到stack中,如果擷取道結束標籤就把這個標籤pop出來。
optimize優化
optimize的作用主要是對生成的AST進行靜態內容的優化,標記靜態節點。所謂靜態內容,指的是和資料沒有關係,不需要每次都更新的內容。
標記靜態節點的作用的作用是為了之後dom diff時,是否需要patch,diff演演算法會直接跳過靜態節點,從而減少了比較的過程,優化了patch的效能。
1.沒有任何指令、資料繫結、事件繫結等 &&
2.沒有 v-if 和 v-for &&
3.不是 slot 和 component &&
4.是 HTML 保留標籤 &&
5.不是 template 標籤的直接子元素並且沒有包含在 for 迴圈中則返回 true
簡單來說,沒有使用vue獨有的語法的節點就可以稱為靜態節點
判斷一個父級元素是靜態節點,則需要判斷它的所有子節點都是靜態節點,否則就不是靜態節點
標記靜態節點的過程是一個不斷遞迴的過程
for (let i = 0, l = node.children.length; i < l; i++) { const child = node.children[i] markStatic(child) if (!child.static) { node.static = false } }
markStatic方法是用來標記靜態節點的方法,它會不斷的迴圈children,如果children還有children,則走相同的邏輯。這樣所有的節點都會被打上標記。
在迴圈中會判斷,子節點是否為靜態節點,如果不是則其父節點不是靜態節點。
generate是將AST轉化成render funtion字串的過程,他遞迴了AST,得到結果是render的字串。
render函數的就是返回一個_c(‘tagName’,data,children)的方法
1.第一個引數是標籤名
2.第二個引數是他的一些資料,包括屬性/指令/方法/表示式等等。
3.第三個引數是當前標籤的子標籤,同樣的,每一個子標籤的格式也是_c(‘tagName’,data,children)。
generate就是通過不斷遞迴形成了這麼一種樹形結構。
genElement
:用來生成基本的render結構或者叫createElement結構genData
: 處理ast結構上的一些屬性,用來生成datagenChildren
:處理ast的children,並在內部呼叫genElement,形成子元素的_c()方法render字串內部有幾種方法
幾種內部方法
_c
:對應的是 createElement 方法,顧名思義,它的含義是建立一個元素(Vnode)_v
:建立一個文字結點。_s
:把一個值轉換為字串。(eg: {{data}})_m
:渲染靜態內容<template> <div id="app"> {{val}} <img src="http://xx.jpg"> </div> </template> { render: with(this) { return _c('div', { attrs: { "id": "app" } }, [_v("n" + _s(val) + "n"), _c('img', { attrs: { "src": "" } }) ] ) } }
那麼問題來了,_c(‘tagName’,data,children)如何拼接的,data是如何拼接的,children又是如何拼接的?
// genElement方法用來拼接每一項_c('tagName',data,children) function genElement (el: ASTElement, state: CodegenState) { const data = el.plain ? undefined : genData(el, state) const children = el.inlineTemplate ? null : genChildren(el, state, true) let code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` return code }
線來看data的拼接邏輯
// function genData (el: ASTElement, state: CodegenState): string { let data = '{' // key if (el.key) { data += `key:${el.key},` } // ref if (el.ref) { data += `ref:${el.ref},` } if (el.refInFor) { data += `refInFor:true,` } // ... 類似的還有很多種情況 data = data.replace(/,$/, '') + '}' return data }
從上面可以看出來,data的拼接過程就是不斷的判讀ast上一些屬性是否存在,然後拼在data上,最後把這個data返回。
那麼children怎麼拼出來呢?
function genChildren ( el: ASTElement, state: CodegenState ): string | void { const children = el.children if (children.length) { return `[${children.map(c => genNode(c, state)).join(',')}]` } } function genNode (node: ASTNode, state: CodegenState): string { if (node.type === 1) { return genElement(node, state) } if (node.type === 3 && node.isComment) { return genComment(node) } else { return genText(node) } }
最後執行render函數就會形成虛擬DOM.
以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45