摘要:還原的難度就在于變成模板了,因為其他的什么等是原封不動的哈哈,可是直接照抄最后鑒于本人能力有限,難免會有疏漏錯誤的地方,請大家多多包涵,如果有任何描述不當的地方,歡迎后臺聯系本人,有重謝
寫文章不容易,點個贊唄兄弟
專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧
研究基于 Vue版本 【2.5.17】
如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧
【Vue原理】Compile - 源碼版 之 generate 節點拼接
終于走到了 Vue 渲染三巨頭的最后一步了,那就是 generate,反正文章已經寫完了,干脆早點發完,反正這部分內容大家也不會馬上看哈哈
或者先看白話版好了
Compile - 白話版
然后,generate的作用就是,解析 parse 生成的 ast 節點,拼接成字符串,而這個字符串,是可以被轉化成函數執行的。函數執行后,會生成對應的 Vnode
Vnode 就是Vue 頁面的基礎,我們就可以根據他完成DOM 的創建和更新了
比如這樣
ast { tag:"div", children:[], attrsList:[{ name:111 }] } 拼接成函數 "_c("div", { attrs:{ name:111 } }, [])" 轉成函數 new Function(傳入上面的字符串) 生成 Vnode { tag: "div", data:{ attrs: {name: "111"} }, children: undefined }本文概覽
本文主要講的是如果去把 生成好的 ast 拼接成 函數字符串(跟上面那個轉換一樣),而 ast 分為很多種,而每一種的拼接方式都不一樣,我們會針對每一種方式來詳細列出來
下面將會講這么多種類型節點的拼接
靜態節點,v-for 節點,v-if 節點,slot 節點,組件節點,子節點 等的拼接,內容較多卻不復雜,甚至有點有趣
那我們就來看看 generate 本身的函數源碼先
比較簡短
function generate(ast, options) { var state = new CodegenState(options); var code = ast ? genElement(ast, state) : "_c("div")"; return { render: "with(this){ return " + code + "}", //專門用于存放靜態根節點的 staticRenderFns: state.staticRenderFns } }
對上面出現的幾個可能有點迷惑的東西解釋一下
參數 optionsoptions 是傳入的一些判斷函數或者指令函數,如下,不一一列舉
{ expectHTML: true, modules: modules$1, directives: directives$1 .... };函數 CodegenState
給該實例初始化編譯的狀態,下面會有源碼
函數 genElement把 ast 轉成字符串的 罪魁禍首
generate 返回值你也看到了
1 返回 genElement 拼接后的字符串code這就是作為 render 的主要形態,包了一層 with
render 會有一塊內容專門說,with 就不多說了哈,就是為了為 render 綁定實例為上下文
2 返回 靜態根節點 的 靜態render這是一個 數組,因為一個模板里面可能存在多個靜態根節點,那么就要把這些靜態根節點都轉換成 render 字符串保存起來,就是保存在數組中
上面是靜態根節點?簡單就是說,第一靜態,第二某一部分靜態節點的最大祖宗,如下圖
兩個 span 就是 靜態根節點,他們都是他們那個靜態部分的最大祖宗,而 div 下 有 v-if 的子節點,所以 div 不是靜態根節點
然后下面這個靜態模板,解析得到 render 放到 staticRenderFns 是這樣的
111staticRenderFns=[ ` with(this){ return _c("div", {attrs:{"name":"a"}},[111])] ) } ` ]
而 staticRenderFns 也會在 render 模塊下詳細記錄
CodegenState初始化實例的編譯狀態
function CodegenState(options) { this.options = options; this.dataGenFns = [ klass$1.genData, style$1.genData]; this.directives = { on , bind, cloak, model,text ,html] this.staticRenderFns = []; };
因為這個函數是給實例初始化一些屬性的,看到很明顯就是給實例添加上了很多屬性,this.xxxx 什么的,那么我們就對 CodegenState 這個函數中添加的屬性解釋一下。
屬性 dataGenFns這個數組,存放的是兩個函數
style$1.genData 處理 ast 中的 style ,包括動態靜態的 style
klass$1.genData 處理 ast 中的 class ,包括動態靜態的 class
比如
解析成 ast { tag: "div", type: 1, staticStyle: "{"height":"0"}", styleBinding: "{width:0}", staticClass: ""a"", classBinding: "name" } 解析成字符串 `_c("div",{ staticClass:"a", class:name, staticStyle:{"height":"0"}, style:{width:0} }) ` staticClass:"a", class:name, staticStyle:{"height":"0"}, style:{width:0} }) `
dataGenFns 會在后面拼接節點數據的時候調用到
屬性 directives這也是個數組,存放的是 Vue 自有指令的獨屬處理函數
包括以下幾個指令的處理函數
v-on,綁定事件
v-bind,綁定屬性
v-cloak,編譯前隱藏DOM
v-model,雙向綁定
v-text,插入文本
v-html,插入html
當你在模板中使用到以上的指令的時候,Vue 會調用相應的函數先進行處理
屬性 staticRenderFns一個數組,用來存放靜態根節點的render 函數,上面有提到過一點
每個實例都獨有這個屬性,如果沒有靜態根節點就為空
比如下面這個模板,有兩個靜態根節點
然后在實例 的 staticRenderFns 中就存放兩個 靜態 render
那么我們現在就來看,generate 的重點函數,genElement
genElement這是 ast 拼接成 字符串 的重點函數,主要是處理各種節點,并且拼接起來
靜態節點,v-for 節點,v-if 節點,slot 節點,組件節點,子節點 等,有一些省略了
可以簡單看看下面的源碼
function genElement(el, state) { if ( el.staticRoot && !el.staticProcessed ) { return genStatic(el, state) } else if ( el.for && !el.forProcessed ) { return genFor(el, state) } else if ( el.if && !el.ifProcessed ) { return genIf(el, state) } else if (el.tag === "slot") { return genSlot(el, state) } else { var code; // 處理 is 綁定的組件 if (el.component) { code = genComponent(el.component, el, state); } // 上面所有的解析完之后,會走到這一步 else { // 當 el 不存在屬性的時候,el.plain = true var data = el.plain ? undefined : genData$2(el, state); // 處理完父節點,遍歷處理所有子節點 var children = genChildren(el, state); code = `_c( "${el.tag}" ${data ? ("," + data) : ""} ${children ? ("," + children) : ""} )` } return code } }
重點是其中的各種處理函數,通過各種條件來選擇函數進行處理,并且會有一個 xxxProcessed 屬性,作用是證明是否已經進行過 xxx 方面的處理了,比如forProcessed = true,證明已經拼接過他的 v-for 了
在相應的函數中,會被這個屬性設置為 true,然后遞歸的時候,就不會再調用相應的函數
以上的各種函數中會調用 genElement,以便遞歸處理其他節點
genElement 按順序處理自身各種類型的節點后,開始 genData$2 拼接節點的數據,比如 attr ,prop 那些,然后再使用 genChildren 處理 子節點
拼接節點數據會在獨立一篇文章記錄,內容很多
下面我們來一個個看其中涉及的節點處理函數
拼接靜態節點function genStatic(el, state) { el.staticProcessed = true; state.staticRenderFns.push( "with(this){ return " + genElement(el, state) + "}" ); return ` _m(${ state.staticRenderFns.length - 1 }) ` }
太簡單了,給一個模板看一下就可以了
處理完,存儲靜態render,并返回字符串 "_m(0)" , 很簡短吼
意思就是獲取 staticRenderFns 中的第一個值
其中的值,也是調用 genElement 得到的靜態 render
拼接 vIf 節點專門用于處理帶有 v-if 的節點
function genIf( el, state) { el.ifProcessed = true; // 避免遞歸 return genIfConditions( el.ifConditions.slice(), state ) }
看到 parse 文章的,想必應該知道 el.ifCondition 是什么了吧
簡單說一下吧,el.ifCondition 是用來存放條件節點的數組
什么叫條件節點啊?
比如 你有這樣的模板
像 上面的 p,span,section 三個節點都是條件節點,不會直接存放到父節點的 children 中
因為并不是馬上顯示的
然后他們解析得到的 ast ,都會被存放到 p 節點的 ifCondition 中
像這樣
{ tag:"div", children:[{ tag:"p", ifCondition:[{ exp: "isShow", block: {..p 的 ast 節點} },{ exp: "isShow==2", block: {..span 的 ast 節點} },{ exp: undefined, block: {..section 的 ast 節點} }] }] }
el.ifCondition 就是把 這個數組復制一遍(我又學到了,之前我并不知道可以這么去復制數組)
然后傳給 genIfCondition,看看源碼
function genIfConditions( conditions, state, ) { // 當 沒有條件的時候,就返回 _e ,意思是空節點 if (!conditions.length) { return "_e()" } // 遍歷一遍之后,就把條件剔除 var condition = conditions.shift(); if (condition.exp) { return ( condition.exp + "?" + genElement(condition.block,state) + ":" + genIfConditions(conditions, state ) ) } else { return genElement(condition.block,state) } }
這個函數的作用呢,是這樣的
1、按順序處理 ifCondition 中的每一個節點,并且會移出數組
2、并且每一個節點使用 三元表達式 去拼接
3、遞歸調用 genIfConditions 去處理剩下的 ifCondition
按下面的模板來說明一下流程
第一輪ifCondition = [ p,span,section ]
獲取 ifCondition 第一個節點,也就是p,并移出 ifCondition 數組
此時 ifCondition = [ span,section ]
p 節點有表達式 isShow,需要三元表達式拼接,變成
" isShow ? _c("p") : genIfConditions( 剩下的 ifCondition )"第二輪
genIfConditions 同樣獲取第一個節點,span
此時 ifCondition = [ section ]
span 有表達式 isShow==2,需要拼接三元表達式,變成
" isShow ? _c("p") : ( isShow==2 ? _c( "span") : genIfConditions( 剩下的 ifCondition ) )"第三輪
genIfConditions 同樣獲取第一個節點,section
此時 ifCondition = [ ]
section 沒有表達式,直接處理節點,拼接成
" isShow ? _c("p") : ( isShow==2 ? _c( "span") : _c( "section") )"
然后就處理完啦,上面的字符串,就是 genIf 處理后拼接上的字符串
接下來看是怎么拼接帶有v-for 的指令的
拼接v-for 節點function genFor( el, state ) { var exp = el.for; var alias = el.alias; var iterator1 = el.iterator1 ? ("," + el.iterator1 ) : ""; var iterator2 = el.iterator2 ? ("," + el.iterator2 ) : ""; el.forProcessed = true; // avoid recursion return ( "_l (" + exp + ",function(" + alias + iterator1 + iterator2 + "){" + "return " + genElement(el, state) + "})" ) }
大家應該都可以看得懂的吧,給個例子
`_c("div", _l( arr ,function(item,index){ return _c("span") } )`
就這樣,v-for 就解析成了一個 _l 函數,這個函數會遍歷 arr,遍歷一遍,就生成一個節點
下面在看看是如何處理子節點的
拼接子節點function genChildren(el, state) { var children = el.children; if (!children.length) return return` [$ { children.map(function(c) { if (node.type === 1) { return genElement(node, state) } return`_v($ { text.type === 2 ? text.expression : (""" + text.text + """) })` }).join(",")) }]` }
同樣的,這個函數也是很簡單的吼
就是遍歷所有子節點,逐個處理子節點,然后得到一個新的數組
1、當子節點 type ==1 時,說明是標簽,那么就要 genElement 處理一遍
2、否則,就是文本節點
如果 type =2 ,那么是表達式文本,否則,就是普通文本
普通文本,需要左右兩邊加引號。表達式是個變量,需要在實例上獲取,所以不用加雙引號
舉個例子
解析成字符串
`_c("div",[ _c("span") ,_c("section") ,_c("a") ])`拼接插槽
function genSlot(el, state) { var slotName = el.slotName || ""default""; var children = genChildren(el, state); var res = ` _t( ${slotName} , ${ children ? ("," + children) : ""} ) ` var attrs = el.attrs && "{" + el.attrs.map(function(a) { return camelize(a.name) + ":" + a.value; }).join(",") + "}"; if (attrs && !children) { res += ",null"; } // _t 的參數順序是 slotName, children,attrs,bind if (attrs) { res += "," + attrs; } return res + ")" }
genSlot 主要是處理子節點 和 綁定在 slot 上的 attrs
屬性 attrs 會逐個拼接成 xx:xx 的形式 ,合成一個新的數組,然后通過 逗號隔開成字符串
原 attrs = [ { name:"a-a" ,value:"aaa"}, { name:"b-b" ,value:"bbb"} ] 轉換后,name 會變成駝峰 attrs = "aA:"aaa", bB:"bbb""
看下例子,一個slot,綁定屬性 a 作為 scope,并且有 span 作為默認內容
解析成這樣
_c("div",[_t("default", [_c("span")] ,{a:aa} )] )
然后剩最后一個了,解析組件的節點
拼接組件function genComponent(componentName, el, state) { var children = genChildren(el, state, true); return `_c( ${componentName}, ${genData$2(el, state)} ${children ? ("," + children) : ""} )` }
其實,解析組件,就是把他先當做普通標簽進行處理,在這里并沒有做什么特殊的操作
但是這個方法是用來處理 【帶有 is 屬性】 的節點
否則 就不會存在 el.component 這個屬性,就不會調用 genComponent
拼接成下面這樣,而其中的 is 屬性的拼接在 下篇文章 genData$2 中會有說明
`_c("div",[_c("test",{tag:"a"})])`
那如果直接寫組件名作為標簽,是怎么處理?
也沒有做什么特殊處理,具體看 genElement 最后那段
同樣當做普通標簽先解析
看個例子
解析成這樣的字符串
`_c("div",[ _c("test", [_c("span")] )] )`走個流程
看了上面這么多的處理函數,各種函數處理后得到的字符串是相加的關系
然后現在用一個小例子來實現以下拼接步驟
1、先解析最外層 div,得到字符串
`_c( "div" `
genChildren 開始解析子節點
2、處理 strong,這是一個靜態根節點,genStatic 處理得到字符串
`_c( "div" , [ _m(0) `
3、處理 p 節點,genIf 處理拼接字符串
`_c( "div" , [ _m(0) , isShow? _c(p) :_e() `
4、處理 span 節點, genFor 拼接字符串
`_c( "div" , [ _m(0) , isShow? _c(p) :_e() , _l(arr,function(item,index){return _c("span")}) `
5、處理 test 組件節點,genComponent 拼接
`_c( "div" , [ _m(0) , isShow? _c(p) :_e() , _l(arr,function(item,index){return _c("span")}), _c("test") `
6、genChildren 處理完所有子節點拼接上末尾的括號得到
`_c( "div" , [ _m(0) , isShow? _c(p) :_e() , _l(arr,function(item,index){return _c("span")}), _c("test") ]) `
然后整個genElement 流程就處理完了
上面得到的 字符串,只要轉換成函數,就可以執行了,于是也就可以得到我們的 Vnode
感悟有時你會想,看這個東西有什么用啊,其實你只做正常項目的話,你的確大可不必去看這部分的內容,但是如果你真的想胸有成竹,百分百掌握Vue,你就必須看,因為你可以做更多東西
比如之前接了個外包,要根據別人打包好的文件,去還原別人的源碼!
難度之大之復雜,你也想得出來,不過幸好我看過源碼,打包后的文件,模板全是 render 函數,所以我可以手動還原出來原始模板!
雖然我也可以寫一個 反編譯模板函數,但是工作量太大,沒得想法了。還原的難度就在于 render 變成模板了,因為其他的什么 method 等是原封不動的哈哈,可是直接照抄
最后鑒于本人能力有限,難免會有疏漏錯誤的地方,請大家多多包涵,如果有任何描述不當的地方,歡迎后臺聯系本人,有重謝
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106698.html
摘要:寫文章不容易,點個贊唄兄弟專注源碼分享,文章分為白話版和源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于版本如果你覺得排版難看,請點擊下面鏈接或者拉到下面關注公眾號也可以吧原理源碼版之節點數據拼接上一篇我們 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究...
摘要:寫文章不容易,點個贊唄兄弟專注源碼分享,文章分為白話版和源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于版本如果你覺得排版難看,請點擊下面鏈接或者拉到下面關注公眾號也可以吧原理白話版終于到了要講白話的時候了 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究...
摘要:寫文章不容易,點個贊唄兄弟專注源碼分享,文章分為白話版和源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于版本如果你覺得排版難看,請點擊下面鏈接或者拉到下面關注公眾號也可以吧原理源碼版之拼接綁定的事件今天我們 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究...
摘要:一旦我們檢測到這些子樹,我們可以把它們變成常數,這樣我們就不需要了在每次重新渲染時為它們創建新的節點在修補過程中完全跳過它們。否則,吊裝費用將會增加好處大于好處,最好總是保持新鮮。 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,...
摘要:頁面這個實例,按理就需要解析兩次,但是有緩存之后就不會理清思路也就是說,其實內核就是不過是經過了兩波包裝的第一波包裝在中的內部函數中內部函數的作用是合并公共和自定義,但是相關代碼已經省略,另一個就是執行第二波包裝在中,目的是進行緩存 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 ...
閱讀 1405·2021-11-25 09:43
閱讀 2260·2021-09-27 13:36
閱讀 1114·2021-09-04 16:40
閱讀 1957·2019-08-30 11:12
閱讀 3308·2019-08-29 14:14
閱讀 565·2019-08-28 17:56
閱讀 1320·2019-08-26 13:50
閱讀 1246·2019-08-26 13:29