Vue 中的compile操作方法
在 Vue 里,模板編譯也是非常重要的一部分,里面也非常復(fù)雜,這次探究不會深入探究每一個細(xì)節(jié),而是走一個全景概要,來吧,大家和我一起去一探究竟。
初體驗
我們看了 Vue 的初始化函數(shù)就會知道,在最后一步,它進行了 vm.$mount(el)
的操作,而這個 $mount 在兩個地方定義過,分別是在 entry-runtime-with-compiler.js
(簡稱:eMount) 和 runtime/index.js(簡稱:rMount) 這兩個文件里,那么這兩個有什么區(qū)別呢?
// 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) { ... 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) }
其實 eMount 最后還是去調(diào)用的 rMount,只不過在 eMount 做了一定的操作,如果你提供了 render 函數(shù),那么它會直接去調(diào)用 rMount,如果沒有,它就會去找你有沒有提供 template,如果你沒有提供 template,它就會用 el 去查詢 dom 生成 template,最后通過編譯返回了一個 render 函數(shù),再去調(diào)用 eMount。
從上面可以看出,最重要的一部分就是 compileToFunctions 這個函數(shù),它最后返回了 render 函數(shù),關(guān)于這個函數(shù),它有點復(fù)雜,我畫了一張圖來看一看它的關(guān)系,可能會有誤差,希望大俠們可以指出。
編譯三步走
看一下這個編譯的整體過程,我們其實可以發(fā)現(xiàn),最核心的部分就是在這里傳進去的 baseCompile 做的工作:
- parse: 第一步,我們需要將 template 轉(zhuǎn)換成抽象語法樹(AST)。
- optimizer: 第二步,我們對這個抽象語法樹進行靜態(tài)節(jié)點的標(biāo)記,這樣就可以優(yōu)化渲染過程。
- generateCode: 第三步,根據(jù) AST 生成一個 render 函數(shù)字符串。
好了,我們接下來就一個一個慢慢看。
解析器
在解析器中有一個非常重要的概念 AST,大家可以去自行了解一下。
在 Vue 中,ASTNode 分幾種不同類型,關(guān)于 ASTNode 的定義在 flow/compile.js 里面,請看下圖:
我們用一個簡單的例子來說明一下:
<div id="demo"> <h1>Latest Vue.js Commits</h1> <p>{{1 + 1}}</p> </div>
我們想一想這段代碼會生成什么樣的 AST 呢?
我們這個例子最后生成的大概就是這么一棵樹,那么 Vue 是如何去做這樣一些解析的呢?我們繼續(xù)看。
在 parse 函數(shù)中,我們先是定義了非常多的全局屬性以及函數(shù),然后調(diào)用了 parseHTML 這么一個函數(shù),這也是 parse 最核心的函數(shù),這個函數(shù)會不斷的解析模板,填充 root,最后把 root(AST) 返回回去。
parseHTML
在這個函數(shù)中,最重要的是 while 循環(huán)中的代碼,而在解析過程中發(fā)揮重要作用的有這么幾個正則表達式。
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 = /^<!\[/
Vue 通過上面幾個正則表達式去匹配開始結(jié)束標(biāo)簽、標(biāo)簽名、屬性等等。
關(guān)于 while 的詳細(xì)注解我放在我倉庫里了,有興趣的可以去看看。
在 while 里,其實就是不斷的去用 html.indexOf('<')
去匹配,然后根據(jù)返回的索引的不同去做不同的解析處理:
- __等于 0:__這就代表這是注釋、條件注釋、doctype、開始標(biāo)簽、結(jié)束標(biāo)簽中的某一種
- __大于等于 0:__這就說明是文本、表達式
- __小于 0:__表示 html 標(biāo)簽解析完了,可能會剩下一些文本、表達式
parse 函數(shù)就是不斷的重復(fù)這個工作,然后將 template 轉(zhuǎn)換成 AST,在解析過程中,其實對于標(biāo)簽與標(biāo)簽之間的空格,Vue 也做了優(yōu)化處理,有些元素之間的空格是沒用的。
compile 其實要說要說非常多的篇幅,但是這里只能簡單的理一下思路,具體代碼還需要各位下去深扣。
優(yōu)化器
從代碼中的注釋我們可以看出,優(yōu)化器的目的就是去找出 AST 中純靜態(tài)的子樹:
把純靜態(tài)子樹提升為常量,每次重新渲染的時候就不需要創(chuàng)建新的節(jié)點了
在 patch 的時候就可以跳過它們
optimize 的代碼量沒有 parse 那么多,我們來看看:
export function optimize (root: ?ASTElement, options: CompilerOptions) { // 判斷 root 是否存在 if (!root) return // 判斷是否是靜態(tài)的屬性 // 'type,tag,attrsList,attrsMap,plain,parent,children,attrs' isStaticKey = genStaticKeysCached(options.staticKeys || '') // 判斷是否是平臺保留的標(biāo)簽,html 或者 svg 的 isPlatformReservedTag = options.isReservedTag || no // 第一遍遍歷: 給所有靜態(tài)節(jié)點打上是否是靜態(tài)節(jié)點的標(biāo)記 markStatic(root) // 第二遍遍歷:標(biāo)記所有靜態(tài)根節(jié)點 markStaticRoots(root, false) }
下面兩段代碼我都剪切了一部分,因為有點多,這里就不貼太多代碼了,詳情請參考我的倉庫。
第一遍遍歷
function markStatic (node: ASTNode) { node.static = isStatic(node) if (node.type === 1) { ... } }
其實 markStatic 就是一個遞歸的過程,不斷地去檢查 AST 上的節(jié)點,然后打上標(biāo)記。
剛剛我們說過,AST 節(jié)點分三種,在 isStatic 這個函數(shù)中我們對不同類型的節(jié)點做了判斷:
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) )) }
可以看到 Vue 對下面幾種情況做了處理:
當(dāng)這個節(jié)點的 type 為 2,也就是表達式節(jié)點的時候,很明顯它不是一個靜態(tài)節(jié)點,所以返回 false
當(dāng) type 為 3 的時候,也就是文本節(jié)點,那它就是一個靜態(tài)節(jié)點,返回 true
如果你在元素節(jié)點中使用了 v-pre 或者使用了 <pre>
標(biāo)簽,就會在這個節(jié)點上加上 pre 為 true,那么這就是個靜態(tài)節(jié)點
如果它是靜態(tài)節(jié)點,那么需要它不能有動態(tài)的綁定、不能有 v-if、v-for、v-else 這些指令,不能是 slot 或者 component 標(biāo)簽、不是我們自定義的標(biāo)簽、沒有父節(jié)點或者元素的父節(jié)點不能是帶 v-for 的 template、 這個節(jié)點的屬性都在 type,tag,attrsList,attrsMap,plain,parent,children,attrs 里面,滿足這些條件,就認(rèn)為它是靜態(tài)的節(jié)點。
接下來,就開始對 AST 進行遞歸操作,標(biāo)記靜態(tài)的節(jié)點,至于里面做了哪些操作,可以到上面那個倉庫里去看,這里就不展開了。
第二遍遍歷
第二遍遍歷的過程是標(biāo)記靜態(tài)根節(jié)點,那么我們對靜態(tài)根節(jié)點的定義是什么,首先根節(jié)點的意思就是他不能是葉子節(jié)點,起碼要有子節(jié)點,并且它是靜態(tài)的。在這里 Vue 做了一個說明,如果一個靜態(tài)節(jié)點它只擁有一個子節(jié)點并且這個子節(jié)點是文本節(jié)點,那么就不做靜態(tài)處理,它的成本大于收益,不如直接渲染。
同樣的,我們在函數(shù)中不斷的遞歸進行標(biāo)記,最后在所有靜態(tài)根節(jié)點上加上 staticRoot 的標(biāo)記,關(guān)于這段代碼也可以去上面的倉庫看一看。
代碼生成器
在這個函數(shù)中,我們將 AST 轉(zhuǎn)換成為 render 函數(shù)字符串,代碼量還是挺多的,我們可以來看一看。
export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { // 這就是編譯的一些參數(shù) const state = new CodegenState(options) // 生成 render 字符串 const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(this){return $[code]}`, staticRenderFns: state.staticRenderFns } }
可以看到在最后代碼生成階段,最重要的函數(shù)就是 genElement 這個函數(shù),針對不同的指令、屬性,我們會選擇不同的代碼生成函數(shù)。最后我們按照 AST 生成拼接成一個字符串,如下所示:
with(this){return _c('div',{attrs:{"id":"demo"}},[(1>0)?_c('h1',[_v("Latest Vue.js Commits")]):_e(),...}
在 render 這個函數(shù)字符串中,我們會看到一些函數(shù),那么這些函數(shù)是在什么地方定義的呢?我們可以在 core/instance/index.js 這個文件中找到這些函數(shù):
// v-once target._o = markOnce // 轉(zhuǎn)換 target._n = toNumber target._s = toString // v-for target._l = renderList // slot target._t = renderSlot // 是否相等 target._q = looseEqual // 檢測數(shù)組里是否有相等的值 target._i = looseIndexOf // 渲染靜態(tài)樹 target._m = renderStatic // 過濾器處理 target._f = resolveFilter // 檢查關(guān)鍵字 target._k = checkKeyCodes // v-bind target._b = bindObjectProps // 創(chuàng)建文本節(jié)點 target._v = createTextVNode // 創(chuàng)建空節(jié)點 target._e = createEmptyVNode // 處理 scopeslot target._u = resolveScopedSlots // 處理事件綁定 target._g = bindObjectListeners // 創(chuàng)建 VNode 節(jié)點 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
在編譯結(jié)束后,我們根據(jù)不同的指令、屬性等等去選擇需要調(diào)用哪一個處理函數(shù),最后拼接成一個函數(shù)字符串。
我們可以很清楚的看到,最后生成了一個 render 渲染字符串,那么我們要如何去使用它呢?其實在后面進行渲染的時候,我們進行了 new Function(render)
的操作,然后我們就能夠正常的使用 render 函數(shù)了。
總結(jié)
大流程走完之后,我相信大家會對編譯過程有一個比較清晰的認(rèn)識,然后再去挖細(xì)節(jié)相信也會容易的多了,讀源碼,其實并不是一個為了讀而讀的過程,我們可以在源碼中學(xué)到很多我們可能在日常開發(fā)中沒有了解到的知識。
至于最后代碼生成器中的那一大段代碼,我還沒有把它注釋好,后面應(yīng)該會將源碼注釋放到倉庫里,不過我也相信大家也能夠順利的去讀懂源碼。
還有一點要提的是在 render 函數(shù)中,Vue 使用了 with 函數(shù),我們平時肯定沒見過,因為官方不推薦我們?nèi)ナ褂?with,我抱著這樣的想法去找了找原因,最后我在知乎上找到了尤大大的回答,這是鏈接,大家可以去了解下。
以上所述是小編給大家介紹的Vue 中的compile,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
Vue中?引入使用?babel-polyfill?兼容低版本瀏覽器的方法
最近在項目中使用 webpack 打包后升級,用戶反饋使用瀏覽器(chrome 45)訪問白屏。經(jīng)過排查發(fā)現(xiàn):由于 chrome 45 無法兼容 ES6 語法導(dǎo)致的,接下來給大家介紹下Vue中?引入使用?babel-polyfill?兼容低版本瀏覽器方法,需要的朋友可以參考下2023-02-02vue axios數(shù)據(jù)請求get、post方法及實例詳解
axios是一個基于Promise,同時支持瀏覽器端和Node.js的HTTP庫,常用于Ajax請求。這篇文章主要介紹了vue axios數(shù)據(jù)請求get、post方法的使用 ,需要的朋友可以參考下2018-09-09vue3 element-plus el-tree自定義圖標(biāo)方式
這篇文章主要介紹了vue3 element-plus el-tree自定義圖標(biāo)方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03vuejs通過filterBy、orderBy實現(xiàn)搜索篩選、降序排序數(shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了vuejs通過filterBy、orderBy實現(xiàn)搜索篩選、降序排序數(shù)據(jù)實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-02-02利用Vue3+Element?Plus封裝公共表格組件(帶源碼)
最近公司項目中頻繁會使用到table表格,而且前端技術(shù)這一塊也用到了vue3來開發(fā),所以基于element plus table做了一個二次封裝的組件,這篇文章主要給大家介紹了關(guān)于利用Vue3+Element?Plus封裝公共表格組件的相關(guān)資料,需要的朋友可以參考下2023-11-11Vue.js結(jié)合Ueditor富文本編輯器的實例代碼
本篇文章主要介紹了Vue.js結(jié)合Ueditor的項目實例代碼,這里整理了詳細(xì)的代碼,具有一定的參考價值,有興趣的可以了解一下2017-07-07vue 導(dǎo)航守衛(wèi)和axios攔截器有哪些區(qū)別
這篇文章主要介紹了vue 導(dǎo)航守衛(wèi)和axios攔截器有哪些區(qū)別,幫助大家更好的理解和使用vue,感興趣的朋友可以了解下2020-12-12最適應(yīng)的vue.js的form提交涉及多種插件【推薦】
這篇文章主要介紹了最適應(yīng)的vue.js的form提交涉及多種插件,涉及到 vue.js動態(tài)添加css樣式 ,tab切換 ,touch,表單提交,驗證,toast,數(shù)據(jù)雙向綁定等。需要的朋友可以參考下2018-08-08