這篇文章主要介紹了Vue 中的compile操做方法,寫的十分的全面細緻,具備必定的參考價值,對此有須要的朋友能夠參考學習下。若有不足之處,歡迎批評指正。css
在 Vue 裏,模板編譯也是很是重要的一部分,裏面也很是複雜,此次探究不會深刻探究每個細節,而是走一個全景概要,來吧,你們和我一塊兒去一探究竟。html
初體驗前端
咱們看了 Vue 的初始化函數就會知道,在最後一步,它進行了 vm.mount 在兩個地方定義過,分別是在 entry-runtime-with-compiler.js(簡稱:eMount) 和 runtime/index.js(簡稱:rMount) 這兩個文件裏,那麼這兩個有什麼區別呢?vue
// entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount // 這個 $mount 其實就是 rMount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
const options = this.$options
if (!options.render) {
...
//歡迎加入前端全棧開發交流圈一塊兒吹水聊天學習交流:864305860
if(template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
...
}
return mount.call(this, el, hydrating)
}//歡迎加入前端全棧開發交流圈一塊兒吹水聊天學習交流:864305860
複製代碼
其實 eMount 最後仍是去調用的 rMount,只不過在 eMount 作了必定的操做,若是你提供了 render 函數,那麼它會直接去調用 rMount,若是沒有,它就會去找你有沒有提供 template,若是你沒有提供 template,它就會用 el 去查詢 dom 生成 template,最後經過編譯返回了一個 render 函數,再去調用 eMount。 從上面能夠看出,最重要的一部分就是 compileToFunctions 這個函數,它最後返回了 render 函數,關於這個函數,它有點複雜,我畫了一張圖來看一看它的關係,可能會有偏差,但願大俠們能夠指出。node
編譯三步走webpack
看一下這個編譯的總體過程,咱們其實能夠發現,最核心的部分就是在這裏傳進去的 baseCompile 作的工做: parse: 第一步,咱們須要將 template 轉換成抽象語法樹(AST)。 optimizer: 第二步,咱們對這個抽象語法樹進行靜態節點的標記,這樣就能夠優化渲染過程。 generateCode: 第三步,根據 AST 生成一個 render 函數字符串。 好了,咱們接下來就一個一個慢慢看。web
解析器面試
在解析器中有一個很是重要的概念 AST,你們能夠去自行了解一下。 在 Vue 中,ASTNode 分幾種不一樣類型,關於 ASTNode 的定義在 flow/compile.js 裏面,請看下圖: 正則表達式
咱們用一個簡單的例子來講明一下:express
<div id="demo">
<h1>Latest Vue.js Commits</h1>
<p>{{1 + 1}}</p>
</div>
複製代碼
咱們想想這段代碼會生成什麼樣的 AST 呢?
咱們這個例子最後生成的大概就是這麼一棵樹,那麼 Vue 是如何去作這樣一些解析的呢?咱們繼續看。 在 parse 函數中,咱們先是定義了很是多的全局屬性以及函數,而後調用了 parseHTML 這麼一個函數,這也是 parse 最核心的函數,這個函數會不斷的解析模板,填充 root,最後把 root(AST) 返回回去。
parseHTML
在這個函數中,最重要的是 while 循環中的代碼,而在解析過程當中發揮重要做用的有這麼幾個正則表達式。
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 = /^<!\[/
//歡迎加入前端全棧開發交流圈一塊兒吹水聊天學習交流:864305860
複製代碼
Vue 經過上面幾個正則表達式去匹配開始結束標籤、標籤名、屬性等等。 關於 while 的詳細註解我放在我倉庫裏了,有興趣的能夠去看看。 在 while 裏,其實就是不斷的去用 html.indexOf('<') 去匹配,而後根據返回的索引的不一樣去作不一樣的解析處理:
parse 函數就是不斷的重複這個工做,而後將 template 轉換成 AST,在解析過程當中,其實對於標籤與標籤之間的空格,Vue 也作了優化處理,有些元素之間的空格是沒用的。 compile 其實要說要說很是多的篇幅,可是這裏只能簡單的理一下思路,具體代碼還須要各位下去深扣。
優化器
從代碼中的註釋咱們能夠看出,優化器的目的就是去找出 AST 中純靜態的子樹: 把純靜態子樹提高爲常量,每次從新渲染的時候就不須要建立新的節點了 在 patch 的時候就能夠跳過它們 optimize 的代碼量沒有 parse 那麼多,咱們來看看:
export function optimize (root: ?ASTElement, options: CompilerOptions) {
// 判斷 root 是否存在
if (!root) return
// 判斷是不是靜態的屬性
// 'type,tag,attrsList,attrsMap,plain,parent,children,attrs'
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 判斷是不是平臺保留的標籤,html 或者 svg 的
isPlatformReservedTag = options.isReservedTag || no
// 第一遍遍歷: 給全部靜態節點打上是不是靜態節點的標記
markStatic(root)
// 第二遍遍歷:標記全部靜態根節點
markStaticRoots(root, false)
}//歡迎加入前端全棧開發交流圈一塊兒吹水聊天學習交流:864305860
複製代碼
下面兩段代碼我都剪切了一部分,由於有點多,這裏就不貼太多代碼了,詳情請參考個人倉庫。
第一遍遍歷
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
...
}
}
複製代碼
其實 markStatic 就是一個遞歸的過程,不斷地去檢查 AST 上的節點,而後打上標記。 剛剛咱們說過,AST 節點分三種,在 isStatic 這個函數中咱們對不一樣類型的節點作了判斷:
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))//歡迎加入前端全棧開發交流圈一塊兒吹水聊天學習交流:864305860
}
複製代碼
能夠看到 Vue 對下面幾種狀況作了處理:
當這個節點的 type 爲 2,也就是表達式節點的時候,很明顯它不是一個靜態節點,因此返回 false 當 type 爲 3 的時候,也就是文本節點,那它就是一個靜態節點,返回 true 若是你在元素節點中使用了 v-pre 或者使用了
標籤,就會在這個節點上加上 pre 爲 true,那麼這就是個靜態節點
若是它是靜態節點,那麼須要它不能有動態的綁定、不能有 v-if、v-for、v-else 這些指令,不能是 slot 或者 component 標籤、不是咱們自定義的標籤、沒有父節點或者元素的父節點不能是帶 v-for 的 template、 這個節點的屬性都在 type,tag,attrsList,attrsMap,plain,parent,children,attrs 裏面,知足這些條件,就認爲它是靜態的節點。
接下來,就開始對 AST 進行遞歸操做,標記靜態的節點,至於裏面作了哪些操做,能夠到上面那個倉庫裏去看,這裏就不展開了。
第二遍遍歷
第二遍遍歷的過程是標記靜態根節點,那麼咱們對靜態根節點的定義是什麼,首先根節點的意思就是他不能是葉子節點,起碼要有子節點,而且它是靜態的。在這裏 Vue 作了一個說明,若是一個靜態節點它只擁有一個子節點而且這個子節點是文本節點,那麼就不作靜態處理,它的成本大於收益,不如直接渲染。
一樣的,咱們在函數中不斷的遞歸進行標記,最後在全部靜態根節點上加上 staticRoot 的標記,關於這段代碼也能夠去上面的倉庫看一看。
代碼生成器
在這個函數中,咱們將 AST 轉換成爲 render 函數字符串,代碼量仍是挺多的,咱們能夠來看一看。
能夠看到在最後代碼生成階段,最重要的函數就是 genElement 這個函數,針對不一樣的指令、屬性,咱們會選擇不一樣的代碼生成函數。最後咱們按照 AST 生成拼接成一個字符串,以下所示:
在 render 這個函數字符串中,咱們會看到一些函數,那麼這些函數是在什麼地方定義的呢?咱們能夠在 core/instance/index.js 這個文件中找到這些函數:
在編譯結束後,咱們根據不一樣的指令、屬性等等去選擇須要調用哪個處理函數,最後拼接成一個函數字符串。
咱們能夠很清楚的看到,最後生成了一個 render 渲染字符串,那麼咱們要如何去使用它呢?其實在後面進行渲染的時候,咱們進行了 new Function(render) 的操做,而後咱們就可以正常的使用 render 函數了。
結語
感謝您的觀看,若有不足之處,歡迎批評指正。
本次給你們推薦一個免費的學習羣,裏面歸納移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。 對web開發技術感興趣的同窗,歡迎加入Q羣:864305860,無論你是小白仍是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時天天更新視頻資料。 最後,祝你們早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峯。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 這就是編譯的一些參數
const state = new CodegenState(options)
// 生成 render 字符串
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return $[code]}`,
staticRenderFns: state.staticRenderFns
}//歡迎加入前端全棧開發交流圈一塊兒吹水聊天學習交流:864305860
}
複製代碼with(this){return _c('div',{attrs:{"id":"demo"}},[(1>0)?_c('h1',[_v("Latest Vue.js Commits")]):_e(),...}
複製代碼// v-once
target._o = markOnce
// 轉換
target._n = toNumber
target._s = toString
// v-for
target._l = renderList
// slot
target._t = renderSlot
// 是否相等
target._q = looseEqual
// 檢測數組裏是否有相等的值
target._i = looseIndexOf
// 渲染靜態樹
target._m = renderStatic
// 過濾器處理
target._f = resolveFilter
// 檢查關鍵字
target._k = checkKeyCodes
// v-bind
target._b = bindObjectProps
// 建立文本節點
target._v = createTextVNode
// 建立空節點
target._e = createEmptyVNode
// 處理 scopeslot
target._u = resolveScopedSlots
// 處理事件綁定
target._g = bindObjectListeners
// 建立 VNode 節點
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
//歡迎加入前端全棧開發交流圈一塊兒吹水聊天學習交流:864305860
複製代碼