摘要:我們先來看下這個(gè)函數(shù)的一些神奇用法對(duì)于上述代碼,也就是函數(shù)來說返回值是。不管你第二個(gè)參數(shù)的函數(shù)返回值是幾維嵌套數(shù)組,函數(shù)都能幫你攤平到一維數(shù)組,并且每次遍歷后返回的數(shù)組中的元素個(gè)數(shù)代表了同一個(gè)節(jié)點(diǎn)需要復(fù)制幾次。
這是我的 React 源碼解讀課的第一篇文章,首先來說說為啥要寫這個(gè)系列文章:
現(xiàn)在工作中基本都用 React 了,由此想了解下內(nèi)部原理
市面上 Vue 的源碼解讀數(shù)不勝數(shù),但是反觀 React 相關(guān)的卻寥寥無幾,也是因?yàn)?React 源碼難度較高,因此我想來攻克這個(gè)難題
自己覺得看懂并不一定看懂了,寫出來讓讀者看懂才是真懂了,因此我要把我讀懂的東西寫出來
這個(gè)系列文章預(yù)計(jì)篇數(shù)會(huì)超過十篇,React 版本為 16.8.6,以下是本系列文章你必須需要注意的地方:
這是一門進(jìn)階課,如果涉及到你不清楚的內(nèi)容,請(qǐng)自行谷歌,另外最好具備 React 的開發(fā)能力
這是一門講源碼的課,只閱讀是不大可能真正讀懂的,需要輔以 Demo 和 Debug 才能真正理解代碼的用途
我 fork 了一份 16.8.6 版本的代碼,并且會(huì)為讀過的代碼加上詳細(xì)的中文注釋。等不及我文章的同學(xué)可以先行閱讀 我的倉庫,并且在閱讀本系列文章的時(shí)候也請(qǐng)跟著閱讀我注釋的代碼。因?yàn)榘姹静煌赡軙?huì)導(dǎo)致代碼不同,并且我不會(huì)在文章中貼上大段的代碼,只會(huì)對(duì)部分代碼做更詳細(xì)的解釋,其他的代碼可以跟著我的注釋閱讀
閱讀源碼最先遇到的問題會(huì)是不知道該從何開始,我這份代碼注釋可以幫助大家解決這個(gè)問題,你只需要跟著我的 commit 閱讀即可
不會(huì)對(duì)任何 DEV 環(huán)境下的代碼做解讀,不會(huì)對(duì)所有代碼進(jìn)行解讀,只會(huì)解讀核心功能(即使這樣也會(huì)是一個(gè)大工程)
最后再提及一遍,請(qǐng)務(wù)必文章和 代碼 相結(jié)合來看,為了篇幅考慮我不會(huì)將所有的代碼都貼上來,我拷貝的累,讀者看的也累
這篇文章內(nèi)容不會(huì)很難,先給大家熱個(gè)身,請(qǐng)大家打開 我的代碼 并定位到 react 文件夾下的 src,這個(gè)文件夾也就是 React 的入口文件夾了。
開始進(jìn)入正文前先說下這個(gè)系列中我的行文思路:1. 代碼盡量通過圖片展示,既美觀又方便閱讀,反正不需要大家。2. 文章中只會(huì)講我認(rèn)為重要或者有意思的代碼,對(duì)于其他代碼請(qǐng)自行閱讀我的倉庫,反正已經(jīng)注釋好代碼了。3. 對(duì)于流程長的函數(shù)調(diào)用會(huì)使用流程圖的方式來總結(jié)。4. 不會(huì)干巴巴的只講代碼,會(huì)結(jié)合實(shí)際來聊聊這些 API 能幫助我們解決什么問題。
文章相關(guān)資料React 16.8.6 源碼中文注釋,這個(gè)鏈接是文章的核心,文中的具體代碼及代碼行數(shù)都是依托于這個(gè)倉庫
render 流程(一)
render 流程(二)
React.createElement
大家在寫 React 代碼的時(shí)候肯定寫過 JSX,但是為什么一旦使用 JSX 就必須引入 React 呢?
這是因?yàn)槲覀兊?JSX 代碼會(huì)被 Babel 編譯為 React.createElement,不引入 React 的話就不能使用 React.createElement 了。
1// 上面的 JSX 會(huì)被編譯成這樣 React.createElement("div", { id: "1" }, "1")那么我們就先定位到 ReactElement.js 文件閱讀下 createElement 函數(shù)的實(shí)現(xiàn)
export function createElement(type, config, children) {}
首先 createElement 函數(shù)接收三個(gè)參數(shù),具體代表著什么相信大家可以通過上面 JSX 編譯出來的東西自行理解。
然后是對(duì)于 config 的一些處理:
這段代碼對(duì) ref 以及 key 做了個(gè)驗(yàn)證(對(duì)于這種代碼就無須閱讀內(nèi)部實(shí)現(xiàn),通過函數(shù)名就可以了解它想做的事情),然后遍歷 config 并把內(nèi)建的幾個(gè)屬性(比如 ref 和 key)剔除后丟到 props 對(duì)象中。
接下里是一段對(duì)于 children 的操作
首先把第二個(gè)參數(shù)之后的參數(shù)取出來,然后判斷長度是否大于一。大于一的話就代表有多個(gè) children,這時(shí)候 props.children 會(huì)是一個(gè)數(shù)組,否則的話只是一個(gè)對(duì)象。因此我們需要注意在對(duì) props.children 進(jìn)行遍歷的時(shí)候要注意它是否是數(shù)組,當(dāng)然你也可以利用 React.Children 中的 API,下文中也會(huì)對(duì) React.Children 中的 API 進(jìn)行講解。
最后就是返回了一個(gè) ReactElement 對(duì)象
內(nèi)部代碼很簡單,核心就是通過 $$typeof 來幫助我們識(shí)別這是一個(gè) ReactElement,后面我們可以看到很多這樣類似的類型。另外我們需要注意一點(diǎn)的是:通過 JSX寫的
代表著 ReactElement,APP 代表著 React Component。 以下是這一小節(jié)的流程圖內(nèi)容:
ReactBaseClasses
上文中講到了 APP 代表著 React Component,那么這一小節(jié)我們就來閱讀組件相關(guān)也就是 ReactBaseClasses.js 文件下的代碼。
其實(shí)在閱讀這部分源碼之前,我以為代碼會(huì)很復(fù)雜,可能包含了很多組件內(nèi)的邏輯,結(jié)果內(nèi)部代碼相當(dāng)簡單。這是因?yàn)?React 團(tuán)隊(duì)將復(fù)雜的邏輯全部丟在了 react-dom 文件夾中,你可以把 react-dom 看成是 React 和 UI 之間的膠水層,這層膠水可以兼容很多平臺(tái),比如 Web、RN、SSR 等等。
該文件包含兩個(gè)基本組件,分別為 Component 及 PureComponent,我們先來閱讀 Component 這部分的代碼。
構(gòu)造函數(shù) Component 中需要注意的兩點(diǎn)分別是 refs 和 updater,前者會(huì)在下文中專門介紹,后者是組件中相當(dāng)重要的一個(gè)屬性,我們可以發(fā)現(xiàn) setState 和 forceUpdate 都是調(diào)用了 updater 中的方法,但是 updater 是 react-dom 中的內(nèi)容,我們會(huì)在之后的文章中學(xué)習(xí)到這部分的內(nèi)容。
另外 ReactNoopUpdateQueue 也有一個(gè)多帶帶的文件,但是內(nèi)部的代碼看不看都無所謂,因?yàn)槎际怯糜趫?bào)警告的。
接下來我們來閱讀 PureComponent 中的代碼,其實(shí)這部分的代碼基本與 Component 一致
PureComponent 繼承自 Component,繼承方法使用了很典型的寄生組合式。
另外這兩部分代碼你可以發(fā)現(xiàn)每個(gè)組件都有一個(gè) isXXXX 屬性用來標(biāo)志自身屬于什么組件。
以上就是這部分的代碼,接下來的一小節(jié)我們將會(huì)學(xué)習(xí)到 refs 的一部分內(nèi)容。
Refsrefs 其實(shí)有好幾種方式可以創(chuàng)建:
字符串的方式,但是這種方式已經(jīng)不推薦使用
ref={el => this.el = el}
React.createRef
這一小節(jié)我們來學(xué)習(xí) React.createRef 相關(guān)的內(nèi)容,其余的兩種方式不在這篇文章的討論范圍之內(nèi),請(qǐng)先定位到 ReactCreateRef.js 文件。
內(nèi)部實(shí)現(xiàn)很簡單,如果我們想使用 ref,只需要取出其中的 current 對(duì)象即可。
另外對(duì)于函數(shù)組件來說,是不能使用 ref 的,如果你不知道原因的話可以直接閱讀 文檔。
當(dāng)然在之前也是有取巧的方式的,就是通過 props 的方式傳遞 ref,但是現(xiàn)在我們有了新的方式 forwardRef 去解決這個(gè)問題。
具體代碼見 forwardRef.js 文件,同樣內(nèi)部代碼還是很簡單
這部分代碼最重要的就是我們可以在參數(shù)中獲得 ref 了,因此我們?nèi)绻朐诤瘮?shù)組件中使用 ref 的話就可以把代碼寫成這樣:
const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton"> {props.children} button> ))
ReactChildren這一小節(jié)會(huì)是這篇文章中最復(fù)雜的一部分,可能需要自己寫個(gè) Demo 并且 Debug 一下才能真正理解源碼為什么要這樣實(shí)現(xiàn)。
首先大家需要定位到 ReactChildren.js 文件,這部分代碼中我只會(huì)介紹關(guān)于 mapChildren 函數(shù)相關(guān)的內(nèi)容,因?yàn)檫@部分代碼基本就貫穿了整個(gè)文件了。
如果你沒有使用過這個(gè) API,可以先自行閱讀 文檔。
對(duì)于 mapChildren 這個(gè)函數(shù)來說,通常會(huì)使用在組合組件設(shè)計(jì)模式上。如果你不清楚什么是組合組件的話,可以看下 Ant-design,它內(nèi)部大量使用了這種設(shè)計(jì)模式,比如說 Radio.Group、Radio.Button,另外這里也有篇 文檔 介紹了這種設(shè)計(jì)模式。
我們先來看下這個(gè)函數(shù)的一些神奇用法
React.Children.map(this.props.children, c => [[c, c]])
對(duì)于上述代碼,map 也就是 mapChildren 函數(shù)來說返回值是 [c, c, c, c]。不管你第二個(gè)參數(shù)的函數(shù)返回值是幾維嵌套數(shù)組,map 函數(shù)都能幫你攤平到一維數(shù)組,并且每次遍歷后返回的數(shù)組中的元素個(gè)數(shù)代表了同一個(gè)節(jié)點(diǎn)需要復(fù)制幾次。
如果文字描述有點(diǎn)難懂的話,就來看代碼吧:
<div> <span>1span> <span>2span> div>
對(duì)于上述代碼來說,通過 c => [[c, c]] 轉(zhuǎn)換以后就變成了
<span>1span> <span>1span> <span>2span> <span>2span>
接下里我們進(jìn)入正題,來看看 mapChildren 內(nèi)部到底是如何實(shí)現(xiàn)的。
這段代碼有意思的部分是引入了對(duì)象重用池的概念,分別對(duì)應(yīng) getPooledTraverseContext 和 releaseTraverseContext 中的代碼。當(dāng)然這個(gè)概念的用處其實(shí)很簡單,就是維護(hù)一個(gè)大小固定的對(duì)象重用池,每次從這個(gè)池子里取一個(gè)對(duì)象去賦值,用完了就將對(duì)象上的屬性置空然后丟回池子。維護(hù)這個(gè)池子的用意就是提高性能,畢竟頻繁創(chuàng)建銷毀一個(gè)有很多屬性的對(duì)象會(huì)消耗性能。
接下來我們來學(xué)習(xí) traverseAllChildrenImpl 中的代碼,這部分的代碼需要分為兩塊來講
這部分的代碼相對(duì)來說簡單點(diǎn),主體就是在判斷 children 的類型是什么。如果是可以渲染的節(jié)點(diǎn)的話,就直接調(diào)用 callback,另外你還可以發(fā)現(xiàn)在判斷的過程中,代碼中有使用到 $$typeof 去判斷的流程。這里的 callback 指的是 mapSingleChildIntoContext 函數(shù),這部分的內(nèi)容會(huì)在下文中說到。
這部分的代碼首先會(huì)判斷 children 是否為數(shù)組。如果為數(shù)組的話,就遍歷數(shù)組并把其中的每個(gè)元素都遞歸調(diào)用 traverseAllChildrenImpl,也就是說必須是單個(gè)可渲染節(jié)點(diǎn)才可以執(zhí)行上半部分代碼中的 callback。
如果不是數(shù)組的話,就看看 children 是否可以支持迭代,原理就是通過 obj[Symbol.iterator] 的方式去取迭代器,返回值如果是個(gè)函數(shù)的話就代表支持迭代,然后邏輯就和之前的一樣了。
講完了 traverseAllChildrenImpl 函數(shù),我們最后再來閱讀下 mapSingleChildIntoContext 函數(shù)中的實(shí)現(xiàn)。
bookKeeping 就是我們從對(duì)象池子里取出來的東西,然后調(diào)用 func 并且傳入節(jié)點(diǎn)(此時(shí)這個(gè)節(jié)點(diǎn)肯定是單個(gè)節(jié)點(diǎn)),此時(shí)的 func 代表著 React.mapChildren 中的第二個(gè)參數(shù)。
接下來就是判斷返回值類型的過程:如果是數(shù)組的話,還是回歸之前的代碼邏輯,注意這里傳入的 func 是 c => c,因?yàn)橐WC最終結(jié)果是被攤平的;如果不是數(shù)組的話,判斷返回值是否是一個(gè)有效的 Element,驗(yàn)證通過的話就 clone 一份并且替換掉 key,最后把返回值放入 result 中,result 其實(shí)也就是 mapChildren 的返回值。
至此,mapChildren 函數(shù)相關(guān)的內(nèi)容已經(jīng)解析完畢,還不怎么清楚的同學(xué)可以通過以下的流程圖再復(fù)習(xí)一遍。
其余內(nèi)容
前面幾小節(jié)的內(nèi)容已經(jīng)把 react 文件夾下大部分有意思的代碼都講完了,其他就剩余了一些邊邊角角的內(nèi)容。比如 memo、context、hooks、lazy,這部分代碼有興趣的可以直接自行閱讀,反正內(nèi)容都還是很簡單的,難的部分都在 react-dom 文件夾中。
其他文章列表render 流程(一)
最后閱讀源碼是一個(gè)很枯燥的過程,但是收益也是巨大的。如果你在閱讀的過程中有任何的問題,都?xì)g迎你在評(píng)論區(qū)與我交流,當(dāng)然你也可以在倉庫中提 Issus。
另外寫這系列是個(gè)很耗時(shí)的工程,需要維護(hù)代碼注釋,還得把文章寫得盡量讓讀者看懂,最后還得配上畫圖,如果你覺得文章看著還行,就請(qǐng)不要吝嗇你的點(diǎn)贊。
下一篇文章就會(huì)是 Fiber 相關(guān)的內(nèi)容,并且會(huì)分成幾篇文章來講解。
最后,覺得內(nèi)容有幫助可以關(guān)注下我的公眾號(hào) 「前端真好玩」咯,會(huì)有很多好東西等著你。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/7364.html
摘要:為了能夠更好的使用這個(gè)工具,今天就對(duì)它進(jìn)行一下源碼剖析。它內(nèi)部的關(guān)鍵代碼是在不指定的時(shí)候等于,這就意味著的源碼剖析到此結(jié)束,謝謝觀看當(dāng)然如果指定了剖析就還得繼續(xù)。好了,源碼剖析到此結(jié)束,謝謝觀看 React-Redux是用在連接React和Redux上的。如果你想同時(shí)用這兩個(gè)框架,那么React-Redux基本就是必須的了。為了能夠更好的使用這個(gè)工具,今天就對(duì)它進(jìn)行一下源碼剖析。 Pr...
摘要:大家可以看到是構(gòu)造函數(shù)構(gòu)造出來的,并且內(nèi)部有一個(gè)對(duì)象,這個(gè)對(duì)象是本文接下來要重點(diǎn)介紹的對(duì)象,接下來我們就來一窺究竟吧。在構(gòu)造函數(shù)內(nèi)部就進(jìn)行了一步操作,那就是創(chuàng)建了一個(gè)對(duì)象,并掛載到了上。下一篇文章還是流程相關(guān)的內(nèi)容。這是我的剖析 React 源碼的第二篇文章,如果你沒有閱讀過之前的文章,請(qǐng)務(wù)必先閱讀一下 第一篇文章 中提到的一些注意事項(xiàng),能幫助你更好地閱讀源碼。 文章相關(guān)資料 React ...
摘要:就是,如果你不了解這個(gè)的話可以閱讀下相關(guān)文檔,是應(yīng)用初始化時(shí)就會(huì)生成的一個(gè)變量,值也是,并且這個(gè)值不會(huì)在后期再被改變。這是我的剖析 React 源碼的第三篇文章,如果你沒有閱讀過之前的文章,請(qǐng)務(wù)必先閱讀一下 第一篇文章 中提到的一些注意事項(xiàng),能幫助你更好地閱讀源碼。 文章相關(guān)資料 React 16.8.6 源碼中文注釋,這個(gè)鏈接是文章的核心,文中的具體代碼及代碼行數(shù)都是依托于這個(gè)倉庫 熱身...
摘要:后面將會(huì)仔細(xì)分析的源碼實(shí)現(xiàn)。更新完成后,對(duì)中的每個(gè)元素執(zhí)行動(dòng)畫的邏輯,對(duì)中的每個(gè)元素執(zhí)行動(dòng)畫的邏輯。事實(shí)上,原因很簡單,事件在某些情況是不會(huì)被觸發(fā)。總結(jié)動(dòng)畫是組件初次后,才會(huì)被添加到的所有子元素上。參考資料官方文檔事件 過去一年,React 給整個(gè)前端界帶來了一種新的開發(fā)方式,我們拋棄了無所不能的 DOM 操作。對(duì)于 React 實(shí)現(xiàn)動(dòng)畫這個(gè)命題,DOM 操作已經(jīng)是一條死路,而 CSS...
摘要:目前,前端領(lǐng)域中勢(shì)頭正盛,使用者眾多卻少有能夠深入剖析內(nèi)部實(shí)現(xiàn)機(jī)制和原理。當(dāng)發(fā)現(xiàn)節(jié)點(diǎn)已經(jīng)不存在,則該節(jié)點(diǎn)及其子節(jié)點(diǎn)會(huì)被完全刪除掉,不會(huì)用于進(jìn)一步的比較。 目前,前端領(lǐng)域中 React 勢(shì)頭正盛,使用者眾多卻少有能夠深入剖析內(nèi)部實(shí)現(xiàn)機(jī)制和原理。本系列文章希望通過剖析 React 源碼,理解其內(nèi)部的實(shí)現(xiàn)原理,知其然更要知其所以然。 React diff 作為 Virtual DOM 的加速...
閱讀 1112·2021-11-16 11:45
閱讀 2761·2021-09-27 13:59
閱讀 1355·2021-08-31 09:38
閱讀 3185·2019-08-30 15:52
閱讀 1342·2019-08-29 13:46
閱讀 2116·2019-08-29 11:23
閱讀 1695·2019-08-26 13:47
閱讀 2548·2019-08-26 11:54