寫在前面
計劃用半年的時間去深入 React
原始碼並記錄下來,本文是系列文章第一章,前面大多數會以功能為主,不會涉及太多事務機制與流程,後半部分以架構、流程為主。這個是一個水到渠成的事情。看的越多,對其理解的廣度就越大,深度也隨之沉澱,在深入的同時站在作者的角度去思考,能夠脫離原始碼照葫蘆畫瓢,才能算理解,讀懂本身原始碼並不重要。可能沒有什麼休息時間,但是會盡量擠出來完善文章,也算是一種興趣與習慣。
起源
2011 年,前端工程師 Jordan Walke
建立了ReactJS的早期原型FaxJS。時間軸傳送門
從入口開始
時至今日(2019.9.28),五個小時前React
已經將版本更新到 16.10.0
了,預計大半年內將步入17大版本。希望在系列文章完結之後更新(免得我又得看一遍)。
React 與 Vue 的原始碼相同的使用 Facebook 開源的 Flow 靜態型別檢查工具,為什麼要用 Flow 而不用 Typescript ? 原因可能是 React 誕生的時間較早,那時候還沒有 Typescript,後來也由於 Typescript 15年被社群廣泛接受才火起來。還一個原因是 Flow 沒有 Typescript 那麼“嚴格”,所有的檢查都是可選的。
fork/clone/open三部曲,找到 packages/react/src/React.js
,剔除註釋和空白行的原始碼還不到一百行,這個入口檔案整合了所有的api暴露出去。
React中的原始碼與React-DOM分離,所以在packages/React內很多隻是“形”上的API
import ReactVersion from '../../shared/ReactVersion';
import {
REACT_FRAGMENT_TYPE,
REACT_PROFILER_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
} from '../../shared/ReactSymbols';
複製程式碼
最頂部匯入了React當前版本號,ReactSymbols
檔案管理著全域性的 React
功能元件 Symbol 標誌
Component
import {Component, PureComponent} from './ReactBaseClasses';
複製程式碼
Component
和 PureComponent
元件都是經常用的, 猜也能猜到都是定義一些初始化方法。
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, callback, 'forceUpdate'); // 強制加入更新佇列
};
複製程式碼
定義了Component類的 setState
和 forceUpdate
方法,以便在元件例項化後呼叫,將當前的props,context,refs進行繫結,並初始化更新。
每個元件內部都有一個 updater
,被用來驅動state更新的工具物件,執行更新佇列,沒傳入updater時,this.updater
預設為 ReactNoopUpdateQueue
,但是它沒什麼意義,只是做警告用的。
const ReactNoopUpdateQueue = {
isMounted: function(publicInstance) {
return false;
},
enqueueForceUpdate: function(publicInstance, callback, callerName) {
warnNoop(publicInstance, 'forceUpdate');
},
enqueueReplaceState: function(publicInstance, completeState, callback, callerName) {
warnNoop(publicInstance, 'replaceState');
},
enqueueSetState: function(publicInstance, partialState, callback, callerName) {
warnNoop(publicInstance, 'setState');
},
};
複製程式碼
isMounted
在元件未掛載的情況下isMounted
一直會返回 false
,例如在 constructor
裡呼叫 setState
或者元件已解除安裝/未使用,其他方法的作用是在開發環境下警告使用者不要在constructor
內呼叫this原型上的方法。因為實際上真正的 updater
都是在 renderer 後注入的。真正的updater:
const classComponentUpdater = {
isMounted, // fn() => true
enqueueSetState(inst, payload, callback) {
// 獲取fiber 也就是inst._reactInternalFiber
const fiber = getInstance(inst);
// 根據 msToExpirationTime(performance.now()) 得到一個時間,後續涉及到 ExpirationTime
const currentTime = requestCurrentTimeForUpdate();
// 獲取suspense配置,與即將新增的 withSuspenseConfig Api強相關,預設情況下都是null
const suspenseConfig = requestCurrentSuspenseConfig();
// 根據開始實際計算任務過期時間
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
//建立update物件
const update = createUpdate(expirationTime, suspenseConfig);
//setState的更新物件
update.payload = payload;
// setState的callback
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}
// 放入佇列
enqueueUpdate(fiber, update);
// 開始排程
scheduleWork(fiber, expirationTime);
},
// other ...
}
複製程式碼
setState的任務排程以這種形式發出的,另外 與forceUpdate 、 replaceState 也差不多。
那上面提到的ExpirationTime是什麼?
ExpirationTime
ExpirationTime是一個“保險”,為防止某個update因為優先順序的原因一直被打斷而未能執行。React會設定一個ExpirationTime,當時間到了ExpirationTime的時候,如果某個update還未執行的話,React將會強制執行該update,這就是ExpirationTime的作用。它有兩種計算方法,一種computeInteractiveExpiration
同步更新,與 computeAsyncExpiration
返回非同步更新的expirationTime
//整型最大數值,V8中針對32位系統所設定的最大值 Math.pow(2,30) - 1;
export const Sync = MAX_SIGNED_31_BIT_INT; // 1073741823
export const Batched = Sync - 1; // 1073741822
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = Batched - 1; //1073741821
function msToExpirationTime(ms) {
return MAGIC_NUMBER_OFFSET - (ms / UNIT_SIZE | 0); // 1073741821 - now()/10|0
}
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision; // 取整,本次區間分類最大值
}
// 計算過期時間
function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs): ExpirationTime {
/* LOW 任務 => 1073741821 - ceiling(1073741821 - currentTime + 5000 / 10, 250 / 10)
1073741821 - (((1073741821 - currentTime + 500) / 25) | 0) * 25 - 25
*/
/* HIGH任務 => 1073741821 - ceiling(1073741821 - currentTime + (__DEV__ ? 500 : 150) / 10, 100 / 10)
DEV 1073741821 - ceiling(1073741821 - currentTime + 50, 10)
1073741821 - (((1073741821 - currentTime + 50) / 10) | 0) * 10 - 10
!DEV 1073741821 - ceiling(1073741821 - currentTime + 15, 10)
1073741821 - (((1073741821 - currentTime + 15) / 10) | 0) * 10 - 10
*/
return (
MAGIC_NUMBER_OFFSET - ceiling(MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, bucketSizeMs / UNIT_SIZE)
);
}
// LOW 低優先順序任務
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
export function computeAsyncExpiration(currentTime: ExpirationTime): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
// HIGH 高優先順序任務
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
export function computeSuspenseExpiration(
currentTime: ExpirationTime,
timeoutMs: number,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
timeoutMs,
LOW_PRIORITY_BATCH_SIZE,
);
}
複製程式碼
通過performance.now()
生成 currentTime,當不支援performance
時轉利用Date.now()
,這個值不用太過關注,只需要理解成時間戳即可。
const now1 = performance.now(); // 53028380
const now2 = performance.now(); // 53028389 ↑ 9
const now3 = performance.now(); // 53028391 ↑ 11
const now4 = performance.now(); // 53028405 ↑ 25
const now5 = performance.now(); // 53028420 ↑ 40
const now6 = performance.now(); // 53028430 ↑ 50
const now7 = performance.now(); // 53028444 ↑ 55
const now8 = performance.now(); // 53028468 ↑ 79
// LOW 任務
1073741821 - (((1073741821 - now1 + 500) / 25) | 0) * 25 - 25; // 53027871
1073741821 - (((1073741821 - now2 + 500) / 25) | 0) * 25 - 25; // 53027871 ↑ 0
1073741821 - (((1073741821 - now3 + 500) / 25) | 0) * 25 - 25; // 53027871 ↑ 0
1073741821 - (((1073741821 - now4 + 500) / 25) | 0) * 25 - 25; // 53027896 ↑ 25
1073741821 - (((1073741821 - now5 + 500) / 25) | 0) * 25 - 25; // 53027896 ↑ 25
1073741821 - (((1073741821 - now6 + 500) / 25) | 0) * 25 - 25; // 53027921 ↑ 50
1073741821 - (((1073741821 - now7 + 500) / 25) | 0) * 25 - 25; // 53027921 ↑ 50
1073741821 - (((1073741821 - now8 + 500) / 25) | 0) * 25 - 25; // 53027946 ↑ 75
// HIGH 任務 以DEV模式為例
1073741821 - (((1073741821 - now1 + 50) / 10) | 0) * 10 - 10; // 53028321
1073741821 - (((1073741821 - now2 + 50) / 10) | 0) * 10 - 10; // 53028331 ↑ 10
1073741821 - (((1073741821 - now3 + 50) / 10) | 0) * 10 - 10; // 53028331 ↑ 10
1073741821 - (((1073741821 - now4 + 50) / 10) | 0) * 10 - 10; // 53028351 ↑ 30
1073741821 - (((1073741821 - now5 + 50) / 10) | 0) * 10 - 10; // 53028361 ↑ 40
1073741821 - (((1073741821 - now6 + 50) / 10) | 0) * 10 - 10; // 53028371 ↑ 50
1073741821 - (((1073741821 - now7 + 50) / 10) | 0) * 10 - 10; // 53028391 ↑ 70
1073741821 - (((1073741821 - now8 + 50) / 10) | 0) * 10 - 10; // 53028411 ↑ 90
複製程式碼
通過規律,可以看到LOW優先順序任務時,區間<25的,得到的都是同一個值,而HIGH高優先順序任務的區間為10,單位為毫秒,這個有什麼用呢?
如果觸發了多次事件,每次難道都要丟enqueueUpdate裡立即排程,那未免效能太差了。
React讓兩個相近(25ms內)的update得到相同的expirationTime,它可以將多次事件分批打包丟入 enqueueUpdate
裡,假如在24ms內觸發了兩個事件,那麼React會將他們丟入同一批車
,目的就是讓這兩個update自動合併成一個Update
,並且只會觸發一次更新,從而達到批量更新的目的。
從之前的程式碼看 computeInteractiveExpiration傳入的是150、100,computeAsyncExpiration傳入的是5000、250,前者的優先順序更高,而過期執行時間為互動事件為 100/UNIT_SIZE = 10,非同步事件則為 250/UNIT_SIZE = 25, 佐證了事實。
PureComponent
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
複製程式碼
與 Component
類相同屬性的 PureComponent
類有所不同,首先建立了一個空函式 ComponentDummy
,並將通過共享原型繼承的方式將例項原型指向了 Component
的原型,其建構函式指定為PureComponent
。其實就是在外面套了一層 pureComponentPrototype
的 Component
。
createRef
import {createRef} from './ReactCreateRef';
複製程式碼
export function createRef(): RefObject {
const refObject = {
current: null,
};
if (__DEV__) {
Object.seal(refObject);
}
return refObject;
}
複製程式碼
返回一個refObject,其current的屬性在元件掛載時進行關聯,與react-dom強相關,後面再瞭解。現在只需要知道它很簡單。
Children
import {forEach, map, count, toArray, only} from './ReactChildren';
複製程式碼
React 將 children 的 API 暴露出來,這裡最常使用的應該是 React.Children.map
和 React.Children.forEach
。
Children.map
適用於替代 this.props.children.map
,因為這種寫法通常用來巢狀元件,但是如果巢狀的是一個函式就會報錯。而 React.Children.map
則不會。當需要寫一個 Radio
元件需要依賴其父元件 RadioGroup
的 props
值,那麼this.props.children.map
配合 cloneElement
簡直不能再完美。還可以用來過濾某些元件。
React.cloneElement(props.children, {
name: props.name
})
render(){
return (
<div>
{
React.Children.map(children, (child, i) => {
if ( i < 1 ) return;
return child;
})
}
</div>
)
}
複製程式碼
mapChildren
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
複製程式碼
mapIntoWithKeyPrefixInternal
第一步,如果子元件為null直接不處理。正常情況下申明一個陣列,進行加工。
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
let escapedPrefix = '';
if (prefix != null) { // 顧名思義,處理key
escapedPrefix = escapeUserProvidedKey(prefix) + '/';
}
// 取出一個物件,作為上下文,遍歷children
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context,
);
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
// 釋放物件
releaseTraverseContext(traverseContext);
}
複製程式碼
getPooledTraverseContext 與 releaseTraverseContext
第二步,處理key的暫時不用管。最終通過 getPooledTraverseContext
到物件池裡取一個物件,給 traverseAllChildren
進行處理,結束的時候通過 releaseTraverseContext
reset所有屬性放回去,做到複用,避免了一次性建立大量物件和釋放物件消耗效能造成的記憶體抖動。池
的概念在計算機領域被大量引用,包括Node的Buffer的記憶體分配策略也是用的池的概念。
getPooledTraverseContext 用來取。 releaseTraverseContext 用來清空後放回
// 維護一個大小為 10 的物件重用池
const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext(
mapResult,
keyPrefix,
mapFunction,
mapContext,
) {
if (traverseContextPool.length) { // 如果當前物件池內有可用物件,就從隊尾pop一個初始化後返回
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,
};
}
}
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);
}
}
複製程式碼
traverseAllChildren/traverseAllChildrenImpl
第三步,最重要的一步,在取出一個待複用物件後,traverseAllChildren
判斷為null就沒必要處理了。直接 return。
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0; // return
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
複製程式碼
function traverseAllChildrenImpl(
children, // children
nameSoFar, // 父級 key
callback, // 如果是可渲染節點
traverseContext, // 物件池複用物件
) {
const type = typeof children;
if (type === 'undefined' || type === 'boolean') {
// 以上都被認為是null。
children = null;
}
// 如果是可渲染的節點則為true,表示能呼叫callback
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: // React元素 或者是 Portals
case REACT_PORTAL_TYPE:
invokeCallback = true;
}
}
}
if (invokeCallback) { // 可渲染節點,直接呼叫回撥
callback( // 呼叫 mapSingleChildIntoContext
traverseContext,
children,
nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
);
return 1;
}
let child;
let nextName;
let subtreeCount = 0; //在當前子樹中找到的子級的層級數。
const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
if (Array.isArray(children)) { // 如果children是陣列,則遞迴處理
// 例如 React.Children.map(this.props.children, c => [[c, c]])
// c => [[c, c]] 會被攤平為 [c, c, c, c]
for (let i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child, i); // 在每一層不斷用“:”分隔拼接key
subtreeCount += traverseAllChildrenImpl(
child,
nextName,
callback,
traverseContext,
);
}
} else { // 如果是物件的話通過 obj[Symbol.iterator] 取迭代器
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,
nextName,
callback,
traverseContext,
);
}
} 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;
}
複製程式碼
const Demo = ({ children }) => {
console.log(React.Children.map(children, c => [[[[c, c]]]]));
return (
children
);
};
const Children = ({ msg }) => (
<span>
{ msg }
</span>
);
複製程式碼
上面函式的核心作用就是通過把傳入的 children 陣列通過遍歷攤平成單個節點,其中迭代的所有callback都是 mapSingleChildIntoContext
。
mapSingleChildIntoContext
// bookKeeping getPooledTraverseContext 內從複用物件池取出來的 traverseContext
// child 傳入的節點
// childKey 節點的 key
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
const {result, keyPrefix, func, context} = bookKeeping;
// func === React.Children.map(props.children, c => c) 的 c => c 函式
let mappedChild = func.call(context, child, bookKeeping.count++);
// 如果func返回值設定為陣列 React.Children.map(this.props.children, c => [c, c])
// 表示每個元素將被返回兩次。假如children為 c1,c2,那麼最後返回的應該是c1,c1,c2,c2
if (Array.isArray(mappedChild)) {
// 是陣列的話,就再呼叫 mapIntoWithKeyPrefixInternal
// 和 mapChildren 呼叫它的流程一樣。遞迴將其鋪平
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
} else if (mappedChild != null) {
// 如果是不為null並且是有效的 Element
if (isValidElement(mappedChild)) {
// 克隆 Element && 替換掉key 推入result
mappedChild = cloneAndReplaceKey(
mappedChild,
keyPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(mappedChild.key) + '/' : '') + childKey,
);
}
result.push(mappedChild);
}
}
複製程式碼
最終的邏輯又回到了 mapIntoWithKeyPrefixInternal
,通過遞迴呼叫使返回的陣列結果展開鋪平。
整體流程
大概整體流程
`mapChildren` ===================> `mapIntoWithKeyPrefixInternal` ==================> `getPooledTraverseContext`(複用物件池)
/\ ||
/||\ ||
|| ||
|| \||/
|| \/
||Yes `traverseAllChildren`(遍歷children樹)
|| ||
|| ||
|| \||/
|| \/
No || (children是陣列又會重新遞迴執行)
`releaseTraverseContext`(釋放物件池)<=====`mapSingleChildIntoContext`(鋪平result)<=============`traverseAllChildrenImpl`
複製程式碼
Children.forEach
相比 map
,forEachChildren
則簡單的多,因為不用去返回一個新的結果,只需要對children做遍歷,
function forEachChildren(children, forEachFunc, forEachContext) {
if (children == null) {
return children;
}
const traverseContext = getPooledTraverseContext(
null, // 不需要返回陣列,所以result為null
null, // key也不需要
forEachFunc,
forEachContext,
);
// 第二個引數 forEachSingleChild 簡單呼叫了 forEachFunc
traverseAllChildren(children, forEachSingleChild, traverseContext);
releaseTraverseContext(traverseContext);
}
function forEachSingleChild(bookKeeping, child, name) {
const {func, context} = bookKeeping;
func.call(context, child, bookKeeping.count++);
}
複製程式碼
Children.count
function countChildren(children) {
return traverseAllChildren(children, () => null, null);
}
複製程式碼
用來計算children的個數,平時用的較少。和前面2個方法的行為差不多,預置的callback也不會進行任何處理。最終返回當前children的子元素,並不會向下遞迴查詢。
Children.toArray
function toArray(children) {
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, child => child);
return result;
}
複製程式碼
用來將children
轉化成普通的陣列,原理和 mapChildren 一樣,可以用來將傳入的children
重新進行排序。
class Sort extends React.Component {
render () {
const children = React.Children.toArray(this.props.children);
return <p>{children.sort((a,b)=>a-b).join('-')}</p>
}
}
<Sort>
{2}{5}{8}{4}{9}
</Sort>
// view
2-4-5-8-9
複製程式碼
Children.only
function onlyChild(children) {
invariant(
isValidElement(children),
'React.Children.only expected to receive a single React element child.',
);
return children;
}
複製程式碼
校驗children是否為ReactElement,是則返回,否則報錯。可以用來製做一個只接受一個 children
的 <Single>
元件。
class Single extends Component{
render(){
return React.Children.only(this.props.children)
}
}
function App(){
return (
<Single>
<div>first</div>
<div>second</div> {/* error */}
</Single>
)
}
複製程式碼
ReactElement
import {
createElement,
createFactory,
cloneElement,
isValidElement,
jsx,
} from './ReactElement';
const React = {
// ...
createElement: __DEV__ ? createElementWithValidation : createElement,
cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
isValidElement: isValidElement
}
複製程式碼
開發環境下會自動validator,切到packages/react/src/ReactElement.js
,大綱如下
hasValidRef ------------------------------- 判斷是否有合理的ref
hasValidKey ------------------------------- 判斷是否有合理的key
defineRefPropWarningGetter ---------------- 鎖定props.ref
defineKeyPropWarningGetter ---------------- 鎖定props.key
ReactElement ------------------------------ 轉化ReactElement
jsx --------------------------------------- 使用jsx方式建立Element
jsxDEV ------------------------------------ 使用jsx方式建立Element(DEV)
createElement ----------------------------- 建立並返回指定型別的ReactElement
createFactory ----------------------------- 工廠模式createElement構造器
cloneAndReplaceKey ------------------------ 替換新key
cloneElement ------------------------------ 克隆Element
isValidElement ---------------------------- 判定是否ReactElement
複製程式碼
createElement
這個方法大家耳熟能詳。React用的最多的方法,沒有之一。它負責React內所有元素節點的建立及初始化。
該方法接受三個引數 type, config, children
,type
是標籤或者元件的名稱,div/span/ul,對應的自定義Component首字母一定是大寫。config
則包含所有的屬性配置,children
代表子節點。
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
if (hasValidRef(config)) {// 查詢config內是否存在合理的ref
ref = config.ref;
}
if (hasValidKey(config)) {// 查詢config內是否存在合理的key
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 將剩餘屬性新增到新的props物件中
// 這也就是為什麼<Component key={Math.random()}/>子元件中為什麼找不到key/ref/__self/__source屬性的原因
for (propName in config) {
if (
// 忽略原型鏈上的屬性,並且抽離key/ref/__self/__source屬性
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// children引數可以不止一個,除去前兩個引數,其他的都是children
const childrenLength = arguments.length - 2;
// 對children做格式處理。一個為物件,多個則為Array
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];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// 解析defaultProps,定義元件的預設Props屬性
// Com.defaultProps = { msg:'default' }
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 在開發環境下,鎖定props上 key與 ref 的getter,不予獲取
if (__DEV__) {
if (key || ref) {
const displayName = typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
// 最後轉化成React元素
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current, // 建立React元素的元件
props,
);
}
複製程式碼
const ReactCurrentOwner = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Fiber),
};
複製程式碼
createElement
僅僅起到為ReactElement
加工前過濾屬性的作用。
create ReactElement
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// $$typeof將其標識為一個React元素
$$typeof: REACT_ELEMENT_TYPE,
// ReactElement的內建屬性
type: type,
key: key,
ref: ref,
props: props,
// 建立此元素的元件。
_owner: owner,
};
//為了利於測試,在開發環境下忽略這些屬性(不可列舉),並且凍結props與element
if (__DEV__) {
element._store = {};
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
// self and source are DEV only properties.
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
複製程式碼
ReactElement
將其屬性做二次處理,等待被渲染成DOM,在平常開發我們通過console.log
列印出自定義Component 屬性與element
一致。
createFactory
工廠模式的 createElement
,通過預置 type
引數建立指定型別的節點。
var child1 = React.createElement('li', null, 'First Text Content');
var child2 = React.createElement('li', null, 'Second Text Content');
// 等價於
var factory = React.createFactory("li");
var child1 = factory(null, 'First Text Content');
var child2 = factory(null, 'Second Text Content');
var root = React.createElement('ul', {className: 'list'}, child1, child2);
ReactDOM.render(root,document.getElementById('root'));
複製程式碼
export function createFactory(type) {
// 通過bind預置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 it
factory.type = type;
return factory;
}
複製程式碼
cloneElement
通過傳入新的element與props及children,得到clone後的一個新元素。element
為cloneElement,config
是 newProps,可以重新定義 key
和 ref
,children
子節點。
整體方法與createElement大致相同。
export function cloneElement(element, config, children) {
// 判斷有效的ReactElement
invariant(
!(element === null || element === undefined),
'React.cloneElement(...): The argument must be a React element, but you passed %s.',
element,
);
let propName;
// copy原始 props
const props = Object.assign({}, element.props);
// 提取保留key & ref
let key = element.key;
let ref = element.ref;
// 為了追蹤與定位,繼承被clone的Element這三個屬性
const self = element._self;
const source = element._source;
let owner = element._owner;
if (config != null) {
// 這裡的處理和createElement差不多
// unique ,ref 和 key 可以自定義覆蓋
if (hasValidRef(config)) {
ref = config.ref;
owner = ReactCurrentOwner.current;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
// 其他屬性覆蓋current props
let 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 props
props[propName] = defaultProps[propName];
} else {
props[propName] = config[propName];
}
}
}
}
// 剩餘的與 createElement 一樣
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);
}
複製程式碼
cloneAndReplaceKey
顧名思義,與 cloneElement
名字上雖然差不多,但實際返回的是通過ReactElement傳入newKey重新建立的舊Element。
export function cloneAndReplaceKey(oldElement, newKey) {
const newElement = ReactElement(
oldElement.type,
newKey,
oldElement.ref,
oldElement._self,
oldElement._source,
oldElement._owner,
oldElement.props,
);
return newElement;
}
複製程式碼
isValidElement
也很簡單,通過判斷 $$typeof
是否為內建的ReactElement型別。
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
複製程式碼
jsx/jsxDEV
沒怎麼使用過這個API,猜測應該是通過 react-dom
轉化後使用的建立語法。
從程式碼邏輯上看,與 createElement
大致形同,jsxDEV
比 jsx
多了兩個能自定義的屬性,source
和 self
,按照程式碼註釋,是為了防止出現 <div key="Hi" {...props} />
情況中 key
比 props
先定義,導致被覆蓋的情況。將對<div {...props} key="Hi" />
之外的所有情況統一使用 jsxDEV
來強行賦值 key 與 ref。
export function jsxDEV(type, config, maybeKey, source, self) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
// Currently, key can be spread in as a prop. This causes a potential issue if key is also explicitly declared (ie. <div {...props} key="Hi" /> or <div key="Hi" {...props} /> ). We want to deprecate key spread,
// but as an intermediary step, we will use jsxDEV for everything except <div {...props} key="Hi" />, because we aren't currently able to tell if key is explicitly declared to be undefined or not.
if (maybeKey !== undefined) {
key = '' + maybeKey;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
if (hasValidRef(config)) {
ref = config.ref;
}
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
複製程式碼
defineRefPropWarningGetter/defineKeyPropWarningGetter
key
是用來優化React渲染速度的,而 ref
是用來獲取到React渲染後的真實DOM節點。正常情況下應該將這兩個屬性置之世外,彷彿這兩個屬性都應該是React本身的API。所以這兩個方法就是用來禁止獲取和設定的。
let specialPropKeyWarningShown, specialPropRefWarningShown;
function defineKeyPropWarningGetter(props, displayName) {
const warnAboutAccessingKey = function() {
if (!specialPropKeyWarningShown) { // 只會讀取一次
specialPropKeyWarningShown = true;
warningWithoutStack(
false,
'%s: `key` is not a prop. Trying to access it will result ' +
'in `undefined` being returned. If you need to access the same ' +
'value within the child component, you should pass it as a different ' +
'prop. (https://fb.me/react-special-props)',
displayName,
);
}
};
warnAboutAccessingKey.isReactWarning = true;
Object.defineProperty(props, 'key', {
get: warnAboutAccessingKey,
configurable: true,
});
}
複製程式碼
當在元件內嘗試 console.log(props.key)
的時候,就會發現報錯。
兩個方法邏輯一模一樣,就不寫貼上兩遍了。
hasValidRef/hasValidKey
這兩個方法差不多,在開發模式下多了一個校驗,通過 Object.prototype.hasOwnProperty
檢查當前物件屬性上是否存在 ref/key
,並獲取其訪問器函式 get
,如果事先被defineKeyPropWarningGetter/defineRefPropWarningGetter
鎖定則 getter.isReactWarning
就必然為 true
(注意鎖定方法呼叫的時機)。
function hasValidRef(config) {
if (__DEV__) {
if (hasOwnProperty.call(config, 'ref')) {
const getter = Object.getOwnPropertyDescriptor(config, 'ref').get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.ref !== undefined;
}
複製程式碼