eact框架的出現(xiàn)赎懦,意味著前端進入了一個新的時代起胰。
作為后端久又,開始做前端的相關(guān)項目以來已經(jīng)有段時間,剛開始使用React覺得很爽效五,挺好用的嘛地消,不過做著做著發(fā)現(xiàn),理解react整個生態(tài)畏妖,理解整個Js的生態(tài)脉执,理解樣式,理解結(jié)構(gòu)前端才是最難的部分戒劫。不過這里我只想談?wù)勛约簩eact的一些了解和認知吧半夷,關(guān)于生態(tài)的東西我后面再聊。自己的感覺學(xué)習(xí)整個React的開發(fā)生態(tài)迅细,還是很陡峭的巫橄,React框架本身還是很好理解的,不過如果加上各種東西柔和在一起茵典,那真是挺煩躁的O婊弧!不過嘛敬尺,我們工程師就是為了搞清這些東西而存在的C赌帷鸯隅!
之前在在‘認知Web前端’中豺总,我覺得現(xiàn)代的Web前端的發(fā)展是分分合合,到了React我覺是首次實現(xiàn)了分和合的高境界却妨。所謂的分是組件化的思維蜻直,所謂合是前端技術(shù)棧的開發(fā)方式之合盯质。組件化的思維其實很早就出現(xiàn),但是沒有很好的實現(xiàn)概而,知道React的出現(xiàn)呼巷,把其推向的高潮。而與此同時迎著組件化的浪潮赎瑰,我們的開發(fā)方式也是把原來的html王悍、css、js全都柔和在了一次開發(fā)餐曼,組件作為單體是合压储,作為一個更大的整體的一部分又是分鲜漩。而這使得復(fù)用性、開發(fā)效率大大提升集惋。
這里是我使用學(xué)習(xí)React框架孕似,寫前端有一段時間了,參考各種文章以來的一些總結(jié)吧刮刑。整個是比較粗粒度了喉祭,后面我想對每一個有意思的部分都單獨寫一篇文章來詳細深入了解。不斷去理解深入實踐雷绢,不斷的更新這里的認知泛烙。
Web前端的主要開發(fā)技術(shù)HTML、CSS翘紊、JS三大金剛胶惰。其中只有JS是一個圖靈完備的編程語言,這也使得只有JS能夠更加靈活霞溪,能夠解決所有的計算問題。為什么這里要提一下這個呢中捆?
因為我覺得這就是React的理念鸯匹,把JS作為主體,H5泄伪、CSS作為輔助殴蓬,使用JS來控制構(gòu)建HTML、CSS蟋滴。并且React也做到了這一點染厅,而且隨著整個生態(tài)的擴展,這種趨勢越發(fā)明顯津函。
在React的官網(wǎng)上它把自己定位為:
正是因為這一理念肖粮,讓React變得無比強大。我們不是在h5中嵌入JS來控制它尔苦,而是我們用JS來完全控制整個繪制涩馆、交互的一切,而HTML和CSS變成了JS的一個附屬品允坚。原來JS魂那、CSS不過是HTML的一個附屬品。整個關(guān)系鏈路發(fā)生了反轉(zhuǎn)式的變化稠项。
有一些基本的概念我們首先需要了解一下涯雅,這些概念在我看來是為我自身構(gòu)建了一個內(nèi)在的模型去理解React背后的設(shè)計原理,這里許多的概念都是可以水平遷移的展运,了解這些對了解相似框架活逆,以及背后的思考極為重要精刷。當這些概念內(nèi)化到我們內(nèi)心的時候,就形成了一張圖划乖,來龍去脈贬养,即使是對我們書寫代碼、架構(gòu)React應(yīng)用也有著極好的作用琴庵,也對于我們理解前端的一些原理有著很好的作用误算。
UI到底是什么?我們在屏幕上所看到的到底是什么迷殿?
React團隊給到的答案是儿礼,我們所看到的UI,是背后數(shù)據(jù)的一個映射庆寺;不同的數(shù)據(jù)反映不同的UI展現(xiàn)蚊夫。
這個答案簡單但是極為重要。
抽象的意義是:我們不可能通過一個組件就去構(gòu)建一個應(yīng)用懦尝,而是通過把React應(yīng)用分解成一個個復(fù)用的組件來去組合知纷。組件化的過程其實就是抽象的過程,比如理解業(yè)務(wù)陵霉,構(gòu)建通用的業(yè)務(wù)組件琅轧,這是一種抽象的能力。比如我們平時用的Ant Design的組件踊挠,本身這個設(shè)計規(guī)范就是一種抽象能力的體現(xiàn)乍桂,抽象出真正可以長期復(fù)用的組件出來。
這種抽象能力使我們這些程序員非常重要的一個能力效床,以前很多時候更多的在后端中會提到抽象睹酌,因為后端是看不見摸不著的,所以我們更多的去強調(diào)剩檀,但是現(xiàn)在作為前端憋沿,我們也需要這種能力。組件化的本質(zhì)是一種抽象的能力沪猴,這種能力可大可小卤妒,小的我們在一個項目中抽象一些業(yè)務(wù)組件,大的就像做一個Ant Design一樣的基礎(chǔ)組件庫字币,更大的可能是更加抽象的組件则披,比如React Motion這類的。這種抽象的能力會伴隨著我們的職業(yè)生涯洗出,這也是需要我不斷精進的地方士复。
在后端領(lǐng)域就一直有組合還是繼承的爭論,什么時候用組合?什么時候用繼承阱洪?不過在React里面的Composition的概念和后端是有一些不同的便贵,繼承的概念比較相似。我也是剛開始通過和后端的概念的類比冗荸,比較去理解的承璃。在React官方的最佳實踐中其實是不推薦使用繼承的,這也是我踩得坑蚌本,之前因為一些后端的思路盔粹,在項目中用了大量的繼承,后來發(fā)現(xiàn)這樣是不可行的程癌。這也是提醒了自己舷嗡,對于不同的領(lǐng)域知識,即使有的概念相似嵌莉,但是也不能生搬硬套去應(yīng)用相同的思路进萄,可以參考幫助理解,但是如果完全使用后端的思路去寫前端锐峭,真的會有大麻煩的中鼠。
每一個組件實例都會在它的內(nèi)部保存一份狀態(tài),狀態(tài)其實是組件內(nèi)部的數(shù)據(jù)沿癞,這些數(shù)據(jù)通常用來控制兜蠕、存儲一些元素的屬性值。比如一個輸入框抛寝,輸入框用戶輸入的值我們可以保存在狀態(tài)中。
狀態(tài)的更改會伴隨著一次更新曙旭,通常我們使用setState來更改狀態(tài)數(shù)據(jù)盗舰,數(shù)據(jù)的變化最終會導(dǎo)致組件的重新渲染(或局部重新渲染)。
每一個我們寫的React組件其實是一個function桂躏,當我們用高級語法寫的時候會忽略這么一個概念钻趋,最后react是在執(zhí)行一個個的function。這就遇到一個問題就是如果fuction的入?yún)⑾嗤料埃敲雌鋵峟unction的結(jié)果都是相同的蛮位,每次不斷的執(zhí)行function其實是有消耗的,所以在React的內(nèi)部會有一個機制鳞绕,對于反復(fù)相同的function失仁,入?yún)⑾嗤敲淳椭粓?zhí)行一次,后面直接拿結(jié)果就好了们何。
這就好像我們把1*1的結(jié)果直接保存萄焦,而不是每次都去計算一個1*1然后得到結(jié)果。這種優(yōu)化方式還是會經(jīng)常碰到的。
不知道怎么樣能夠很好的翻譯這個詞拂封,所謂的Reconciliation就是React所使用的算法用來區(qū)分一棵樹和另外一棵樹之間的區(qū)別茬射,找到哪些部分需要更新。Reconciliation是這種算法的名稱冒签,就我所知在抛,React15及之前和React16分別有兩個不同的實現(xiàn),這個后面我會講到萧恕。
調(diào)度安排的作用其實就是去安排一系列的工作如何執(zhí)行刚梭、何時執(zhí)行。
React通過一系列的計算廊鸥,最后的目的是渲染成Web頁面也就是一棵DOM樹望浩,然后有因為一些比如我們的操作關(guān)閉了頁面等等,最后整個DOM樹銷毀了惰说。這樣一個從計算到渲染再到銷毀的過程磨德,就好像是一個生命從出生到死亡的生命歷程。這個就是一個生命周期吆视,有了生命周期典挑,我們就可以更好去控制React在不同的階段的操作。
那么在整個過程中啦吧,React是一步一步的做的您觉,就好像是一個管道,中間會有很多步授滓,那么React在中間的許多步都埋了一個一個的鉤子琳水,這一個個的鉤子就是React暴露給我們能夠在生命周期內(nèi)自定義的部分。下面列舉了React提供的生命周期的函數(shù):
componentWillMount
componentDidMount
componentWillReceiveProps
getSnapshotBeforeUpdate
shouldComponentUpdate
componentWillUpdate
componentDidUpdate
render
componentWillUnmount
componentDidCatch
以上在React15及之前的版本都是可用的般堆,但是React16之后有些生命周期被移除了在孝。
可以先參考這里的文檔了解詳情:https://reactjs.org/docs/react-component.html
JSX是React團隊創(chuàng)建的一種語法糖吧,方便我們開發(fā)人員可以在JS里面書寫HTML形式的Tag淮摔。它只是開發(fā)階段的過渡形式私沮,在編譯之后會轉(zhuǎn)換成一個個的Function。你如我們可以這樣:
consthelloWorld=
Hello World
;我們可以對比一下不用JSX和橙。// 上面的編譯后就自動轉(zhuǎn)換為下面的了仔燕,方便很多!consthellWorld=React.createElement('p',null,'Hello World');有了JSX之后變得非常的直觀魔招,上面的例子晰搀,如果沒有JSX那么真的開發(fā)起來很痛苦啊0彀摺厕隧!
在React最最常提到的兩個概念就是VDOM樹,以及Diffing算法。
Virtual DOM直譯過來就是虛擬DOM樹吁讨,這個概念更多的是一個編程的模式的存在髓迎。所謂的虛擬DOM樹就是抽象真正的DOM,比如一個div的dom建丧,那么在會抽象一個div的虛擬dom排龄,這個dom里面包含了真正dom元素的引用、類型等等參數(shù)翎朱,然后在內(nèi)存中構(gòu)建一個和真實的dom樹一樣的結(jié)構(gòu)的樹橄维。正因為做了這么一層抽象,React就可以先不用直接操作dom了拴曲,而是在內(nèi)存中先構(gòu)建一個必要的虛擬dom樹争舞,然后做一系列的運算,等做完后澈灼,在把最后的計算的結(jié)果竞川,再繪制成真正的DOM樹。這樣可以大大減少DOM操作的次數(shù)叁熔,提高性能委乌。
我們知道同一時間只有一個完整的dom樹,當我們做數(shù)據(jù)變動的時候荣回,我們就是在更改dom樹的結(jié)構(gòu)遭贸,最粗暴的方法的全量替換,那么這肯定是不可取的心软,這就提到了React團隊使用的Diffing算法壕吹,稱為Reconciliation。整個算法做的事情就是計算出當前狀態(tài)的dom樹和下一個狀態(tài)的dom的區(qū)別删铃,從而重新渲染的時候只渲染更改的部分耳贬。比如下圖示意圖,我們需要變更的部分其實就是右邊的分支多了一個元素泳姐。
這個算法的本質(zhì)是尋找兩顆樹的異同部分,然后把一棵樹變成另外一顆樹暂吉。實現(xiàn)算法的過程其實是一個大的遞歸胖秒,去遍歷所有的元素,為了是算法性能慕的,React做了兩個假設(shè):
兩個不同類型的元素會產(chǎn)生兩顆不同的樹
通過開發(fā)人員手動的設(shè)定唯一的key值來標示子元素的不變性
通過這兩個假設(shè)阎肝,通過一次遞歸就可以計算出下一棵樹其復(fù)雜度控制在O(n).
首先是比較組件根元素的類型,如果不同直接刪除原來元素直接替換肮街。比如把div改為了img风题,把a改為了div等等。這個過程是把老的元素刪除,然后新建新的元素插入沛硅。
如果組件根元素類型相同眼刃,那么比較新老元素的屬性,并把新的屬性更新上去摇肌。
接著遍歷組件的根元素的子元素擂红,看是否有唯一的key,如果有唯一的key围小,react就知道哪些元素是新增昵骤,哪些是已有的,那么就沒比較重新渲染子元素了肯适,如有有新增就直接插入就可以了变秦。
算法不難理解,了解了這些機制后框舔,我們就知道平時寫react代碼的時候蹦玫,可以有針對性,如何寫出搞性能的React代碼雨饺。
React最后更具算法得出的變更钳垮,最后轉(zhuǎn)換成一些列直接調(diào)用了原生的dom操作。
很明顯這里產(chǎn)生了幾個注意點
盡量保證組件根元素不要有變化
組件根元素內(nèi)的子元素保證有唯一的Key
通過上述的算法额港,我們就知道了到底是哪一部分變化了饺窿,這就是這個Reconciliation,也就是diffing算法做的事情移斩。
那么接下來的一步就是render了肚医,把變化的部分變成一系列真實的DOM操作。?也正是這種兩步走的方式向瓷,使得React可以做到跨平臺肠套,對于IOS、android平臺猖任,重寫render部分你稚。
React Fiber的出現(xiàn)是為了解決上述的算法的問題而出現(xiàn)的,基本上是對于React核心算法的一次重寫朱躺。這個算法要比上述的diffing算法要復(fù)雜很多刁赖。在React 16版本之后已經(jīng)實現(xiàn)了對核心算法的重寫,所以React 15和React 16其實是一個分水嶺了长搀,雖然在使用的過程中我們感知不多宇弛,但是巨大的改變已經(jīng)發(fā)生。這里講講自己的一些理解吧源请。
React Fiber的出現(xiàn)為React打開了一個新的未來枪芒。和上一章描述的算法有本質(zhì)的區(qū)別彻况。
讓我們用白話的方式重新在走一下上一章的Reconciliation算法。首先比如我們創(chuàng)建一個React App舅踪,如下:
Hello World<Title></Page></App>
那么首先第一次創(chuàng)建的時候纽甘,React會把它轉(zhuǎn)換為Virtual DOM,然后渲染到可能是網(wǎng)頁也可能是native的app上硫朦。接下來的改動就是一次次的更新了贷腕,當這個app改動的時候,React會重新創(chuàng)建一棵Virtual DOM樹咬展,然后比較兩棵樹這件的差異泽裳,至此無關(guān)任何平臺。如果我們是在網(wǎng)頁上使用需要渲染為DOM即使是一個小小的改動破婆,那么React會遍歷整個虛擬樹涮总,找出不同處,比如更新一個屬性祷舀,改變一個樣式又或者刪除一個dom節(jié)點等等瀑梗。這樣一個過程就是React 15及以前的Reconciliation算法,簡單有效裳扯。這個過程在任何時候都會反復(fù)的進行抛丽,不管我們的瀏覽器是否變慢了,或者渲染的是否在屏幕上等等饰豺,任何事情都無法阻止這個渲染的流程亿鲜。
問題到底處在哪里?如果沒有問題React團隊也就不需要去做這么大的改動了冤吨。
通過上述的描述蒿柳,React的整個重diffing到更新的整個過程是自動進行的并且無法被停止的,而這就是問題所在漩蟆。當我們應(yīng)用很小的時候垒探,其實沒什么問題,即使是有很大的更新也不會多到哪里去怠李,也不會發(fā)現(xiàn)有什么性能問題圾叼,但是如果我們應(yīng)用開始變得很大很復(fù)雜,那么每次React這么一個渲染的流程會導(dǎo)致性能下降捺癞,React需要更多的時間來計算夷蚊,從而導(dǎo)致失幀的現(xiàn)象,拉長了某一幀的時間翘簇,導(dǎo)致每秒的幀率不均衡撬码。一般要保證體驗的良好儿倒,保持每秒60幀是比較好的版保,也就是大約每一幀的16ms呜笑,并且要保持這種頻率,而不是一會兒40幀一會兒60幀彻犁,如果是這樣對用戶來說是不能接受的叫胁,尤其是有動畫等效果的時候更是如此,失幀帶來的體驗下降是巨大的汞幢。也就是整個瀏覽器頁面的計算量驼鹅,我們需要保證每一幀在16ms內(nèi)完成。我們會發(fā)現(xiàn):
1.其實在UI界面中森篷,我們并不需要每次有更新就執(zhí)行整個渲染流程输钩。
2.不同的更新渲染,其實是有優(yōu)先級的仲智,比如動畫买乃、用戶的交互等我們可能希望執(zhí)行更快,比起更新一個數(shù)據(jù)钓辆。
如何達到這種效果呢剪验?
通過上述的場景描述,React Fiber就是為了解決這些事情前联,它把不同的更新渲染操作分解為一個個小的任務(wù)塊功戚,他們有優(yōu)先級、性能要求等等似嗤,然后React就可以調(diào)度安排這些任務(wù)啸臀,不再一次性不停的執(zhí)行,而是把不同的任務(wù)根據(jù)不同的優(yōu)先級双谆、性能等把這些任務(wù)均勻的分布在每一幀里面壳咕,從而使客戶體驗更加順滑。
之前在其他文章中看到一個比喻來形容React Fiber顽馋。我們是Renderer程序員谓厘,然后有個Reconciler產(chǎn)品經(jīng)理,本來呢寸谜,產(chǎn)品經(jīng)理提需求比如做一個hotfix竟稳、新功能,Renderer程序員沒法拒絕只能做熊痴,即使現(xiàn)在手頭上已經(jīng)有很多事情他爸,這就導(dǎo)致完成需求需要更長時間,業(yè)務(wù)方就不滿意了果善。那么有了React Fiber之后呢诊笤,我們就可以把hotfix、新需求做優(yōu)先級安排的去做巾陕,這樣各方都會比較滿意讨跟。
這里就不具體展開了纪他,后面在通過另一篇文章來詳細闡述。