閱讀時間大約16~22min
做者:汪汪
我的主頁:www.zhihu.com/people/wang…javascript
市面上有不少基於vue的core和compile作出的優化開源框架,爲非Web場景引入了Vue的能力,所以學習成本低,受到廣大開發者的歡迎,下面大致列一下我所瞭解到的,有更優秀的歡迎你們評論指出html
分類 | 技術 |
---|---|
跨平臺native | weex |
小程序 | mpvue |
服務端渲染 | Vue SSR |
小程序多端統一框架 | uni-app |
至於提供類Vue開發體驗的框架就數不勝數了,如小程序框架--wepy,前端
從其餘的方面看,github日榜,Vue天天都有過100的star,足見其火熱程度,這也是爲何你們都爭先恐後的在非web領域提供Vue的支持。那麼Vue的底層架構及其應用就尤其重要了vue
瞭解Vue的底層架構,是爲非web領域提供Vue能力的大前提。Vue核心分爲三大塊:core,compiler,platform,下面分別介紹其原理及帶來的能力。java
core是Vue的靈魂所在,正是core實現了經過vnode方式,遞歸生成指定平臺視圖並在數據變更時,自動diff更新視圖,也正是由於VNode機制,使得core是平臺無關的,就算core的功能在於UI渲染。node
我將從以下幾個方面來講明coregit
將vnode生成的具體平臺元素append到已知節點上。咱們拿web平臺舉例,用vnode經過document.createElement生成dom,而後在append到文檔樹中某個節點上。後面咱們也會常常說到掛載組件,它指的就是執行組件對應render生成vnode,而後遍歷vnode生成具體平臺元素,組件的根節點元素會被append到父元素上。github
指令在Vue中是具備特定含義的屬性,指令分兩類,一類是編譯時處理,在生成的render函數上體現,如:v-if,v-for,另一類是運行時使用,更多的是對生成的具體平臺元素操做,web平臺的話就是對dom的操做web
vnode是虛擬node節點,是具體平臺元素對象的進一步抽象(簡化版),每個平臺元素對應一個vnode,可經過vnode結構完整還原具體平臺元素結構。 下面以web平臺來解釋vnode。對於web,假定有以下結構:算法
<div class="box" @click="onClick">------------------對應一個vnode
<p class="content">哈哈</p>-------對應一個vnode
<TestComps></TestComps>----------自定義組件一樣對應一個vnode
<div></div>-----------------------對應一個vnode
</div>
複製代碼
通過Vue的compile模塊將生成渲染函數,執行這個渲染函數就會生成對應的vnode結構:
//這裏我只列出關鍵的vnode信息
{
tag:'div',
data:{attr:{},staticClass:'box',on:{click:onClick}},
children:[{
tag:'p',
data:{attr:{},staticClass:'content',on:{}},
children:[{
tag:'',
data:{},
text:'哈哈'
}]
},{
tag:'div',
data:{attr:{},on:{}},
},{
tag:'TestComps',
data:{
attr:{},
hook:{
init:fn,
prepatch:fn,
insert:fn,
destroy:fn
}
},
}]
}
複製代碼
最外層的div對應一個vnode,包含三個孩子vnode,注意自定義組件也對應一個vnode,不過這個vnode上掛着組件實例
組件實例其實就是Vue實例對象,只有自定義組件纔會有,平臺相關元素是沒有的,要看懂Vue的core,明白下面這個關係很重要。如今,讓咱們來直觀感覺下:
假定有以下結構的模板,元素上的vnode表示生成的對應vnode名稱:
// new Vue的template,對應的實例記爲vm1
<div vnode1>
<p vnode2></p>
<TestComps vnode3
testAttr="hahha"
@click="clicked"
:username="username"
:password="password"></TestComps>
</div>
複製代碼
// TestComps的template,對應的實例記爲vm2
<div vnode4>
<span vnode5></span>
<p vnode6></p>
</div>
複製代碼
// 生成的vnode關係樹爲
vnode1={
tag:'div',
children:[vnode2,vnode3]
}
vnode3={
tag:'TestComps',
children:undefined,
parent:undefined
}
vnode4={
tag:'div',
children:[vnode5,vnode6],
parent:vnode3 //這一點關係很重要
}
複製代碼
// 生成的vm關係樹爲
vm1={
$data:{password: "123456",username: "aliarmo"}, //組件對應state
$props:{} //使用組件時候傳下來到模板裏面的數據
$attrs:{},
$children:[vm2],
$listeners:{}
$options: {
components: {}
parent: undefined //父組件實例
propsData: undefined //使用組件時候傳下來到模板裏面的數據
_parentVnode: undefined
}
$parent:undefiend //當前組件的父組件實例
$refs:{} //當前組件裏面包含的dom引用
$root:vm1 //根組件實例
$vnode:undefined //組件被引用時候的那個vnode,好比<TestComps></TestComps>
_vnode:vnode1 //當前組件模板根元素所對應的vnode對象
}
vm2={
$data:{} //組件對應state
$props:{password: "123456",username: "aliarmo"} //使用組件時候傳下來到模板裏面的數據
$attrs:{testAttr:'hahha'},
$children:[],
$listeners:{click:fn}
$options: {
components: {}
parent: vm1 //父組件實例
propsData: {password: "123456",username: "aliarmo"} //使用組件時候傳下來到模板裏面的數據
_parentVnode: vnode3
}
$parent:vm1 //當前組件的父組件實例
$refs:{} //當前組件裏面包含的dom引用
$root:vm1 //根組件實例
$vnode:vnode3 //組件被引用時候的那個vnode,好比<TestComps></TestComps>
_vnode:vnode4 //當前組件模板根元素所對應的vnode對象
}
複製代碼
它可讓咱們在下一個事件循環作一些操做,而非在本次循環,用於異步更新,原理在於microtask和macrotask
讓咱們來看段代碼:
new Promise(resolve=>{
return 123
}).then(data=>{
console.log('step2',data)
})
console.log('step1')
複製代碼
結果是先輸出 step1,而後在step2,resolve的promise是一個microtask,同步代碼是macrotask
// 在Vue中
this.username='aliarmo' // 能夠觸發更新
this.pwd='123' // 一樣能夠觸發更新
複製代碼
那同時改變兩個state,是否會觸發兩次更新呢,並不會,由於this.username觸發更新的回調會被放入一個經過Promise或者MessageChannel實現的microtask中,亦或是setTimeout實現的macrotask,總之到了下一個事件循環。
一個組件對應一個watcher,在掛載組件的時候建立這個觀察者,組件的state,包含data,props都是被觀察者,被觀察者的任何變化會被通知到觀察者,被觀察者的變更致使觀察者執行的動做是vm._update(vm._render(), hydrating)
,組件從新render生成vnode並patch。
明白這個關係很重要:觀察者包含對變更作出響應的定義,一個組件對應一個觀察者對應組件裏面的全部被觀察者,被觀察者可能被用於其餘組件,那麼一個被觀察者會對應多個觀察者,當被觀察者發生變更時,通知到全部觀察者作出更新響應。
組件A的state1發生了變化,那會致使觀察了這個state1的watcher收到變更通知,會致使組件A從新渲染生成新的vnode,在組件A新vnode和老的vnode patch的過程當中,會updateChildrenComponent,也就是致使子組件B的props被從新設置一個新值,由於子組件B是有觀察傳入的state1的,所以會通知到相應watcher,致使子組件B的更新
整個watcher體系的創建過程:
vm._update(vm._render(), hydrating)
,執行render函數致使屬性的get函數被調用,每一個屬性會對應一個dep實例,在這個時候,dep實例關聯到組件對應的watcher,實現依賴收集,關聯後popTarget。state變更響應過程:
vm._update(vm._render(), hydrating)
,執行這個run函數,致使從新生成vnode,進行patch,通過diff,達到更新UI目的父組件state變化如何致使子組件也發生變化?
父組件state更新後,會致使渲染函數從新執行,生成新的vnode,在oldVnode和newVnode patch的過程當中,若是遇到的是組件vnode,會updateChildrenComponent,這裏面作的操做就是更新子組件的props,由於子組件是有監聽props屬性的變更的,致使子組件re-render
父組件傳入一個對象給子組件,子組件改變傳入的對象props,父組件又是如何被更新到的?
大前提:若是父組件傳給子組件的props中有對象,那麼子組件接收到的是這個對象的引用。也就是ParentComps中的this.person和SubComps中的this.person指向同一個對象
// 假定父組件傳person對象給子組件SubComps
Vue.component('ParentComps',{
data(){
return {
person:{
username:'aliarmo',
pwd:123
}
}
},
template:`
<div>
<p>{{person.username}}</p>
<SubComps :person="person" />
</div>
`
})
複製代碼
如今咱們在SubComps裏面,更新person對象的某個屬性,如:this.person.username='wmy' 這樣會致使ParentComps和SubComps的更新,爲何呢?
由於Vue在ParentComps中會深度遞歸觀察對象的每一個屬性,在第一次執行ParentComps的render的時候,綁定ParentComps的Watcher,傳入到SubComps後,不會對傳入的對象在進行觀察,在第一次執行SubComps的render的時候,會綁定到SubComps的Watcher,所以當SubComps改變了this.person.username的值,會通知到兩個Watcher,致使更新。這很好的解釋了憑空在傳入的props屬性對象上掛載新的屬性不觸發渲染,由於傳入的props屬性對象是在父組件被觀察的。
當組件的state發生變化,從新執行渲染函數生成新的vnode,而後將新生成的vnode與老的vnode進行對比,以最小的代價更新原有視圖。diff算法的原理是經過移動、新增、刪除和替換oldChildrenVnodes對應的結構來生成newChildrenVnodes對應的結構,而且每一個老的元素只能被複用一次,老元素最終的位置取決於當前新的vnode。要明確傳入diff算法的是兩個sameVnode的孩子節點,從二者的開頭和結尾位置,同時往中間靠,直到二者中的一個到達中間。
PS:oldChildrenVnodes表示老的孩子vnode節點集合,newChildrenVnodes表示state變化後生成的新的孩子vnode節點集合
說這個算法以前,先得明白如何判斷兩個vnode爲sameVnode,我只大致列一下:
<Comps1 key="key1" />
, <Comps2 key="key2" />
,key值就不相等, <Comps1 key="key1" />
, <Comps2 key="key1" />
, key值就是相等的, <div></div>
,<p></p>
,這兩個的key值是undefined,key值相等,這個是sameVnode的大前提。整個vnode diff流程
大前提,要看懂這個vnode diff,務必先明白vnode是啥,如何生成的,vnode與elm的關係,詳情請看上面的vnode概念
(1)首先vnode的elm指向oldVnode的elm
(2)使用vnode的數據更新elm的attr,class,style,domProps,events等
(3)若是vnode是文本節點,則直接設置elm的text,結束
(4)若是vnode是非文本節點&&有孩子&&oldVnode沒有孩子,則elm直接append
(5)若是vnode是非文本節點&&沒有孩子&&oldVnode有孩子,則直接移除elm的孩子節點
(6)若是非文本節點&&都有孩子節點,則updateChildren,進入diff 算法,前面5個步驟排除了不能進行diff狀況
這裏還有強調下,傳入diff算法的是兩個sameVnode的孩子節點,那麼如何用newChildrenVnodes替換oldChildrenVnodes,最簡單的方式莫過於,遍歷newChildrenVnodes,直接從新生成這個html片斷,皆大歡喜。可是這樣作會 不斷的createElement,對性能有影響,因而前輩們就想出了這個diff算法。
(1)取二者最左邊的節點,判斷是否爲sameVnode,若是是則進行上述的第二步patch vnode過程,整個流程走完後,此時elm的class,style,events等已經更新了,elm的children結構也經過前面說的整個流程獲得了更新,這時候就看是否須要移動這個elm了,由於都是孩子的最左邊節點,所以位置不變,最左邊節點位置向前移動一步
(2)若是不是(1)所述case,取二者最右邊的節點,跟(1)的斷定流程同樣,不過是最右邊節點位置向前移動一步
(3)若是不是(1)(2)所述case,取oldChildrenVnodes最左邊節點和newChildrenVnodes最右邊節點,跟(1)的斷定流程同樣,不過,elm的位置須要移動到oldVnode最右邊elm的右邊,由於vnode取的是最右邊節點,若是與oldVnode的最右邊節點是sameVnode的話,位置是不用改變的,所以newChildrenVnodes的最右節點和oldChildrenVnodes的最右節點位置是對應的,但因爲是複用的oldChildrenVnodes的最左邊節點,oldChildrenVnodes最右邊節點尚未被複用,所以不能替換掉,因此移動到oldChildrenVnodes最右邊elm的右邊。而後oldChildrenVnodes最左邊節點位置向前移動一步,newChildrenVnodes最右邊節點位置向前移動一步
(4)若是不是(1)(2)(3)所述case,取oldChildrenVnodes最右邊節點和newChildrenVnodes最左邊節點,跟(1)的斷定流程同樣,不過,elm的位置須要移動到oldChildrenVnodes最左邊elm的左邊,由於vnode取的是最左邊節點,若是與oldChildrenVnodes的最左邊節點是sameVnode的話,位置是不用改變的,所以newChildrenVnodes的最左節點和oldChildrenVnodes的最左節點位置是對應的,但因爲是複用的oldChildrenVnodes的最右邊節點,oldChildrenVnodes最左邊節點尚未被複用,所以不能替換掉,因此移動到oldChildrenVnodes最左邊elm的左邊。而後oldChildrenVnodes最右邊節點位置向前移動一步,newChildrenVnodes最左邊節點位置向前移動一步
(5)若是不是(1)(2)(3)(4)所述case,在oldChildrenVnodes中尋找與newChildrenVnodes最左邊節點是sameVnode的oldVnode,若是沒有找到,則用這個新的vnode建立一個新element,插入位置如後所述,若是找到了,則跟(1)的斷定流程同樣,不過插入的位置是oldChildrenVnodes的最左邊節點的左邊,由於若是newChildrenVnodes最左邊節點與oldChildrenVnodes最左邊節點是sameVnode的話,位置是不用變的,而且複用的是oldChildrenVnodes中找到的oldVNode的elm。被複用過的oldVnode後面不會再被取出來。而後newChildrenVnodes最左邊節點位置向前移動一步
(6)通過上述步驟,oldChildrenVnodes或者newChildrenVnodes的最左節點與最右節點重合,退出循壞
(7)若是是oldChildrenVnodes的最左節點與最右節點先重合,說明newChildrenVNodes還有節點沒有被插入,遞歸建立這些節點對應元素,而後插入到oldChildrenVnodes的最左節點的右邊或者最右節點的左邊,由於是從二者的開始和結束位置向中間靠攏,想一想,若是newChildrenVNodes剩餘的第一個節點與oldChildrenVnodes的最左邊節點爲sameVnode的話,位置是不用變的
(8)若是是newChildrenVnodes的最左節點與最右節點先重合,說明oldChildrenVnodes中有一段結構沒有被複用,開始和結束位置向中間靠攏,所以沒有被複用的位置是oldChildrenVnodes的最左邊和最右邊之間節點,刪除節點對應的elm便可。
舉個栗子來描述下具體的diff過程(web平臺):
// 有Vue模板以下
<div> ------ oldVnode1,newVnode1,element1
<span v-if="isShow1"></span> -------oldVnode2,newVnode2,element2
<div :key="key"></div> -------oldVnode3,newVnode3,element3
<p></p> -------oldVnode4,newVnode4,element4
<div v-if="isShow2"></div> -------oldVnode5,newVnode5,element5
</div>
// 若是 isShow1=true,isShow2=true,key="aliarmo"那麼模板將會渲染成以下:
<div>
<span></span>--------------element2
<div key="aliarmo"></div>----------element3
<p></p>-------------element4
<div></div>----------element5
</div>
// 改變state,isShow1=false,isShow2=true,key="wmy",那麼模板將會渲染成以下:
<div>
<div key="wmy"></div>------------element6
<p></p>-------------------element4
<div></div>---------element5
</div>
複製代碼
那麼,改變state後的dom結構是如何生成的?
如上圖,在isShow1=true,isShow2=true,key="aliarmo"
條件下,生成的vnode結構是: oldVnode1,oldVnodeChildren=[oldVnode2,oldVnode3,oldVnode4,oldVnode5]
對應的dom結構爲:
改變state爲isShow1=false,isShow2=true,key="wmy"
後,生成的新vnode結構是
newVnode1,newVnodeChildren=[newVnode3,newVnode4,newVnode5]
最左邊兩個新老vnode對比,也就是oldVnode2
,newVnode3
,不是sameVnode
,
那最右邊兩個新老vnode對比,也就是oldVnode5
,newVnode5
,是sameVnode
,不用移動原來的Element5所在位置,原有dom結構未發生變化,
最左邊兩個新老vnode對比,也就是oldVnode2
,newVnode3
,不是sameVnode
,
那最右邊兩個新老vnode對比,也就是oldVnode4
,newVnode4
,是sameVnode
,不用移動原來的Element4所在位置,原有dom結構未發生變化,
最左邊兩個新老vnode對比,也就是oldVnode2
,newVnode3
,不是sameVnode
,
那最右邊兩個新老vnode對比,也就是oldVnode3
,newVnode3
,因爲key值不一樣,不是sameVnode
,
當前最左邊和最右邊對比,oldVnode2
,newVnode3
,不是sameVnode
當前最右邊和最左邊對比,oldVnode5
,newVnode3
,不是sameVnode
在遍歷oldVnodeChildren,尋找與newVnode3
爲sameVnode
的oldVnode
,沒有找到,則用newVnode3
建立一個新的元素Element6
,插入到當前oldVnode2
所對應元素的最左邊,dom結構發生變化 newVnodeChildren
兩頭重合,退出循環,刪除剩餘未被複用元素Element2
,Element3
如今咱們終於能夠理一下,從new Vue()開始,core裏面發生了些什麼
Vue的compiler部分負責對template的編譯,生成render和staticRender函數,編譯一次永久使用,因此通常咱們在構建的時候就作了這件事情,以提升頁面性能。執行render和staticRender函數能夠生成VNode,從而爲core提供這一層抽象。
template ==》 AST ==》 遞歸ATS生成render和staticRender ==》VNode
(1)template轉化成AST過程
先讓咱們來直觀感覺下AST,它描述了下面的template結構
// Vue模板
let template = `
<div class="Test" :class="classObj" v-show="isShow">
{{username}}:{{password}}
<div>
<span>hahhahahha</span>
</div>
<div v-if="isVisiable" @click="onClick"></div>
<div v-for="item in items">{{item}}</div>
</div>
`
複製代碼
下面描述下template轉爲AST的簡要過程:
(1)若是是開始標籤,則處理相似於下面字符串
<div class="Test" :class="classObj" v-show="isShow">
複製代碼
經過正則能夠很容易解析出tag,全部屬性列表,再對屬性列表進行分類,分別解析出v-if,v-for等指令,事件,特殊屬性等,template去除被解析的部分,回到步驟1
(2)若是是結束標籤,則處理相似於下面字符串,一樣template去除被解析的部分,回到步驟1
</div>
複製代碼
{{username}}:{{password}} 或者 用戶名:密碼
複製代碼
(2)AST生成render和staticRender
主要是遍歷ast(有興趣的同窗能夠本身體驗下,如:遍歷AST生成還原上述模板,相信會有不同的體驗),根據每一個節點的屬性拼接渲染函數的字符串,如:模板中有v-if="isVisiable",那麼AST中這個節點就會有一個if屬性,這樣,在建立這個節點對應的VNode的時候,就會有
(isVisiable) ? _c('div') : _e()
複製代碼
在with的做用下,isVisiable
的值決定了VNode是否生成。固然,對於一些指令,在編譯時是處理不了的,會在生成VNode的時候掛載在VNode上,解析VNode時再進行進一步處理,好比v-show,v-on。
下面是上面模板生成的render和staticRender:
// render函數
(function anonymous() {
with (this) {
return _c('div', {
directives: [{
name: "show",
rawName: "v-show",
value: (isShow),
expression: "isShow"
}],
staticClass: "Test",
class: classObj
}, [_v("\n " + _s(username) + ":" + _s(password) + "\n "), _m(0), _v(" "), (isVisiable) ? _c('div', {
on: {
"click": onClick
}
}) : _e(), _v(" "), _l((items), function(item) {
return _c('div', [_v(_s(item))])
})], 2)
}
}
)
// staticRender
(function anonymous() {
with (this) {
return _c('div', [_c('span', [_v("hahhahahha")])])
}
}
)
複製代碼
其中this是組件實例,_c、_v分別用於建立VNode和字符串,像username和password是在定義組件時候傳入的state並被掛載在this上。
platform模塊與具體平臺相關,咱們能夠在這裏定義平臺相關接口傳入runtime和compile,以實現具體平臺的定製化,所以爲其餘平臺帶來Vue能力,大部分工做在這裏。
須要傳入runtime的是如何建立具體的平臺元素,平臺元素之間的關係以及如何append,insert,remove平臺元素等,元素生成後須要進行的屬性,事件監聽等。拿web平臺舉例,咱們須要傳入document.createElement,document.createTextNode,遍歷vnode的時候生成HTML元素;掛載時須要的insertBefore;state發生變化致使vnode diff時的remove,append等。還有生成HTML元素後,用setAttribute和removeAttribute操做屬性;addEventListener和removeEventListener進行事件監聽;提供一些有利於web平臺使用的自定義組件和指令等等
須要傳入compile的是對某些特殊屬性或者指令在編譯時的處理。如web平臺,須要對class,style,model的特殊處理,以區別於通常的HTML屬性;提供web平臺專用指令,v-html(編譯後實際上是綁定元素的innerHTML),v-text(編譯後實際上是綁定元素的textContent),v-model,這些指令依賴於具體的平臺元素。
說了這麼多,最終目的是爲了複用Vue的core和compile,以期在其餘的平臺上帶來Vue或者類Vue的開發體驗,前面也說了不少複用成功的例子,若是你要爲某一平臺帶來Vue的開發體驗,能夠進行參考。大前端概念下,指不定那天,汽車顯示屏,智能手錶等終端界面就能夠用Vue來進行開發,爽歪歪。那麼,如何複用呢?固然我只說必選的,你能夠定製更多更復雜的功能方便具體平臺使用。
定義vnode生成具體平臺元素所須要的nodeOps,也就是元素的增刪改查,對於web來講nodeOps是要建立,移動真正的dom對象,若是是其餘平臺,可自行定義這些元素的操做方法;
定義vnode生成具體平臺元素所須要的modules,對於web來講,modules是用來操做dom屬性的方法;
具體平臺須要定義一個本身的$mount方法給Vue,掛載組件到已存在的元素上;
還有一些方法,如isReservedTag,是否爲保留的標籤名,自定義組件名稱不能與這些相同;mustUseProp,斷定元素的某個屬性必需要跟組件state綁定,不能綁定一個常量等等;
軟件行業有一句名言,沒有什麼問題是添加一層抽象層不能解決的,若是有那就兩層,Vue的設計正是如此(可能Vue也是借鑑別人的),compile的AST抽象層銜接了模板語法和render函數,通過VNode這個抽象層讓core剝離了具體平臺。
這篇文章的最終目的是爲了讓你們瞭解到複用Vue源碼的core和compile進行二次開發,能夠作到爲具體平臺帶來Vue或者類Vue的開發體驗。固然,這只是一種思路,說不定哪天,Vue風格開發體驗失寵了,那麼咱們也能夠把這種思路應用到新開發風格上。
關注【IVWEB社區】公衆號獲取每週最新文章,通往人生之巔!