1. JSX到JavaScript的轉換
<div id="div" key="key"><span>1</span><span>2</span>
</div>
React.createElement("div", // 大寫開頭會當做原生dom標簽的字符串,而組件使用大寫開頭時,這里會成為變量引用{ id: "div", key: "key" },React.createElement("span", null, "1"),React.createElement("span", null, "2")
);
createElement(type, config, children) {// 從用戶傳入的 config 中把4種內置的屬性 key、ref、self、source 單獨挑出來,config 中剩余的參數作為用戶的 props。// 然后返回使用 ReactElement 創建的 element 對象。
return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, );
}
2. ReactElement
const ReactElement = function(type, key, ref, self, source, owner, props) {const element = {// This tag allows us to uniquely identify this as a React Element$$typeof: REACT_ELEMENT_TYPE,// Built-in properties that belong on the elementtype: type, // 可能是原生dom標簽字符串如'span',也可能是一個class類(class組件),還可能是一個function(函數式組件),也可能是 forwardRef 返回的對象,或者 symbol 標記等 key: key,ref: ref,props: props,// Record the component responsible for creating this element._owner: owner,};if (__DEV__) {// 略}return element;
};
React.createElement("div",{ id: "div", key: "key" },React.createElement("span", null, "1"),React.createElement("span", null, "2")
);// 于是最初的 jsx 經過 React.createElement() 后成為了下面的對象樹,
// 也是函數式組件返回的東西,也是 class 組件組件 render() 方法返回的東西
element = {$$typeof: REACT_ELEMENT_TYPE,type: type,key: key,ref: ref,props: {userProps1: userProps1,// ... 用戶其他的自定義propschildren: [{$$typeof: REACT_ELEMENT_TYPE,type: type,key: key,ref: ref,props: props,_owner: owner,}, {$$typeof: REACT_ELEMENT_TYPE,type: type,key: key,ref: ref,props: props,_owner: owner,}]},_owner: owner,
};
3. 基類 React.Component
Component 類可能不是想象中那樣用于渲染子組件什么的,只是做一些綁定工作:
function Component(props, context, updater) {this.props = props;this.context = context;// If a component has string refs, we will assign a different object later.this.refs = emptyObject;// We initialize the default updater but the real one gets injected by the// renderer.this.updater = updater || ReactNoopUpdateQueue;
}// PureComponent 繼承了 Component
function PureComponent(props, context, updater) {this.props = props;this.context = context;// If a component has string refs, we will assign a different object later.this.refs = emptyObject;this.updater = updater || ReactNoopUpdateQueue;
}
// pureComponentPrototype.isPureReactComponent = true;// 常規的 setState,這里只是入隊列
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');
};// 可主動強制更新,這里也只是入隊列
// enqueueForceUpdate 內部實現了 ”重載”
Component.prototype.forceUpdate = function(callback) {this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
4. createRef & ref
ref 用于獲取 dom 節點或者 class component 的實例。
有三種用法,第一種 string 方法不推薦使用,后面在 React 17 中應該會被廢棄:
import React from 'react'export default class RefDemo extends React.Component {constructor() {super()this.objRef = React.createRef()// { current: null }}componentDidMount() {setTimeout(() => {this.refs.stringRef.textContent = 'string ref got'this.methodRef.textContent = 'method ref got'this.objRef.current.textContent = 'obj ref got'}, 1000)}render() {return (<><p ref="stringRef">span1</p><p ref={ele => (this.methodRef = ele)}>span3</p><p ref={this.objRef}>span3</p></>)}
}// export default () => {
// return <div>Ref</div>
// }
Object.seal() 密封,阻止目標對象上增刪屬性,并且關閉所有屬性的 configurable,但仍可以修改現有的、是 writable 的屬性。
Object.freeze() 凍結,比密封更嚴格,會將 writable 也關閉,意味著現有屬性“不可以”修改。但如果屬性本身是個引用類型,比如 const object = {a: 1, b: []},那么即使凍結 object 后,object.b.push(666) 也是可以的。
另外,凍結也會凍結目標對象上的 prototype 原型對象。
createRef 源碼:
import type {RefObject} from 'shared/ReactTypes';// an immutable object with a single mutable value
export function createRef(): RefObject {const refObject = {current: null,};if (__DEV__) {Object.seal(refObject);}return refObject;
}
5. forwardRef
字面意思,轉發 ref ?為什么 ref 需要轉發,不能像上面那樣直接使用?
dom 節點或者 class component,是可以由 react 在組件渲染之后把實例綁定(或提供)至我們指定的“容器”中,之后我們就可以從“容器”中引用剛才的實例,進行想要的操作。
React.createRef() 返回的就是 { current: null },這個對象就可以理解為一個“容器”,我們以在 jsx 上聲明的方式,提供給 react,渲染之后返回我們想要的實例引用。

然而 function 函數式組件并沒有實例,就是個函數。所以外部用戶無差別地嘗試為組件提供 ref “容器”希望回頭能拿到實例時,如果遇到 function 組件,則會失敗、報錯。而這種報錯可以通過轉發 ref 來避免,因為 function 組件沒有所謂的實例,但內部至少返回了 dom 或者另外的 class 組件吧,所以把 ref 轉發給它們即可。
即外部用戶最終拿到的實例引用,其實是函數式組件內層的實例。
自己的組件可能知道報錯,不會去直接試圖獲取 function 組件的 ref,但如果是作為第三方組件庫提供給其他 react 用戶來調用,則要使用 forwardRef 來轉發用戶的訴求,并實現他們。
import React from 'react';class App extends React.Component {render() {return <div>div</div>;}
}const TargetComponent = React.forwardRef((props, ref) => (<input type="text" ref={ref}/>
));export default class Comp extends React.Component {constructor() {super();this.ref = React.createRef();this.ref2 = React.createRef();}componentDidMount() {// 雖然還是聲明在 TargetComponent 函數組件上,但最終拿到了有效的實例引用,即內部的 dom 節點this.ref.current.value = 'ref get input';console.log('this.ref', this.ref);console.log('this.ref2', this.ref2);}render() {return <><TargetComponent ref={this.ref}/><App ref={this.ref2} /></>;}
}
React.forwardRef() 源碼:
export default function forwardRef<Props, ElementType: React$ElementType>(render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {if (__DEV__) {// 用戶不按規定的使用會拋出異常信息}return {$$typeof: REACT_FORWARD_REF_TYPE,render,};
}
可以看到向 forwardRef 傳入的 render 函數至少沒在這里被調用,只是用對象包了一層,并增加了一個 $$typeof
屬性,值是個 symbol。所以上面例子中聲明實際等價于:
const TargetComponent = {$$typeof: REACT_FORWARD_REF_TYPE,render: (props, ref) => (<input type="text" ref={ref}/>)
};// 經過 React.createElement() 創建出的 element:
const element = {// This tag allows us to uniquely identify this as a React Element$$typeof: REACT_ELEMENT_TYPE,type: TargetComponent,key: null,ref: React.createRef(),props: {},// Record the component responsible for creating this element._owner: owner,
};
6. Context
使用 context 也有兩種方式,childContextType 和 createContext。
childContextType 是老的方式,在將來的 React 17 中應該會被廢棄,所以優先使用 createContext。
import React from 'react'const MyContext = React.createContext('default');
const { Provider, Consumer } = MyContext;class Parent extends React.Component {state = {newContext: '123',};render() {return (<><div><label>newContext:</label><inputtype="text"value={this.state.newContext}onChange={e => this.setState({ newContext: e.target.value })}/></div><Provider value={this.state.newContext}>{this.props.children}</Provider></>)}
}class Parent2 extends React.Component {render() {return this.props.children}
}function Child1(props, context) {console.log(MyContext);console.log(context);return <Consumer>{value => <p>newContext: {value}</p>}</Consumer>
}export default () => (<Parent><Parent2><Child1 /></Parent2></Parent>
);
React.createContext() 源碼:
返回的 context 對象中有 $$typeof: REACT_CONTEXT_TYPE
,且有 Provider 和 Consumer,Provider 只是用對象包了一下原 context,添加了 $$typeof: REACT_PROVIDER_TYPE
屬性;而 Consumer 壓根就是引用原 context。有點俄羅斯套娃的感覺,能想到的就是 $$typeof
會作為 react 更新時判斷不同類型的依據。而套娃的操作,可能就是為了方便操作 context,引來引去總能找到那個 context。
export function createContext<T>(defaultValue: T,calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {if (calculateChangedBits === undefined) {calculateChangedBits = null;} else {if (__DEV__) {// 略);}}const context: ReactContext<T> = {$$typeof: REACT_CONTEXT_TYPE,_calculateChangedBits: calculateChangedBits,// As a workaround to support multiple concurrent renderers, we categorize// some renderers as primary and others as secondary. We only expect// there to be two concurrent renderers at most: React Native (primary) and// Fabric (secondary); React DOM (primary) and React ART (secondary).// Secondary renderers store their context values on separate fields._currentValue: defaultValue,_currentValue2: defaultValue,// These are circularProvider: (null: any),Consumer: (null: any),};context.Provider = {$$typeof: REACT_PROVIDER_TYPE,_context: context,};if (__DEV__) {// 異常處理} else {context.Consumer = context;}return context;
}
打印 React.createContext() 返回的 context 對象,驗證套娃操作:

7. ConcurrentMode
在 React 16.6 提出,讓 react 整體渲染過程可以根據優先級排列,可以任務調度、可以中斷渲染過程等。來提高渲染性能,減少頁面卡頓。
flushSync 使用優先級最高的方式進行更新,用來提高 this.setState 優先級。
使用 ConcurrentMode 包裹的組件都是低優先級的,所以為了演示高低優先級帶來的區別感受,對比使用 flushSync。
例子中使用 flushSync 時,setState 優先級最高,基本是立即更新,這也導致動畫卡頓明顯,因為 200ms setState 間隔太快了,可能還沒來得及渲染完,又要更新。因此使用 ConcurrentMode 把更新的優先級降低,從而不會頻繁更新動畫,顯得流暢許多。
import React, { ConcurrentMode } from 'react'
import { flushSync } from 'react-dom'import './index.css'class Parent extends React.Component {state = {async: true,num: 1,length: 20000,}componentDidMount() {this.interval = setInterval(() => {this.updateNum()}, 200)}componentWillUnmount() {// 別忘了清除intervalif (this.interval) {clearInterval(this.interval)}}updateNum() {const newNum = this.state.num === 3 ? 0 : this.state.num + 1if (this.state.async) {this.setState({num: newNum,})} else {flushSync(() => {this.setState({num: newNum,})})}}render() {const children = []const { length, num, async } = this.statefor (let i = 0; i < length; i++) {children.push(<div className="item" key={i}>{num}</div>,)}return (<div className="main">async:{' '}<inputtype="checkbox"checked={async}onChange={() => flushSync(() => this.setState({ async: !async }))}/><div className="wrapper">{children}</div></div>)}
}export default () => (<ConcurrentMode><Parent /></ConcurrentMode>
)
@keyframes slide {0% {margin-left: 0;/* transform: translateX(0); */}50% {margin-left: 200px;/* transform: translateX(200px); */}100% {margin-left: 0;/* transform: translateX(0); */}
}.wrapper {width: 400px;animation-duration: 3s;animation-name: slide;animation-iteration-count: infinite;display: flex;flex-wrap: wrap;background: red;
}.item {width: 20px;height: 20px;line-height: 20px;text-align: center;border: 1px solid #aaa;
}
React.ConcurrentMode 源碼:
沒錯,源碼就是一個 Symbol 符號,顯示 react 內部會判斷該標記然后做些什么。
React.ConcurrentMode = REACT_CONCURRENT_MODE_TYPE;
不難想象,經過 createElement() 創建出的 reactElement 樹節點應該會是這樣:
element = {// This tag allows us to uniquely identify this as a React Element$$typeof: REACT_ELEMENT_TYPE,// Built-in properties that belong on the elementtype: REACT_CONCURRENT_MODE_TYPE, // 看這里key: null,ref: null,props: props,// Record the component responsible for creating this element._owner: owner,
};
8. Suspense
Suspense 是一種在組件所依賴的數據尚未加載 ok 時,負責和 react 進行溝通,展示中間態的機制。
react 將會等待數據加載完成然后進行 UI 更新。
傳統做法是在組件第一次掛載之后,即 componentDidMount() 或 useEffect() 中加載數據。即:
- Start fetching
- Finish fetching(然后調用 setState)
- Start rendering(再次渲染)
而 Suspense 也是先獲取數據,(而且可以比傳統做法更早一步,在第一次渲染之前),接著立馬就開始第一次渲染(甚至在網絡請求被實際發出前),遇到懸而未決即數據尚未獲取,則掛起(suspends)該組件,跳過,然后繼續渲染 element 樹中其他的組件,如果又遇到還沒搞定的 Suspense,則繼續掛起并跳過。
- Start fetching
- Start rendering
- Finish fetching
Suspense 具體使用時,有一個“邊界”概念,只有當一個 Suspense 內部的所有“掛起”都落地后,Suspense 才會停止展示 fallback 中間態,然后一并展示內部的 UI,因此可以通過合理增添 Suspense 邊界來控制這種粒度。
// 用于懶加載的組件:
import React from 'react'
export default () => <p>Lazy Comp</p>
import React, { Suspense, lazy } from 'react'const LazyComp = lazy(() => import('./lazy.js'))let data = ''
let promise = ''
function requestData() {if (data) return dataif (promise) throw promisepromise = new Promise(resolve => {setTimeout(() => {data = 'Data resolved'resolve()}, 2000)})throw promise
}function SuspenseComp() {const data = requestData() // 數據尚未加載完成時,該組件的渲染會被掛起。return <p>{data}</p>
}export default () => (<Suspense fallback="loading data"><SuspenseComp /><LazyComp /></Suspense>
)
Suspense 源碼:
Suspense: REACT_SUSPENSE_TYPE // 又是一個 symbol 常量
lazy 源碼:
_ctor
就是調用 lazy() 時傳入的函數,該函數應返回一個 Thenable(具有then方法的對象)。react 渲染到該組件時,會調用 _ctor
函數。
_status
用于記錄當前 Thenable 的狀態, -1
代表尚未解決,對應到 promise 中也就是 pending 狀態。
_result
用于存放 resolve 后的結果。
import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {return {$$typeof: REACT_LAZY_TYPE,_ctor: ctor,// React uses these fields to store the result._status: -1,_result: null, };
}
另外,關于 Suspense 能解決異步競態問題的理解:
異步請求帶來的競態問題,本質是因為異步請求和 React 分別處于各自的生命周期,二者并未相互同步、一一對應。往往需要等待一段時間,在數據返回之后才去調用 setState,如果快速操作 UI,多次發送相同請求時,由于異步請求時間的不確定性,可能第一條請求反而比第二條同樣的請求,響應的更慢,這將導致頁面展示了“舊”的數據,帶來了混亂。
而使用 Suspense 時,是在UI觸發之后,立即調用 setState 嘗試去更新數據,如果數據還沒返回,則 Suspense 會掛起(suspend)并展現 fallback 中間態。但這個等待正確數據的時間管理工作,已經交給 Suspense 內部去處理了,setState 的使命已經立即完成。暫時理解為臟活累活交給 Suspense 自行消化。
Suspense 結合 useTransition
使用更佳:
This scenario (Receded → Skeleton → Complete) is the default one. However, the Receded state is not very pleasant because it “hides” existing information. This is why React lets us opt into a different sequence (Pending → Skeleton → Complete) with
useTransition
.
When weuseTransition
, React will let us “stay” on the previous screen — and show a progress indicator there. We call that a Pending state. It feels much better than the Receded state because none of our existing content disappears, and the page stays interactive.
9. Hooks
hooks 用于 function 函數式組件,內部沒有 class component 中那樣用來維持內部狀態的 this 對象,也沒有典型的生命周期方法。
一個非常簡單的例子,hooks 是一個大塊的內容,這里不表。
/*** 必須要react和react-dom 16.7以上*/import React, { useState, useEffect } from 'react';export default () => {const [name, setName] = useState('jokcy');useEffect(() => {console.log('component update');return () => {console.log('unbind');};}, []);return (<><p>My Name is: {name}</p><input type="text" value={name} onChange={e => setName(e.target.value)}/></>);
}
useState 源碼:
useEffect 等 use* 類似,都是在調用 dispatcher 上的方法。后期會再深入研究 hooks 內層的源碼:
export function useState<S>(initialState: (() => S) | S) {const dispatcher = resolveDispatcher();return dispatcher.useState(initialState);
}
// 局部
function resolveDispatcher() {const dispatcher = ReactCurrentOwner.currentDispatcher;invariant(dispatcher !== null,'Hooks can only be called inside the body of a function component.',);return dispatcher;
}
/*** Keeps track of the current owner.** The current owner is the component who should own any components that are* currently being constructed.*/
const ReactCurrentOwner = {/*** @internal* @type {ReactComponent}*/current: (null: null | Fiber), // 代表當前正在被構建的組件實例currentDispatcher: (null: null | Dispatcher),
};export default ReactCurrentOwner;
10. Children
Children 上的方法有:
Children: {map,forEach,count,toArray,only,
},
React.Children.map() 中 map 可以遍歷 child 并映射展開:
import React from 'react'function ChildrenDemo(props) {console.log(props.children)console.log(React.Children.map(props.children, c => [c, [c, c]]))return props.children
}export default () => (<ChildrenDemo><span>1</span><span>2</span></ChildrenDemo>
)
map 遍歷 children 時有個 contextPool 對象常量池的概念,用于復用對象。以節省遞歸遍歷 child 時多次對象創建和 GC 回收的開銷。
React 這么實現主要是兩個目的:
- 拆分 map 出來的數組
-
因為對 Children 的處理一般在 render 里面,所以會比較頻繁,所以設置一個對象池子減少聲明和 GC 的開銷。
image
11. Other
-
memo
,用于 function component,功能上對標 class component 中的pureComponent
。
淺層源碼中,也沒多少東西,只是返回了一個帶有 $$typeof
標記的對象及 type
、 compare
,memo 這種返回類似前面 createRef 的返回:
export default function memo<Props>(type: React$ElementType,compare?: (oldProps: Props, newProps: Props) => boolean,
) {if (__DEV__) {if (!isValidElementType(type)) {warningWithoutStack(false,'memo: The first argument must be a component. Instead ' +'received: %s',type === null ? 'null' : typeof type,);}}return {$$typeof: REACT_MEMO_TYPE,type,compare: compare === undefined ? null : compare,};
}
-
FragElement
(簡寫方式是空標簽<></>
)
最外層源碼就是 FragElement: REACT_FRAGMENT_TYPE
。
是臨時節點,因為 React 要求不能直接返回多個兄弟節點,要么“包”一層,要么返回數組。而 FragElement 就是用來“包”一層的,相比于用真實的 div 去“包”一層,FragElement 并不會實際被創建。
- cloneElement
克隆一個節點,源碼基本和 createElement 一樣,只是第一個參數從 type
變為了 element
,實際即要克隆的 element 對象上屬性,然后把參數丟給 ReactElement() 來返回一個新的 element 對象:
/*** Clone and return a new ReactElement using element as the starting point.* See https://reactjs.org/docs/react-api.html#cloneelement*/
export function cloneElement(element, config, children) {invariant(!(element === null || element === undefined),'React.cloneElement(...): The argument must be a React element, but you passed %s.',element,);let propName;// Original props are copiedconst props = Object.assign({}, element.props);// Reserved names are extractedlet key = element.key;let ref = element.ref;// Self is preserved since the owner is preserved.const self = element._self;// Source is preserved since cloneElement is unlikely to be targeted by a// transpiler, and the original source is probably a better indicator of the// true owner.const source = element._source;// Owner will be preserved, unless ref is overriddenlet owner = element._owner;if (config != null) {if (hasValidRef(config)) {// Silently steal the ref from the parent.ref = config.ref;owner = ReactCurrentOwner.current;}if (hasValidKey(config)) {key = '' + config.key;}// Remaining properties override existing propslet defaultProps;if (element.type && element.type.defaultProps) {defaultProps = element.type.defaultProps;}for (propName in config) {if (hasOwnProperty.call(config, propName) &&!RESERVED_PROPS.hasOwnProperty(propName)) {if (config[propName] === undefined && defaultProps !== undefined) {// Resolve default propsprops[propName] = defaultProps[propName];} else {props[propName] = config[propName];}}}}// Children can be more than one argument, and those are transferred onto// the newly allocated props object.const childrenLength = arguments.length - 2;if (childrenLength === 1) {props.children = children;} else if (childrenLength > 1) {const childArray = Array(childrenLength);for (let i = 0; i < childrenLength; i++) {childArray[i] = arguments[i + 2];}props.children = childArray;}return ReactElement(element.type, key, ref, self, source, owner, props);
}
- createFactory
如果時候 jsx 語法,而不是手動用 JS 調用 createElement 來創建 element 樹的話,基本不會用到該方法,createFactory 就只是包了一層,省的每次創建同樣類型的 element,都傳入第一個 type 參數了:
/*** Return a function that produces ReactElements of a given type.* See https://reactjs.org/docs/react-api.html#createfactory*/
export function createFactory(type) {const factory = createElement.bind(null, type);// Expose the type on the factory and the prototype so that it can be// easily accessed on elements. E.g. `<Foo />.type === Foo`.// This should not be named `constructor` since this may not be the function// that created the element, and it may not even be a constructor.// Legacy hook: remove itfactory.type = type;return factory;
}
- isValidElement
判斷一個對象是否是合法的 element:
/*** Verifies the object is a ReactElement.* See https://reactjs.org/docs/react-api.html#isvalidelement* @param {?object} object* @return {boolean} True if `object` is a ReactElement.* @final*/
export function isValidElement(object) {return (typeof object === 'object' &&object !== null &&object.$$typeof === REACT_ELEMENT_TYPE);
}

喜歡的朋友記得點贊、收藏、關注哦!!!