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