寫文章不容易,點個讚唄兄弟
專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧 研究基於 Vue版本 【2.5.17】html
若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧node
【Vue原理】Compile - 源碼版 之 generate 節點拼接 express
終於走到了 Vue 渲染三巨頭的最後一步了,那就是 generate,反正文章已經寫完了,乾脆早點發完,反正這部份內容你們也不會立刻看哈哈數組
或者先看白話版好了 Compile - 白話版 bash
而後,generate的做用就是,解析 parse 生成的 ast 節點,拼接成字符串,而這個字符串,是能夠被轉化成函數執行的。函數執行後,會生成對應的 Vnode函數
Vnode 就是Vue 頁面的基礎,咱們就能夠根據他完成DOM 的建立和更新了學習
好比這樣ui
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 分爲不少種,而每一種的拼接方式都不同,咱們會針對每一種方式來詳細列出來this
下面將會講這麼多種類型節點的拼接spa
靜態節點,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
}
}
複製代碼
對上面出現的幾個可能有點迷惑的東西解釋一下
options 是傳入的一些判斷函數或者指令函數,以下,不一一列舉
{
expectHTML: true,
modules: modules$1,
directives: directives$1
....
};
複製代碼
給該實例初始化編譯的狀態,下面會有源碼
把 ast 轉成字符串的 罪魁禍首
你也看到了
這就是做爲 render 的主要形態,包了一層 with
render 會有一塊內容專門說,with 就很少說了哈,就是爲了爲 render 綁定實例爲上下文
這是一個 數組,由於一個模板裏面可能存在多個靜態根節點,那麼就要把這些靜態根節點都轉換成 render 字符串保存起來,就是保存在數組中
上面是靜態根節點?簡單就是說,第一靜態,第二某一部分靜態節點的最大祖宗,以下圖
兩個 span 就是 靜態根節點,他們都是他們那個靜態部分的最大祖宗,而 div 下 有 v-if 的子節點,因此 div 不是靜態根節點
而後下面這個靜態模板,解析獲得 render 放到 staticRenderFns 是這樣的
<div name="a">
<span>111</span>
</div>
staticRenderFns=[
`
with(this){
return _c('div',
{attrs:{"name":"a"}},[111])]
)
}
`
]
複製代碼
而 staticRenderFns 也會在 render 模塊下詳細記錄
初始化實例的編譯狀態
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 這個函數中添加的屬性解釋一下。
這個數組,存放的是兩個函數
style$1.genData 處理 ast 中的 style ,包括動態靜態的 style
klass$1.genData 處理 ast 中的 class ,包括動態靜態的 class
好比
<div class="a" :class="aa"
style="height:0" :style="{width:0}">
</div>
解析成 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 會在後面拼接節點數據的時候調用到
這也是個數組,存放的是 Vue 自有指令的獨屬處理函數
包括如下幾個指令的處理函數
v-on,綁定事件
v-bind,綁定屬性
v-cloak,編譯前隱藏DOM
v-model,雙向綁定
v-text,插入文本
v-html,插入html
當你在模板中使用到以上的指令的時候,Vue 會調用相應的函數先進行處理
一個數組,用來存放靜態根節點的render 函數,上面有提到過一點
每一個實例都獨有這個屬性,若是沒有靜態根節點就爲空
好比下面這個模板,有兩個靜態根節點
而後在實例 的 staticRenderFns 中就存放兩個 靜態 render
那麼咱們如今就來看,generate 的重點函數,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
專門用於處理帶有 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)
}
}
複製代碼
這個函數的做用呢,是這樣的
一、按順序處理 ifCondition 中的每個節點,而且會移出數組
二、而且每個節點使用 三元表達式 去拼接
三、遞歸調用 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 的指令的
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(','))
}]`
}
複製代碼
一樣的,這個函數也是很簡單的吼
就是遍歷全部子節點,逐個處理子節點,而後獲得一個新的數組
一、當子節點 type ==1 時,說明是標籤,那麼就要 genElement 處理一遍
二、不然,就是文本節點
若是 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')] )] )`
複製代碼
看了上面這麼多的處理函數,各類函數處理後獲得的字符串是相加的關係
而後如今用一個小例子來實現如下拼接步驟
一、先解析最外層 div,獲得字符串
`_c( 'div' `
複製代碼
genChildren 開始解析子節點
二、處理 strong,這是一個靜態根節點,genStatic 處理獲得字符串
`_c( 'div' , [ _m(0) `
複製代碼
三、處理 p 節點,genIf 處理拼接字符串
`_c( 'div' , [ _m(0) , isShow? _c(p) :_e() `
複製代碼
四、處理 span 節點, genFor 拼接字符串
`_c( 'div' , [
_m(0) ,
isShow? _c(p) :_e() ,
_l(arr,function(item,index){return _c('span')})
`
複製代碼
五、處理 test 組件節點,genComponent 拼接
`_c( 'div' , [
_m(0) ,
isShow? _c(p) :_e() ,
_l(arr,function(item,index){return _c('span')}),
_c('test')
`
複製代碼
六、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 等是原封不動的哈哈,但是直接照抄
鑑於本人能力有限,不免會有疏漏錯誤的地方,請你們多多包涵,若是有任何描述不當的地方,歡迎後臺聯繫本人,有重謝