使用React也滿三年了,從剛剛會使用到逐漸探究其底層實(shí)現(xiàn)绍移,以便學(xué)習(xí)幾招奇技淫巧從而在自己的代碼中使用,寫出高效的代碼讥电。下面整理一些知識點(diǎn)蹂窖,算是React看書,使用恩敌,感悟的一些總結(jié):
- 函數(shù)式編程
- React事件系統(tǒng)
- 高階組件
- 組件性能優(yōu)化
- React源碼初探
- VirtualDOM 模型
1. 函數(shù)式編程
函數(shù)式編程是一種如何編寫程序的方法論瞬测,與之對應(yīng)的就是命令式編程。
以我自己的理解纠炮,函數(shù)式編程就是以函數(shù)為中心月趟,將大段過程拆成一個(gè)個(gè)函數(shù),組合嵌套使用恢口。這個(gè)思想在JavaScript中很常見孝宗。舉個(gè)阮一峰老師的例子:
我們有一個(gè)數(shù)學(xué)表達(dá)式:
(1 + 2) * 3 - 4
將上述表達(dá)式不假思索的轉(zhuǎn)換成代碼:
const a = 1 + 2;
const b = a * 3;
const c = b - 4;
以函數(shù)式編程思想:將運(yùn)算過程定義成不同的函數(shù),如下:
const result = substract(multiply(add(1, 2), 3), 4);
是不是感覺很高端但又一臉懵逼耕肩。沒錯(cuò)因妇,函數(shù)式編程在處理大段過程中就顯得很容易理解,但是簡單邏輯中就顯得復(fù)雜猿诸,因?yàn)榉庋b起來的函數(shù)需要時(shí)間去閱讀婚被。
對上述表達(dá)式進(jìn)行變形:
add(1,2).multiply(3).subtract(4);
是不是也很熟悉。函數(shù)式編程在JavaScript中應(yīng)用確實(shí)很普遍两芳。
目前最當(dāng)紅的Python摔寨、Ruby、Javascript怖辆,對函數(shù)式編程的支持都很強(qiáng)是复,就連老牌的面向?qū)ο蟮腏ava、面向過程的PHP竖螃,都忙不迭地加入對匿名函數(shù)的支持淑廊。越來越多的跡象表明,函數(shù)式編程已經(jīng)不再是學(xué)術(shù)界的最愛特咆,開始大踏步地在業(yè)界投入實(shí)用季惩。
也許繼"面向?qū)ο缶幊?之后录粱,"函數(shù)式編程"會成為下一個(gè)編程的主流范式(paradigm)。未來的程序員恐怕或多或少都必須懂一點(diǎn)画拾。
這里不做多介紹啥繁,有興趣可以看看:
2.React事件系統(tǒng)
React事件與DOM事件
React 基于 Virtual DOM 實(shí)現(xiàn)了一個(gè) SyntheticEvent (合成事件)層青抛,我們所定義的事件處理器會接收到一個(gè) SyntheticEvent 對象的實(shí)例旗闽,它完全符合 W3C 標(biāo)準(zhǔn),不會存在任何 IE 標(biāo)準(zhǔn)的兼容性問題蜜另。并且與原生的瀏覽器事件一樣擁有同樣的接口适室,同樣支持事件的冒泡機(jī)制,我們可以使用 stopPropagation() 和 preventDefault() 來中斷它举瑰。
所有事件都自動綁定到最外層上捣辆。如果需要訪問原生事件對象,可以使用 nativeEvent 屬性此迅。
使用React的時(shí)候都知道汽畴,React有一套自己的事件系統(tǒng),典型的特征就是元素綁定事件都要使用React提供的事件接口:
// in html
<button onclick="activateLasers()">
Activate Lasers
</button>
// in React
<button onClick={activateLasers}>
Activate Lasers
</button>
React的合成事件實(shí)際上是做了一層事件委托(事件代理):
它并不會把事件處理函數(shù)直接綁定到真實(shí)的節(jié)點(diǎn)上邮屁,而是把所有事件綁定到結(jié)構(gòu)的最外層整袁,使用一個(gè)統(tǒng)一的事件監(jiān)聽器,這個(gè)事件監(jiān)聽器上維持了一個(gè)映射來保存所有組件內(nèi)部的事件監(jiān)聽和處理函數(shù)佑吝。當(dāng)組件掛載或卸載時(shí),只是在這個(gè)統(tǒng)一的事件監(jiān)聽器上插入或刪除一些對象绳匀;當(dāng)事件發(fā)生時(shí)芋忿,首先被這個(gè)統(tǒng)一的事件監(jiān)聽器處理,然后在映射里找到真正的事件處理函數(shù)并調(diào)用疾棵。這樣做簡化了事件處理和回收機(jī)制戈钢,效率
也有很大提升。
也就是說React使用了一個(gè)事件代理是尔,所有事件綁定都只是事件代理保存了一個(gè)映射殉了,事件發(fā)生的時(shí)候,調(diào)用處理函數(shù)拟枚,并沒有真正的使用原生事件薪铜。我們來看一個(gè)例子:
componentDidMount () {
document.querySelector('#testEvent').addEventListener('click', (e)=>{
console.log('dom event');
console.log(e);
})
}
componentDidUnMount () {
document.querySelector('#testEvent').removeEventListener('click');
}
handleClick (e) {
console.log('react event');
console.log(e);
}
render () {
return (
<div>
<div onClick={::this.handleClick}>Test React Event</div>
<div id='testEvent'>Test dom Event</div>
</div>
);
}
這里有兩個(gè)div,使用React綁定事件和原生DOM事件恩溅,兩種事件綁定方法不同導(dǎo)致相同的效果隔箍,完全不同的原理。
使用原生DOM綁定打印的事件就是原生的脚乡,React事件打印出來的事件:
[圖片上傳失敗...(image-8bd58-1574906928855)]
可以看到是個(gè)Proxy對象蜒滩,里面有觸發(fā)事件的target和處理事件的handler,這就是React的合成事件。
另外如果在react中綁定原生事件俯艰,組件卸載的時(shí)候記得解除綁定捡遍,避免內(nèi)存泄漏。
React的合成事件還有一個(gè)優(yōu)點(diǎn)在于不需要處理瀏覽器事件兼容性竹握,方便操作稽莉。
原生事件分成三個(gè)部分:事件捕獲,目標(biāo)事件處理涩搓,事件冒泡污秆。IE9以下不支持事件捕獲,所以React沒有實(shí)現(xiàn)它昧甘,僅支持事件冒泡良拼。
有些事件React沒有實(shí)現(xiàn),window.resize事件充边。
所以庸推,請盡量避免在 React 中混用合成事件和原生 DOM 事件。因?yàn)閮烧呤遣煌氖录到y(tǒng)浇冰,阻止 React 事件冒泡的行為只能用于 React 合成事件系統(tǒng)中贬媒,且沒辦法阻止原生事件的冒泡。反之肘习,在原生事件中的阻止冒泡行為际乘,卻可以阻止 React 合成事件的傳播。
3.高階組件
高階組件是React中比較有特點(diǎn)的一類問題漂佩,高階組件(High Order Component)文章里單獨(dú)進(jìn)行了詳細(xì)介紹脖含。
這里只是補(bǔ)一張圖:組合式組件開發(fā)實(shí)踐
[圖片上傳失敗...(image-cad4f9-1574906928855)]
4.組件性能優(yōu)化
從過往的經(jīng)驗(yàn)與實(shí)踐中,我們都知道影響網(wǎng)頁性能最大的因素是瀏覽器的重繪(reflow)和重排版(repaint)投蝉。React 背后的 Virtual DOM 就是盡可能地減少瀏覽器的重繪與重排版养葵。
關(guān)于瀏覽器重繪和重排版問題,請看我之前的文章:瀏覽器渲染頁面過程與頁面優(yōu)化
這里要介紹的就是:
- 多使用純函數(shù):無依賴瘩缆;相同輸入相同輸出关拒;重復(fù)使用。
- PureComponent:本質(zhì)上講庸娱,PureComponent就是重寫了
shouldComponentUpdate
着绊,對nextProps
和nextState
與當(dāng)前state和props做淺比較,性能上優(yōu)化涌韩。 - Immutable:使用
Immutable
共享數(shù)據(jù)節(jié)點(diǎn)畔柔,節(jié)省渲染。 - key:列表渲染指定key臣樱,相同key不渲染靶擦;盡量不要使用index當(dāng)key腮考,最好是id。
- react-addons-pref:插件量化性能優(yōu)化效果玄捕。
對這塊有興趣的踩蔚,推薦幾篇文章:
5.React源碼初探
React項(xiàng)目目錄構(gòu)成如下圖:
[圖片上傳失敗...(image-abe94f-1574906928855)]
- addons:工具方法插件:
PureRenderMixin
、CSSTransitionGrouo
枚粘、Fragment
馅闽、LinkedStateMixin
。 - isomorphic:包含一系列同構(gòu)方法馍迄。
- shared:公用方法和常用方法福也。
- test:測試方法。
- core/tests:邊界錯(cuò)誤的測試用例攀圈。
- renderers:React的核心代碼暴凑,包含大部分功能實(shí)現(xiàn),因此進(jìn)行單獨(dú)分析赘来。
renderers包包含內(nèi)容:
[圖片上傳失敗...(image-f3fa3f-1574906928855)]
-
dom:包含client现喳,server和shared。
- client:包含DOM操作方法(findDOMNode犬辰,setInnerHTML嗦篱,setTextContent等)以及事件方法。這里的事件方法主要是一些非底層的實(shí)用性事件方法幌缝,
如事件監(jiān)聽(ReactEventListener)灸促、常用事件方法(TapEventPlugin、EnterLeaveEventPlugin)以及一些合成事件(SyntheticEvents
等)狮腿。 - server:主要包含服務(wù)端渲染的實(shí)現(xiàn)和方法(如 ReactServerRendering腿宰、ReactServerRenderingTransaction
等)。 - shared:包含文本組件(ReactDOMTextComponent)缘厢、標(biāo)簽組件(ReactDOMComponent)、
DOM 屬性操作(DOMProperty甩挫、DOMPropertyOperations)贴硫、CSS 屬性操作(CSSProperty、
CSSPropertyOperations)等伊者。
- client:包含DOM操作方法(findDOMNode犬辰,setInnerHTML嗦篱,setTextContent等)以及事件方法。這里的事件方法主要是一些非底層的實(shí)用性事件方法幌缝,
-
shared:包含event和reconciler英遭。
- event:包含一些更為底層的事件方法,如事件插件中心(EventPluginHub)亦渗、事件注冊
(EventPluginRegistry)挖诸、事件傳播(EventPropagators)以及一些事件通用方法。
React 自定義了一套通用事件的插件系統(tǒng)法精,該系統(tǒng)包含事件監(jiān)聽器多律、事件發(fā)射器痴突、事
件插件中心、點(diǎn)擊事件狼荞、進(jìn)/出事件辽装、簡單事件、合成事件以及一些事件方法相味。 - reconciler:稱為協(xié)調(diào)器拾积,它是最為核心的部分,包含 React 中自定義組件的實(shí)現(xiàn)
(ReactCompositeComponent)丰涉、組件生命周期機(jī)制拓巧、setState 機(jī)制(ReactUpdates、
ReactUpdateQueue)一死、DOM diff 算法(ReactMultiChild)等重要的特性方法肛度。
- event:包含一些更為底層的事件方法,如事件插件中心(EventPluginHub)亦渗、事件注冊
[圖片上傳失敗...(image-c78763-1574906928855)]
這里簡單介紹React目錄構(gòu)成以及每塊的功能,大致了解摘符,需要的時(shí)候找到對應(yīng)位置深入研究贤斜。
React 也能夠?qū)崿F(xiàn) Virtual DOM 的批處理更新,當(dāng)操作 Virtual DOM 時(shí), 不會馬上生成真實(shí)的DOM逛裤,而是會將一個(gè)事件循環(huán)(event loop)內(nèi)的兩次數(shù)據(jù)更新進(jìn)行合并瘩绒,這樣就使得 React 能夠在事件循環(huán)的結(jié)束之前完全不用操作真實(shí)的 DOM。
6.VirtualDOM 模型
VirtualDOM是React的一個(gè)核心带族,也是React一個(gè)著名的特點(diǎn)锁荔,之前我有篇文章對此有過簡單的介紹,以及如何簡單實(shí)現(xiàn)根據(jù)VirtualDOM渲染頁面:React學(xué)習(xí)報(bào)告蝙砌,可以做基本入門查看阳堕。
VirtualDOM與真實(shí)DOM的關(guān)系很簡單:
- 真實(shí)DOM可以理解為是xml格式存儲DOM,VirtualDOM可以理解為json格式的存儲DOM择克。
- 只需要存儲節(jié)點(diǎn)的關(guān)鍵信息:類型恬总,id,class肚邢,屬性壹堰,style,事件骡湖,嵌套關(guān)系等即可贱纠,按照一定的轉(zhuǎn)換規(guī)則將json轉(zhuǎn)成DOM。
- 流程關(guān)系:jsx語法->識別jsx語法生成VirtualDOM樹->根據(jù)渲染規(guī)則生成真實(shí)DOM->HTML响蕴。
Virtual DOM中的節(jié)點(diǎn)成為ReactNode谆焊,分成ReactELement,ReactFragment浦夷,ReactText辖试。ReactElement又分成ReactComponentElemnt和ReactDOMElement辜王。
下面是 ReactNode 中不同類型節(jié)點(diǎn)所需要的基礎(chǔ)元素:
type ReactNode = ReactElement | ReactFragment | ReactText;
type ReactElement = ReactComponentElement | ReactDOMElement;
type ReactDOMElement = {
type : string,
props : {
children : ReactNodeList,
className : string,
etc.
},
key : string | boolean | number | null,
ref : string | null
};
type ReactComponentElement<TProps> = {
type : ReactClass<TProps>,
props : TProps,
key : string | boolean | number | null,
ref : string | null
};
type ReactFragment = Array<ReactNode | ReactEmpty>;
type ReactNodeList = ReactNode | ReactEmpty;
type ReactText = string | number;
type ReactEmpty = null | undefined | boolean;
這里以DOM標(biāo)簽(ReactDOMComponent)為例,介紹VirtualDOM模型如何創(chuàng)建節(jié)點(diǎn):
屬性更新
當(dāng)執(zhí)行 mountComponent 方法時(shí)剃执,ReactDOMComponent 首先會生成標(biāo)記和標(biāo)簽誓禁,通過 this.createOpenTagMarkupAndPutListeners(transaction) 來處理 DOM 節(jié)點(diǎn)的屬性和事件。
- 如果節(jié)點(diǎn)綁定了事件肾档,則針對當(dāng)前的節(jié)點(diǎn)添加代理摹恰,調(diào)用
enqueuePutListener(this,propKey, propValue, transaction)
。 - 存在樣式的話怒见,樣式合并
Object.assign({}, props.style)
俗慈,然后通過CSSPropertyOperations.createMarkupForStyles(propValue, this)
創(chuàng)建樣式。 - 通過
DOMPropertyOperations.createMarkupForProperty(propKey, propValue)
創(chuàng)建屬性遣耍。 - 通過
DOMPropertyOperations.createMarkupForID(this._domID)
創(chuàng)建唯一標(biāo)識闺阱。
其實(shí),早有開發(fā)者向 React 官方提過問題舵变,建議去掉這個(gè)雞肋的屬性標(biāo)識(data-reactid)這終于在 React 15.0版本上實(shí)現(xiàn)了酣溃。據(jù)官方宣稱,去除 data-reactid 使得 React 性能有了 10% 的提升纪隙。
更新子節(jié)點(diǎn)
當(dāng)執(zhí)行 mountComponent 方法時(shí)赊豌,ReactDOMComponent 會通過 this._createContentMarkup(transaction, props, context) 來處理 DOM 節(jié)點(diǎn)的內(nèi)容。
先是刪除不需要的子節(jié)點(diǎn)和內(nèi)容绵咱。如果舊節(jié)點(diǎn)存在碘饼,而新節(jié)點(diǎn)不存在,說明當(dāng)前節(jié)點(diǎn)在更新后被刪除悲伶,此時(shí)執(zhí)行方法 this.updateChildren(null, transaction, context)艾恼;如果舊的內(nèi)容存在,而新的內(nèi)容不存在麸锉,說明當(dāng)前內(nèi)容在更新后被刪除钠绍,此時(shí)執(zhí)行方法 this.updateTextContent('')。
再是更新子節(jié)點(diǎn)和內(nèi)容花沉。如果新子節(jié)點(diǎn)存在五慈,則更新其子節(jié)點(diǎn),此時(shí)執(zhí)行方法 this.updateChildren(nextChildren,transaction, context)
主穗;如果新的內(nèi)容存在,則更新內(nèi)容毙芜,此時(shí)執(zhí)行方法 this.updateTextContent('' + nextContent)
忽媒。
當(dāng)卸載組件時(shí),ReactDOMComponent 會進(jìn)行一系列的操作腋粥,如卸載子節(jié)點(diǎn)晦雨、清除事件監(jiān)聽架曹、清空標(biāo)識等。
[圖片上傳失敗...(image-c05267-1574906928855)]