前段時間由于React Licence的問題,團(tuán)隊(duì)積極探索React的替代產(chǎn)品,考慮到未來可能的移動業(yè)務(wù),團(tuán)隊(duì)旨在尋找一款遷移成本低、體積小的替代產(chǎn)品。經(jīng)過多次探索,Preact進(jìn)入了我們的視野。自從接觸Preact,一路學(xué)習(xí)下來,掉了很多頭發(fā),收獲了很多思想。這里介紹一下Preact的實(shí)現(xiàn)思路,分享一下自己的想法。
什么是Preact
一句話介紹PReact,是3KB的輕量級替代反應(yīng),和ES6 API一樣。如果我覺得這句話太模糊,我可以多說幾句。Preact = performance+react,這是Preact這個名字的由來。其中一場表演足以看出作者的意圖。下圖反映了不同框架在長列表初始化場景下的性能,可以看出Preact確實(shí)有突出的性能。
高性能、輕量化、準(zhǔn)時制生產(chǎn)是Preact的核心?;谶@些主題,Preact關(guān)注react的核心功能,并實(shí)現(xiàn)了一個簡單且可預(yù)測的diff算法,使其成為速度最快的虛擬DOM框架之一。同時,preact-compat為兼容性提供了保障,使得preact可以無縫連接react生態(tài)系統(tǒng)中的大量組件,同時補(bǔ)充了Preact沒有實(shí)現(xiàn)的很多功能。
與預(yù)啟動工作流相比,列表初始化時間較長
本文簡要介紹了Preact的過去和現(xiàn)在,然后講述了Preact的工作流程,主要包括五個模塊:
組成部分
h函數(shù)
提供
Diff算法
回收機(jī)制
循環(huán)過程見下圖。
首先,我們定義組件。在渲染開始的時候,我們會先進(jìn)入H函數(shù)生成對應(yīng)的虛擬節(jié)點(diǎn)(如果寫了JSX,需要先轉(zhuǎn)碼)。每個vnode都包含自己節(jié)點(diǎn)的信息及其子節(jié)點(diǎn)的信息,這些信息被鏈接到一個虛擬dom樹中?;谏傻膙node,渲染模塊將根據(jù)當(dāng)前的dom樹控制進(jìn)程,并為后續(xù)的diff操作做一些準(zhǔn)備。Preact的diff算法的實(shí)現(xiàn)不同于基于雙虛擬dom樹的react的思想。Preact只維護(hù)一個新的虛擬dom樹。在diff期間,將基于dom樹恢復(fù)舊的虛擬dom樹,然后將兩者進(jìn)行比較。在比較過程中,實(shí)時對dom樹進(jìn)行補(bǔ)丁操作,最終生成新的DOM樹。同時,diff過程中卸載的組件和節(jié)點(diǎn)不會被直接刪除,而是分別放入回收池進(jìn)行緩存。當(dāng)再次構(gòu)建同類型的組件或節(jié)點(diǎn)時,可以在回收池中找到同名的元素進(jìn)行重構(gòu),從而避免從零開始構(gòu)建的開銷。
前期工作流程圖
在了解了Preact的工作流程之后,我們將對上面提到的五個模塊逐一進(jìn)行解讀。
1.組成部分
關(guān)鍵詞:鉤子,鏈接狀態(tài),批量更新
相信有過react開發(fā)經(jīng)驗(yàn)的同學(xué)對組件的概念都比較熟悉,這里就不做過多的解釋了,只介紹Preact在組件層面增加的一些新特性。
掛鉤功能
Preact除了基本的生命周期功能外,還提供了三個鉤子功能,方便用戶在指定的時間點(diǎn)進(jìn)行統(tǒng)一操作。
后安裝
更新后
卸載前
linkStateLinkState就是針對render方法中用戶操作的回調(diào)綁定這個的場景,這樣每次render的時候都會在本地創(chuàng)建一個函數(shù)閉包,這樣效率很低,會迫使垃圾收集器做很多不必要的工作。linkState的理想應(yīng)用場景如下。
exportDefaultClassappextendsComponent { constructor(){ super();this . state = { text:' initial ' } } HandleChange = e = & gt;{ this . SetState({ text:e . target . value })} render({ desc },{ text } } { return(& lt;div>。& ltinput value = { text } OnChange = { this . LinkState(' text ',' target . value ')} & gt;& ltdiv>。{ text } & lt/div>。& lt/div>。)}}
但是,linkState的實(shí)現(xiàn)。。。當(dāng)組件初始化時,為每個回調(diào)創(chuàng)建一個閉包,綁定它,并創(chuàng)建一個實(shí)例屬性來緩存綁定的回調(diào)函數(shù),因此在再次呈現(xiàn)時不需要再次綁定。實(shí)際效果相當(dāng)于組件的構(gòu)造函數(shù)中的綁定。尷尬的是,linkState只實(shí)現(xiàn)了setState操作,不支持自定義參數(shù),所以使用場景有限。
//linkState源代碼//cache回調(diào)linkstate (key,eventpath) {letc = this。_ linkedstates | |(此。_ link ed States = { });returnc[key+event path]| |(c[key+event path]= createLinkedState(this,key,event path));}//創(chuàng)建閉包ExportFunctionCreateLinkedState(組件,鍵,事件路徑){let path = key。拆分('.')第一次注冊回調(diào)時;return function(e){ lett = e & amp;& ampe.target||this,state={},obj=state,v=isString(eventPath)?delver(e,eventPath):t.nodeName?(t.type.match(/^che|rad/)?t.checked:t.value):e,I = 0;for(;i<。path . length-1;i++){ obj = obj[path[I]]| |(obj[path[I]]=!i & amp& ampcomponent . state[path[I]]| | { });} obj[path[I]]= v;component . SetState(state);};}批量更新
Preact實(shí)現(xiàn)了組件的批量更新。具體實(shí)現(xiàn)思路是,每次執(zhí)行狀態(tài)或道具更新時,都會立即更新相應(yīng)的屬性,但基于新狀態(tài)或道具的渲染操作會被推入更新隊(duì)列,在當(dāng)前事件循環(huán)結(jié)束時或下一個事件循環(huán)開始時,會逐一執(zhí)行隊(duì)列中的操作。同一組件狀態(tài)的多次更新不會重復(fù)進(jìn)入隊(duì)列。如下圖所示,屬性更新后,組件渲染前,_dirty的值為true,因此組件渲染前后續(xù)的屬性更新操作不會使組件重復(fù)入隊(duì)。
//更新隊(duì)列源代碼exportfunctionenqueuerender(組件){if(!組件。_臟& amp& amp(組件。_ dirty = true)& amp;& ampitems . push(component)= = 1){(options . DEBOUNDERATION | | delay)(re render);}}2.h函數(shù)
關(guān)鍵詞:節(jié)點(diǎn)合并
h函數(shù)的作用類似于React。并用于生成虛擬節(jié)點(diǎn)。接受的輸入格式如下。這三個參數(shù)是節(jié)點(diǎn)類型、節(jié)點(diǎn)屬性和子元素。
H ('a ',{href:'/',h {'span ',null,' home'}})節(jié)點(diǎn)合并
在生成vnode的過程中,h函數(shù)會合并相鄰的簡單節(jié)點(diǎn),以減少節(jié)點(diǎn)數(shù)量,減輕diff的負(fù)擔(dān)。請看下面的例子。
從“preact”導(dǎo)入{h,Component };constinnerchildren =[[' inner child 2 ',' innerchild3'],' inner child 4 '];constitinearchildren =[& lt;div>。{ innerinnerchildren } & lt/div>。,& ltspan>。desc & lt;/span>。]ExportDefaultClassAppExtendsComponent { render(){ return(& lt;div>。{ innerchildren } & lt/div>。)}}
3.提供
關(guān)鍵詞:過程控制,差異準(zhǔn)備
首先說明一下,這里的渲染模塊一般指的是整個過程中把vnode插入dom樹的操作。然而,這種操作的一些工作是由diff模塊承擔(dān)的,所以實(shí)際上,渲染模塊更負(fù)責(zé)過程控制和進(jìn)入diff的準(zhǔn)備工作。
過程控制
所謂過程控制分為兩部分,即判斷節(jié)點(diǎn)類型是自定義組件還是原生dom節(jié)點(diǎn),判斷渲染類型是第一次渲染還是更新操作。根據(jù)不同的情況,指定不同的渲染路線,執(zhí)行相應(yīng)的生命周期方法、鉤子函數(shù)和渲染邏輯。
Diff就緒
如前所述,Preact在內(nèi)存中只維護(hù)一個內(nèi)容更新的新虛擬dom樹,另一個表示更新的舊虛擬dom樹實(shí)際上是從dom樹中恢復(fù)的。同時,dom樹的更新操作是在比較的同時進(jìn)行修補(bǔ)的。為了保證以上操作不被混淆,在生成/更新dom樹之前,需要給dom節(jié)點(diǎn)添加一些自定義屬性來記錄狀態(tài)。
//創(chuàng)建自定義屬性記錄export functionrendercomponent(component,opts,mountall,ischild) {if (component。_ disable)返回;letskip,rendered,props=component.props,state=component.state,context=component.context,previous props = component . previous props | | props,previous state = component . previous context | | state,previous context = component . previous | context,isUpdate=component.base,nextBase=component.nextBase,initialBase=isUpdate||nextBase,initialBase_component,inst,cbase4.Diff算法
關(guān)鍵詞:DOM依賴,斷開或不斷開,文檔片段
Diff流程主要分為兩個階段。第一階段是建立虛擬節(jié)點(diǎn)和dom Node之間的對應(yīng)關(guān)系,第二階段是比較它們并更新dom Node。
在實(shí)際執(zhí)行過程中,diff操作的起點(diǎn)是update組件的根節(jié)點(diǎn)和表示其下一個狀態(tài)的vnode之間的比較。在這一步中,它們之間的對應(yīng)關(guān)系是非常明確的,但是在下一步中,需要確定它們的子元素之間的對應(yīng)關(guān)系。具體方法是先將鍵值相同的子節(jié)點(diǎn)配對,然后再將同類型的節(jié)點(diǎn)配對。最后,未配對的vnode被視為新添加的節(jié)點(diǎn),而單個dom節(jié)點(diǎn)的命運(yùn)被回收。
進(jìn)入更新階段后,將根據(jù)虛擬節(jié)點(diǎn)的類型和dom樹中的引用節(jié)點(diǎn)進(jìn)行分類處理,并在diff過程中實(shí)時進(jìn)行補(bǔ)丁操作,最終生成新的dom節(jié)點(diǎn),然后遞歸子節(jié)點(diǎn)。
差異流程圖的DOM依賴關(guān)系
前面介紹過了,相信大家對Preact的虛擬dom實(shí)現(xiàn)都有一定的了解,這里就不贅述了。這種實(shí)現(xiàn)的優(yōu)點(diǎn)是總能真實(shí)地反映之前的虛擬dom樹,缺點(diǎn)是存在內(nèi)存泄漏的風(fēng)險(xiǎn)。
斷開或未斷開
斷開是什么意思
眾所周知,當(dāng)我們對dom樹中的節(jié)點(diǎn)執(zhí)行removeChild操作時,每次執(zhí)行都會觸發(fā)一次頁面的回流,這是一種代價(jià)高昂的行為。所以當(dāng)我們要執(zhí)行一系列這樣的操作時,可以采取這樣的優(yōu)化方法,先創(chuàng)建一個節(jié)點(diǎn),然后在這個節(jié)點(diǎn)上執(zhí)行所有子節(jié)點(diǎn)的追加操作,然后將這個節(jié)點(diǎn)作為根節(jié)點(diǎn)的子樹追加或者替換到dom樹中一次,只觸發(fā)一次回流就完成了整個子樹的更新,這就叫做斷開連接。
相反,創(chuàng)建一個節(jié)點(diǎn)后,立即將該節(jié)點(diǎn)插入到dom樹中,然后繼續(xù)子節(jié)點(diǎn)的操作,這叫做連通。
繼續(xù)進(jìn)行預(yù)認(rèn)證
在明確了這個前提之后,我們再來看看Preact,Disconnected或者connected的實(shí)現(xiàn),這是一個圍城。雖然作者聲稱Preact的渲染方法是脫節(jié)的,但事情的真相是,并不總是如此。在一個簡單的例子中,textnode的值被修改或者舊節(jié)點(diǎn)被textnode替換。Preact做的是創(chuàng)建一個textnode或者修改上一個textnode的nodeValue。雖然糾結(jié)這個場景沒有意義,但是為了完整的介紹diff過程,還是要先說明一下。言歸正傳。先看第一個例子。為了說明這個問題,我們用一個稍微極端的例子。
在這個例子中,我們可以看到在輸入文本后,有一個從div子樹到section子樹的更新。為了描述一個極端情況,更新前后的子節(jié)點(diǎn)是相同的。
//示例1:占位符所在的子樹只有不同的根節(jié)點(diǎn)。從“preact”導(dǎo)入{h,Component };exportDefaultClassappextendsComponent { constructor(){ super();this . state = { text:' ' } } handlechang = e = & gt。{ this . SetState({ text:e . target . value })} render({ desc },{ text }){ return(& lt;div>。& ltinput value = { text } OnChange = { this . handlechang }/& gt。{text?& ltsectionkey='placeholder'>。& lth2>。占位符<。/h2>。& lt/section>。:& ltdiv鍵= '占位符' >;& lth2>。占位符<。/h2>。& lt/div>。} & lt/div>。)}}
接下來,看看這個場景的diff操作的詳細(xì)流程。
//idiff logic le tout = DOM of native DOM,//annotation 1 nodename = string(vnode . nodename),prevsvgmode = issvgmode,vchildren = vnode.childrenisSvgMode=nodeName==='svg '?true:nodeName==='foreignObject '?false:ISsvg mode;if(!DOM){//Note 2 out = create node(nodename,issvgmode);}elseif(!IsNamedNode(dom,nodeName)){//comment 3 out = create node(nodeName,issvgmode);while(dom.firstChild)out。(DOM . FirstChild);if(DOM . parent node)DOM . parent node . replace child(out,DOM);RecolkeNodeTree(DOM);}//子節(jié)點(diǎn)遞歸...else if(vchildren & amp;vchildren . length | | fc){ innerDiffNode(out,vchildren,context,mount all);}……
不管參與diff的元素是自定義組件還是原生dom,最后都是以解構(gòu)后的dom形式進(jìn)行比較。因此,我們只需要關(guān)注本機(jī)dom的diff邏輯。
首先看一下注1的位置。dom表示dom樹上的節(jié)點(diǎn),即要更新的節(jié)點(diǎn),vnode是要呈現(xiàn)的虛擬節(jié)點(diǎn)。例1中diff的起點(diǎn)是最外層的div,也就是第一輪的dom變量,所以注2和注3的判斷都是假的。之后,out節(jié)點(diǎn)的子節(jié)點(diǎn)和對應(yīng)vnode的子節(jié)點(diǎn)遞歸地不同。
然后,這里說明第一個問題。渲染操作的起點(diǎn)始終是連通的。
if(vlen){ for(leti = 0;i<。vleni++){ vchild = vchildren[I];child = nullletkey = vchild.key//相同的鍵值匹配if(key!= null){ if(KeyEdlen & amp;& ampkeyinked){ child = keyed[key];keyed[key]= undefined;key edlen-;} }//同一個nodeName匹配elseif(!兒童和青少年。& amp最小<。children len){ for(j = min;j<。童裝;j++){ c = children[j];if(c & amp;& ampisSameNodeType(c,VC hild)){ child = c;children[j]= undefined;if(j = = = children len-1)children len-;if(j = = = min)min++;打破;} } }//當(dāng)vnode為節(jié)節(jié)點(diǎn)時,dom樹中既沒有相同的鍵節(jié)點(diǎn),也沒有相同的nodeName節(jié)點(diǎn),因此為null child = idiff (child,vchild,context,mount all);……
子節(jié)點(diǎn)之間對應(yīng)關(guān)系的建立基礎(chǔ)要么是相同的鍵值,要么是相同的節(jié)點(diǎn)名??梢灾?,截面與div的關(guān)系不滿足上述兩個條件。因此,當(dāng)再次輸入idiff方法時,將在Note 2的位置創(chuàng)建一個新的節(jié)節(jié)點(diǎn),因?yàn)閐om不存在,它將被分配給out。當(dāng)再次進(jìn)行子元素diff時,因?yàn)閛ut是一個新節(jié)點(diǎn),不包含任何子元素,所以diff節(jié)的所有子元素的對象都為空,這意味著最終會創(chuàng)建該節(jié)的所有子元素(不管是否設(shè)置了鍵值),即使它們與舊dom上的節(jié)點(diǎn)相同。。。所以綜上所述,就是例1的情況,段的所有子節(jié)點(diǎn)都是新建的,不是重用的,而是整個操作過程都是在斷開的情況下進(jìn)行的。
如果兩者添加相同的鍵值會怎樣?
//例2,組件結(jié)構(gòu)是一樣的,唯一的區(qū)別是把從‘preact’導(dǎo)入的相同鍵值{h,Component }添加到占位符所在的子樹中;exportDefaultClassappextendsComponent { constructor(){ super();this . state = { text:' ' } } handlechang = e = & gt。{ this . SetState({ text:e . target . value })} render({ desc },{ text }){ return(& lt;div>。& ltinput value = { text } OnChange = { this . handlechang }/& gt。{text?& ltsectionkey='placeholder'>。& lth2>。占位符<。/h2>。& lt/section>。:& ltdiv鍵= '占位符' >;& lth2>。占位符<。/h2>。& lt/div>。} & lt/div>。)}}
因?yàn)樗鼈冇邢嗤逆I值,在確定了vnode和dom的對應(yīng)關(guān)系后,就可以成功配對,進(jìn)入diff鏈接。但是,替換操作會使所有后續(xù)操作連接起來。好消息是相同的子節(jié)點(diǎn)被重用。
//原生dom // dom節(jié)點(diǎn)的diff邏輯,即div存在,與vnode節(jié)點(diǎn)類型section type elseif(!isNamedNode(dom,nodeName)){ out = create node(nodeName,isSvgMode);while(dom.firstChild)out。(DOM . FirstChild);if(DOM . parent node)DOM . parent node . replace child(out,DOM);RecolkeNodeTree(DOM);}DocumentFragment
除了上述斷開連接的方法,還可以通過DocumentFragment一次將一系列節(jié)點(diǎn)插入到dom中。當(dāng)文檔片段節(jié)點(diǎn)被插入到文檔樹中時,它不是文檔片段本身,而是它的所有后代節(jié)點(diǎn)。這使得DocumentFragment成為一個有用的占位符,臨時存儲插入文檔一次的節(jié)點(diǎn)。github上也有人問了作者同樣的問題。作者說,他曾試圖通過DocumentFragment的方式減少回流的次數(shù),但最終的結(jié)果令人驚訝。
上圖是作者寫的測試用例性能對比圖,橫坐標(biāo)是每秒操作數(shù)。值越大,執(zhí)行效率越高??梢钥闯觯瑹o論是連接還是斷開,DocumentFragement的性能都比較差。具體原因還有待研究。BenchMark的原始鏈接。
5.回收機(jī)制
關(guān)鍵詞:回收池&增強(qiáng)安裝
回收池&增強(qiáng)安裝
當(dāng)一個節(jié)點(diǎn)從dom中移除時,它不會被直接刪除,而是根據(jù)節(jié)點(diǎn)類型(組件或節(jié)點(diǎn))執(zhí)行一些清理邏輯后,存儲在兩個回收池中。每次執(zhí)行裝載操作時,創(chuàng)建方法都會在回收池中查找相同類型的節(jié)點(diǎn)。一旦找到這種相同類型的節(jié)點(diǎn),它們將作為要更新的參考節(jié)點(diǎn)被傳遞到diff算法中,以便在隨后的比較過程中,來自回收池的節(jié)點(diǎn)將作為原型被修補(bǔ)以生成新節(jié)點(diǎn)。相當(dāng)于把Mount改成Update,從而避免了從零開始構(gòu)建的額外開銷。
現(xiàn)實(shí)結(jié)局往往不如童話,回收機(jī)制終于出事。在犯罪現(xiàn)場的傳輸門口,恢復(fù)機(jī)制在某些情況下會導(dǎo)致節(jié)點(diǎn)的錯誤重用...因此,就像發(fā)炎的闌尾一樣,恢復(fù)機(jī)制可能很快就會從我們的視線中消失。
標(biāo)簽
本文重點(diǎn)介紹了Preact的工作流程以及各個模塊的一些工作細(xì)節(jié),希望能夠吸引更多的人參與到社區(qū)交流中來。歡迎對文章內(nèi)容感興趣的朋友隨時聯(lián)系我。如果網(wǎng)上交流不暢,你可以把簡歷發(fā)到colaz1667@163.com。我能想到的最浪漫的事,就是一路陪你收集一點(diǎn)點(diǎn)笑點(diǎn),然后坐在工作站上慢慢聊。
1.《preact Preact:一個備胎的自我修養(yǎng)》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識,僅代表作者本人觀點(diǎn),與本網(wǎng)站無關(guān),侵刪請聯(lián)系頁腳下方聯(lián)系方式。
2.《preact Preact:一個備胎的自我修養(yǎng)》僅供讀者參考,本網(wǎng)站未對該內(nèi)容進(jìn)行證實(shí),對其原創(chuàng)性、真實(shí)性、完整性、及時性不作任何保證。
3.文章轉(zhuǎn)載時請保留本站內(nèi)容來源地址,http://f99ss.com/shehui/1595435.html