進擊React原始碼之磨刀試煉2
進擊React原始碼之磨刀試煉部分為原始碼解讀基礎部分,會包含多篇文章,本篇為第二篇,第一篇《進擊React原始碼之磨刀試煉1》入口(點選進入)。
初探Component與PureComponent
如果有沒用過PureComponent
或不瞭解的同學,可以看看這篇文章何時使用Component還是PureComponent?
猜猜元件內部如何實現?
Component(元件)作為React中最重要的概念,每當建立類元件都要繼承Component
或PureComponent
,在未開始看原始碼的時候,大家可以先跟自己談談對於Component
和PureComponent
的印象,不妨根據經驗猜一猜Component
先來寫個簡單的元件
class CompDemo extends PureComponent {
constructor(props) {
super(props);
this.state = {
msg: 'hello world'
}
}
componentDidMount() {
setTimeout(() => {
this.setState({
msg: 'Hello React'
});
},1000)
}
render() {
return (
<div className="CompDemo">
<div className="CompDemo__text">
{this.state.msg}
</div>
</div>
)
}
}
複製程式碼
通過這個簡單的元件,我們猜猜,Component
/PureComponent
元件內部可能幫我們處理了props
,state
,定義了生命週期函式,setState
,render
等很多功能。
原始碼實現
開啟packages/react/src/ReactBaseClasses.js
if(__DEV__)
語句,有興趣瞭解的同學可以翻閱原始碼研究。
Component
function Component(props,context,updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState,callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',);
this.updater.enqueueSetState(this,partialState,callback,'setState');
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this,'forceUpdate');
};
複製程式碼
以上就是Component相關的原始碼,它竟如此出奇的簡潔!字面來看懂它也很簡單,首先定義了Component
建構函式,之後在其原型鏈上設定了isReactComponent
(Component元件標誌)、setState
方法和forceUpdate
方法。
Component
建構函式可以接收三個引數,其中props
和context
我們大多數人應該都接觸過,在函式中還定義了this.refs
為一個空物件,但updater
就是一個比較陌生的東西了,在setState
和forceUpdate
方法中我們可以看到它的使用:
-
setState
並沒有具體實現更新state的方法,而是呼叫了updater
的enqueueSetState
,setState
接收兩個引數:partialState
就是我們要更新的state
內容,callback
可以讓我們在state
更新後做一些自定義的操作,this.updater.enqueueSetState
在這裡傳入了四個引數,我們可以猜到第一個為當前例項物件,第二個是我們更新的內容,第三個是傳入的callback
,最後一個是當前操作的名稱。這段程式碼上面invariant
的作用是判斷partialState
是否是物件、函式或者null
,如果不是則會給出提示。在這裡我們可以看出,setState
第一個引數不僅可以為Object
,也可以是個函式,大家在實際操作中可以嘗試使用。 -
forceUpdate
相比於setState
,只有callback
,同時在使用enqueueForceUpdate
時候也少傳遞了一個引數,其他引數跟setState
中呼叫保持一致。
這個updater.enqueueForceUpdate
來自ReactDom
,React
與ReactDom
是分開的兩個不同的內容,很多複雜的操作都被封裝在了ReactDom
中,因此React
才保持如此簡潔。React
在不同平臺(native和web)使用的都是相同的程式碼,但是不同平臺的DOM操作流程可能是不同的,因此將state
的更新操作通過物件方式傳遞過來,可以讓不同的平臺去自定義自己的操作邏輯,React
就可以專注於大體流程的實現。
PureComponent
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
function PureComponent(props,updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
Object.assign(pureComponentPrototype,Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
複製程式碼
看完Cpomponent
的內容,再看PureComponent
就很簡單了,單看PureComponent
的定義是與Component
是完全一樣的,這裡使用了寄生組合繼承的方式,讓PureComponent
繼承了Component,之後設定了isPureReactComponent
標誌為true。
如果有同學對JavaScript繼承不是很瞭解,這裡找了一篇掘金上的文章深入JavaScript繼承原理 大家可以點選進入檢視
Refs的用法與實現
ref的使用
通過ref
我們可以獲得元件內某個子節點的資訊病對其進行操作,ref的使用方式有三種:
class RefDemo extends PureComponent {
constructor() {
super()
this.objRef = React.createRef()
}
componentDidMount() {
setTimeout(() => {
this.refs.stringRef.textContent = "String ref content changed";
this.methodRef.textContent = "Method ref content changed";
this.objRef.current.textContent = "Object ref content changed";
},3000)
}
render() {
return (
<div className="RefDemo">
<div className="RefDemo__stringRef" ref="stringRef">this is string ref</div>
<div className="RefDemo__methodRef" ref={el => this.methodRef = el}>this is method ref</div>
<div className="RefDemo__objRef" ref={this.objRef}>this is object ref</div>
</div>
)
}
}
export default RefDemo;
複製程式碼
- string ref(不推薦,可能廢棄):通過字串方式設定ref,會在this.refs物件上掛在一個
key
為所設字串的屬性,用來表示該節點的例項物件。如果該節點為dom,則對應dom示例,如果是class component
則對應該元件例項物件,如果是function component
,則會出現錯誤,function component
沒有例項,但可以通過forward ref
來使用ref
。 - method ref:通過function來建立ref(筆者在之前實習工作中基本都是使用這種方式,非常好用)。
- 通過
createRef()
建立物件,預設建立的物件為{current: null}
,將其傳遞個某個節點,在元件渲染結束後會將此節點的例項物件掛在到current
上
createRef的實現
原始碼位置packages/react/src/ReactCreactRef.js
export function createRef(): RefObject {
const refObject = {
current: null,};
return refObject;
}
複製程式碼
它上方有段註釋an immutable object with a single mutable value
,告訴我們創建出來的物件具有單個可變值,但是這個物件是不可變的。在其內部跟我們上面說的一樣,建立了{current: null}
並將其返回。
forwardRef的使用
const FunctionComp = React.forwardRef((props,ref) => (
<div type="text" ref={ref}>Hello React</div>
))
class FnRefDemo extends PureComponent {
constructor() {
super();
this.ref = React.createRef();
}
componentDidMount() {
setTimeout(() => {
this.ref.current.textContent = "Changed"
},3000)
}
render() {
return (
<div className="RefDemo">
<FunctionComp ref={this.ref}/>
</div>
)
}
}
複製程式碼
forwardRef
的使用,可以讓Function Component
使用ref,傳遞引數時需要注意傳入第二個引數ref
forwardRef的實現
export default function forwardRef<Props,ElementType: React$ElementType>(
render: (props: Props,ref: React$Ref<ElementType>) => React$Node,) {
return {
$$typeof: REACT_FORWARD_REF_TYPE,render,};
}
複製程式碼
forwardRef
接收一個函式作為引數,這個函式就是我們的函式元件,它包含props
和ref
屬性,forwardRef
最終返回的是一個物件,這個物件包含兩個屬性:
-
$$typeof
:這個屬性看過上一篇文章的小夥伴應該還記得,它是標誌React Element型別的東西。 - render: 我們傳遞進來的函式元件。
這裡說明一下,儘管forwardRef
返回的物件中$$typeof
為REACT_FORWARD_REF_TYPE
,但是最終建立的ReactElement的$$typeof仍然是REACT_ELEMENT_TYPE
這裡文字描述有點繞,配合圖片來看文字會好點。
在上述forwardRef使用
的程式碼中建立的FunctionComp
是{$$typeof:REACT_FORWARD_REF_TYPE,render}
這個物件,在使用<FunctionComp ref={this.ref}/>
時,它的本質是React.createElement(FunctionComp,{ref: xxxx},null)
這樣的,此時FunctionComp
是我們傳進createElement
中的type
引數,createElement
返回的element
的$$typeof
仍然是REACT_ELEMENT_TYPE
;
ReactChildren的使用方法和實現
ReactChildren的使用
function ParentComp ({children}) {
return (
<div className="parent">
<div className="title">Parent Component</div>
<div className="content">
{children}
</div>
</div>
)
}
複製程式碼
這樣的程式碼大家平時用的應該多一點,在使用ParentComp
元件時候,可以在標籤中間寫一些內容,這些內容就是children。
來看看React.Children.map的使用
function ParentComp ({children}) {
return (
<div className="parent">
<div className="title">Parent Component</div>
<div className="content">
{React.Children.map(children,c => [c,c,[c]])}
</div>
</div>
)
}
class ChildrenDemo extends PureComponent{
constructor() {
super()
this.state = {}
}
render() {
return (
<div className="childrenDemo">
<ParentComp>
<div>child 1 content</div>
<div>child 2 content</div>
<div>child 3 content</div>
</ParentComp>
</div>
)
}
}
export default ChildrenDemo;
複製程式碼
我們在使用這個API的時候,傳遞了兩個引數,第一個是children
,大家應該比較熟悉,第二個是一個回撥函式,回撥函式傳入一個引數(代表children的一個元素),返回一個陣列(陣列不是一位陣列,裡面三個元素最後一個還是陣列),在結果中我們可以看到,這個API將我們返回的陣列平鋪為一層[c1,c1,c2,c3,c3],瀏覽器中顯示的也就如上圖所示。
有興趣的小夥伴可以嘗試閱讀官方檔案對於這個api的介紹
ReactChildren的實現
在react.js
中定義React
時候我們可以看到一段關於Children
的定義
Children: {
map,forEach,count,toArray,only,},複製程式碼
Children包含5個API,這裡我們先詳細討論map API。這一部分並不是很好懂,請大家看的時候一定要用心。
筆者讀這一部分也是費了很大的勁,然後用思維導圖軟體畫出了這個思維導圖+流程圖的東西(暫時就給它起名為思維流程圖,其實更流程一點,而不思維),畫得還是比較詳細的,所以就很大,小夥伴最好把這個圖下載下來放大看(可以配合原始碼,也可以配合下文),圖片地址user-gold-cdn.xitu.io/2019/8/21/1…
由於圖太小不清楚,下面也會分別截出每個函式的流程圖。
開啟packages/react/src/ReactChildren.js
,找到mapChildren
function mapChildren(children,func,context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children,result,null,context);
return result;
}
複製程式碼
這段程式碼短小精悍,給我們提供了直接使用的API。它內部邏輯也非常簡單,首先看看children
是否為null
,如果如果為null
就直接返回null
,如果不是,則定義result
(初始為空陣列)來存放結果,經過mapIntoWithKeyPrefixInternal
的一系列處理,得到結果。結果不管是null
還是result
,其實我們再寫程式碼的時候都遇到過,如果一個元件中間什麼都沒傳,結果就是null什麼都不會顯示,如果傳遞了一個<div>
那就顯示這個div
,如果傳遞了一組div
那就顯示這一組(此時就是children不為null的情況),最後顯示出來的東西也就是result
這個陣列。
這一系列處理就是什麼處理?
function mapIntoWithKeyPrefixInternal(children,array,prefix,context) {
let escapedPrefix = '';
if (prefix != null) {
escapedPrefix = escapeUserProvidedKey(prefix) + '/';
}
const traverseContext = getPooledTraverseContext(
array,escapedPrefix,);
traverseAllChildren(children,mapSingleChildIntoContext,traverseContext);
releaseTraverseContext(traverseContext);
}
複製程式碼
在進入這個函式的時候,一定要注意使用這個函式時候傳遞進來的引數究竟是哪幾個,不然後面傳遞次數稍微一多就會暈頭轉向。
從上一個函式跳過來的時候傳遞了5個引數,大家可以注意一下這五個引數代表的是什麼:
-
children
:我們再元件中間寫的JSX程式碼 -
result
: 最終處理完成存放結果的陣列 -
prefix
: 字首,這裡為null -
func
: 我們在演示使用的過程中傳入的第二個引數,是個回撥函式c => [c,[c]]
-
context
: 上下文物件
這個函式首先對prefix
字首字串做了個處理,處理完之後還是個字串。然後通過getPooledTraverseContext
函式從物件重用池
中拿出一個物件,說到這裡,我們就不得不打斷一下這個函式的講解,突然出現一個物件重用池
的概念,很多人會很懵逼,並且如果強制把這個函式解析完再繼續下一個,會讓很多讀者產生很多疑惑,不利於後面原始碼的理解。
暫時跳到getPooledTraverseContext
看看物件重用池
const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext(
mapResult,keyPrefix,mapFunction,mapContext,) {
if (traverseContextPool.length) {
const traverseContext = traverseContextPool.pop();
traverseContext.result = mapResult;
traverseContext.keyPrefix = keyPrefix;
traverseContext.func = mapFunction;
traverseContext.context = mapContext;
traverseContext.count = 0;
return traverseContext;
} else {
return {
result: mapResult,keyPrefix: keyPrefix,func: mapFunction,context: mapContext,count: 0,};
}
}
複製程式碼
首先看在使用getPooledTraverseContext
獲取物件的時候,傳遞了4個引數:
-
array
: 上個函式中對應的result
,表示最終返回結果的陣列 -
escapedPrefix
: 字首,一個字串,沒什麼好說的 -
func
: 我們使用API傳遞的回撥函式c=>[c,[c]]
-
context
: 上下文物件
然後我們看看它做了什麼,它去一個traverseContextPool
陣列(這個陣列預設為空陣列,最多存放10個元素)中嘗試pop
取出一個元素,如果能取出來的話,這個元素是一個物件,有5個屬性,這裡會把傳進來的4個引數儲存在這四個元素中,方便後面使用,另外一個屬性是個用來計數的計數器。如果沒取出來,就返回一個新物件,包含的也是這五個屬性。這裡要跟大家說說物件重用池
了。這個物件有5個屬性,如果每次使用這個物件都重新建立一個,那麼會有較大的建立物件開銷,為了節省這部分建立的開銷,我們可以在使用完這個物件之後,把它的5個屬性都置為空(count就是0了),然後扔回這個陣列(物件重用池
)中,後面要用的時候就直接從物件重用池
中拿出來,不必重新建立物件,增加開銷了。
再回到mapIntoWithKeyPrefixInternal
函式中繼續向下讀
通過上一步拿到一個帶有5個屬性的物件之後,繼續經過traverseAllChildren
函式的一系列處理,得到了最終的結果result
,其中具體內容太多下面再說,然後通過releaseTraverseContext
函式釋放了那個帶5個引數的物件。我們先來看看如何釋放的:
function releaseTraverseContext(traverseContext) {
traverseContext.result = null;
traverseContext.keyPrefix = null;
traverseContext.func = null;
traverseContext.context = null;
traverseContext.count = 0;
if (traverseContextPool.length < POOL_SIZE) {
traverseContextPool.push(traverseContext);
}
}
複製程式碼
這裡也跟我們上面說的物件重用池有所對應
,這裡先把這個物件的5個屬性清空,然後看看物件重用池是不是有空,有空的話就把這個清空的屬性放進去,方便下次使用,節省建立開銷。
traverseAllChildren和traverseAllChildrenImpl的實現
function traverseAllChildren(children,traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children,'',traverseContext);
}
複製程式碼
這個函式基本沒做什麼重要的事,僅僅判斷了children
是否為null
,如果是的話就返回0,不是的話就進行具體的處理。還是強調這裡傳遞的引數,一定要注意,看圖就可以了,就不用文字描述了。
重要的是traverseAllChildrenImpl
函式,這個函式有點長,這裡給大家分成了兩部分,可以分開看
function traverseAllChildrenImpl(
children,nameSoFar,traverseContext,) {
// 第一部分
const type = typeof children;
if (type === 'undefined' || type === 'boolean') {
children = null;
}
let invokeCallback = false;
if (children === null) {
invokeCallback = true;
} else {
switch (type) {
case 'string':
case 'number':
invokeCallback = true;
break;
case 'object':
switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
}
}
}
if (invokeCallback) {
callback(
traverseContext,children,nameSoFar === '' ? SEPARATOR + getComponentKey(children,0) : nameSoFar,);
return 1;
}
// 第二部分
let child;
let nextName;
let subtreeCount = 0; // Count of children found in the current subtree.
const nextNamePrefix =
nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child,i);
subtreeCount += traverseAllChildrenImpl(
child,nextName,);
}
} else {
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
const iterator = iteratorFn.call(children);
let step;
let ii = 0;
while (!(step = iterator.next()).done) {
child = step.value;
nextName = nextNamePrefix + getComponentKey(child,ii++);
subtreeCount += traverseAllChildrenImpl(
child,);
}
} else if (type === 'object') {
let addendum = '';
const childrenString = '' + children;
invariant(
false,'Objects are not valid as a React child (found: %s).%s',childrenString === '[object Object]'
? 'object with keys {' + Object.keys(children).join(',') + '}'
: childrenString,addendum,);
}
}
return subtreeCount;
}
複製程式碼
上面的流程圖說的很詳細了,大家可以參照來看原始碼。這裡就簡單說一下這個函式的兩部分分別作了什麼事。
第一部分是對children
型別進行了檢查(沒有檢查為Array或迭代器物件的情況),如果檢查children是合法的ReactElement就會進行callback
的呼叫,這裡一定要注意callback
傳進來的是誰,這裡是callback為mapSingleChildIntoContext
,一直讓大家關注傳參問題,就是怕大家看著看著就搞混了。
第二部分就是針對children
是陣列和迭代器物件的情況進行了處理(迭代器物件檢查的原理是obj[Symbol.iterator]
,比較簡單大家可以自己定位原始碼找一下具體實現),然後對他們進行遍歷,每個元素都重新執行traverseAllChildrenImpl
函式形成遞迴。
它其實只讓可渲染的單元素進行下一步callback
的呼叫,如果是陣列或迭代器,就進行遍歷。
最後一步callback => mapSingleChildIntoContext的實現
function mapSingleChildIntoContext(bookKeeping,child,childKey) {
const {result,context} = bookKeeping;
let mappedChild = func.call(context,bookKeeping.count++);
if (Array.isArray(mappedChild)) {
mapIntoWithKeyPrefixInternal(mappedChild,childKey,c => c);
} else if (mappedChild != null) {
if (isValidElement(mappedChild)) {
mappedChild = cloneAndReplaceKey(
mappedChild,// Keep both the (mapped) and old keys if they differ,just as
// traverseAllChildren used to do for objects as children
keyPrefix +
(mappedChild.key && (!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(mappedChild.key) + '/'
: '') +
childKey,);
}
result.push(mappedChild);
}
}
複製程式碼
這裡我們就用到了從物件重用池拿出來的物件
,那個物件作用其實就是利用那5個屬性幫我們儲存了一些需要使用的變數和函式,然後執行我們傳入的func
(c => [c,[c]]
),如果結果不是陣列而是元素並且不為null
就會直接儲存到result
結果中,如果是個陣列就會對它進行遍歷,從mapIntoWithKeyPrefixInternal
開始重新執行形成遞迴呼叫,直到最後將巢狀陣列中所有元素都拿出來放到result
中,這樣就形成了我們最初看到的那種效果,不管我們的回撥函式是多少層的陣列,最後都會變成一層。
小結
這裡文字性的小結就留給大家,給大家畫了一張總結性的流程圖(有參考yck大神的圖),但其實是根據自己看原始碼畫出來的並不是搬運的。
ReactChildren的其他方法
{
forEach,}
複製程式碼
對於這幾個方法,大家可以自行查看了,建議先瀏覽一遍forEach
,跟map
非常相似,但是比map
少了點東西。其他幾個都是四五行的程式碼,大家自己看看。裡面用到的函式我們上面都有講到。
小結
這篇文章跟大家一起讀了Component
、refs
和Children
相關的原始碼,最複雜的還是數Children
了,說實話,連看大神部落格,看原始碼、畫圖帶寫文章,花了七八個小時,其實內容跟大神們的文章比起來還是很不一樣的,如果基礎不是很好的同學,我感覺這裡會講的更詳細。
大家一起努力,明天的我們一定會感謝今天努力的自己。
原創不易,如果本篇文章對你有幫助,希望可以幫忙點個贊,有興趣也可以幫忙github點個star,感謝各位。本篇文章github地址