前言:今天偶有閑暇霸褒,突然想知道 虛擬DOM 是如何實現(xiàn)的膛檀,說到 虛擬DOM 馅扣,便不得不說說 createElement
方法斟赚。所以先看 createElement
方法,并于此記錄一波差油。(下次在寫這種源碼指不定啥時候)
目前看的版本: 16.13.1
看源碼準備工作
第一步:clone項目
第二步拗军,搜索:createElement
, 搜索結(jié)果中,唯一能和定義沾邊的就是這個 React.d.ts
文件了
第三步:根據(jù) React.d.ts
蓄喇,找到當(dāng)前目錄的 src
发侵,接著找 React.js
(ts
語法我還沒學(xué)過,不過看到 .ts妆偏,我就去找根目錄下的同名文件刃鳄,一般都能找到)。 看到這熟悉的聲明語句楼眷,我就知道我找到了 铲汪。
const createElement = __DEV__ ? createElementWithValidation : createElementProd;
開始正式查看
createElement
方法在 開發(fā)模式中使用 createElementWithValidation
熊尉,在生產(chǎn)模式中使用 createElementProd
。(開發(fā)者模式和生產(chǎn)模式的區(qū)分變量 __DEV__
掌腰,為啥叫這個狰住,一會我研究研究在發(fā)一篇文章,頭一次考慮這么細)
一般在開發(fā)者模式中齿梁,注釋和代碼縮進都會很不錯催植,所以看開發(fā)者模式的方法 createElementWithValidation
。
createElementWithValidation
勺择, 顧名思義 創(chuàng)建_元素_經(jīng)過_校驗
创南。具體怎么校驗和創(chuàng)建,咱們接著往下看省核。 (這個方法是相對引用的文件稿辙,怎么找我就不說了)
createElementWithValidation
方法第一句。玩呢气忠。邻储。上來就是一函數(shù)。旧噪。 先看名字 isValidElementType
吨娜,盲猜:是_有效_元素_類型
, 點擊去瞅瞅淘钟。
得嘞宦赠,看完第一行的這個函數(shù),咱們接著往下走米母。
為了方便勾扭,我直接在代碼注釋內(nèi)寫理解了
// We warn in this case but don't throw. We expect the element creation to
// succeed and there will likely be errors in render.
/***
在這種情況下我們會警告但不要拋出錯誤。我們希望元素創(chuàng)建成功铁瞒,并且呈現(xiàn)中可能會有錯誤尺借。
**/
if (!validType) { /*** 如果沒有經(jīng)過前面的類型校驗 **/
let info = '';
/*** 如果是個 undefined 元素,或者 是個空對象精拟, 我們就提示他:
您可能忘記從定義組件的文件中導(dǎo)出組件,或者您可能混淆了默認導(dǎo)入和命名導(dǎo)入虱歪。(不得不說這個提示是真的友好)
**/
if (
type === undefined ||
(typeof type === 'object' &&
type !== null &&
Object.keys(type).length === 0)
) {
info +=
' You likely forgot to export your component from the file ' +
"it's defined in, or you might have mixed up default and named imports.";
}
/*** 獲取出錯文件路徑 或者出錯組件名稱蜂绎,方便查找, 查找路徑的這個方法后邊再看笋鄙,跟我目標虛擬DOM 不是很沾邊 **/
const sourceInfo = getSourceInfoErrorAddendumForProps(props);
if (sourceInfo) {
/*** 如果存在引用信息师枣,就提示出錯路徑和出錯行數(shù) **/
info += sourceInfo;
} else {
/*** 沒有引用,那就提示出錯組件名稱 **/
info += getDeclarationErrorAddendum();
}
/*** 判斷組件 類型萧落,分為 null ,array, 自定義組件, 原生組件 **/
let typeString;
if (type === null) {
typeString = 'null';
} else if (Array.isArray(type)) {
typeString = 'array';
} else if (type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE) {
typeString = `<${getComponentName(type.type) || 'Unknown'} />`;
info =
' Did you accidentally export a JSX literal instead of a component?';
} else {
typeString = typeof type;
}
/*** 如果是開發(fā)模式践美,就拼接錯誤洗贰,并輸出 **/
if (__DEV__) {
console.error(
'React.createElement: type is invalid -- expected a string (for ' +
'built-in components) or a class/function (for composite ' +
'components) but got: %s.%s',
typeString,
info,
);
}
}
OK,第一大段落陨倡,錯誤驗證就算結(jié)束了敛滋。接下來請看第二段,生成 element
元素
/*** 創(chuàng)建元素兴革,一個大函數(shù)绎晃,容后再解釋 **/
const element = createElement.apply(this, arguments);
// The result can be nullish if a mock or a custom function is used.
/*** 如果使用mock或自定義函數(shù),則結(jié)果可能為空杂曲。 **/
// TODO: Drop this when these are no longer allowed as the type argument.
/*** 當(dāng)不再允許這些參數(shù)作為類型參數(shù)時庶艾,刪除此項。 **/
if (element == null) {
/*** 如果是 mock 或 自定義函數(shù)時擎勘, 返回 null元素 **/
return element;
}
// Skip key warning if the type isn't valid since our key validation logic
// doesn't expect a non-string/function type and can throw confusing errors.
// We don't want exception behavior to differ between dev and prod.
// (Rendering will throw with a helpful message and as soon as the type is
// fixed, the key warnings will appear.)
/***翻譯
如果類型無效咱揍,則跳過密鑰警告,因為我們的密鑰驗證邏輯不需要非字符串/函數(shù)類型棚饵,并且可能會引發(fā)混淆錯誤煤裙。
我們不希望生產(chǎn)模式和開發(fā)模式的異常處理有所不同。
(提示一條有幫助的消息蟹地,在類型被修復(fù)之前积暖,將顯示關(guān)鍵警告。)
**/
if (validType) {
for (let i = 2; i < arguments.length; i++) {
/*** 函數(shù)大意:
確保每個元素要么在靜態(tài)位置傳遞怪与,要么在定義了顯式鍵屬性的數(shù)組中傳遞夺刑,要么在具有有效鍵屬性的對象文本中傳遞。 **/
validateChildKeys(arguments[i], type);
}
}
/*** 校驗Props 有效性分别, 分為 fragment標簽和 普通標簽 **/
if (type === REACT_FRAGMENT_TYPE) {
validateFragmentProps(element);
} else {
validatePropTypes(element);
}
/*** 元素創(chuàng)建完畢遍愿,返回元素 **/
return element;
接下來,看重點耘斩。 createElement
方法 所在文件packages/react/src/ReactElement.js
首先沼填,函數(shù)部分上來就是這些聲明,這些聲明也標志了一個 react組件
需要的參數(shù)
/*** props 名稱 **/
let propName;
/*** 提取props 括授,放到 props變量內(nèi) **/
// Reserved names are extracted
const props = {};
let key = null; /*** 元素 key 值 **/
let ref = null; /*** 元素ref **/
let self = null; /*** 元素上一級 **/
let source = null; /*** 元素路徑 **/
校驗 ref
/*** 存在 ref 坞笙,并且有效 ,函數(shù)內(nèi) `Object.getOwnPropertyDescriptor(config, 'ref').get;`寫的賊棒 **/
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
/*** 開發(fā)模式中荚虚,如果ref 無效薛夜,提示的內(nèi)容,應(yīng)該是新版才會用的 useRef 和 createRef **/
warnIfStringRefCannotBeAutoConverted(config);
}
}
校驗 key
if (hasValidKey(config)) {
key = '' + config.key;
}
對 self
和 source
進行校驗版述,賦值
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
props
屬性處理梯澜,利用遍歷和賦值
// Remaining properties are added to a new props object
for (propName in config) {
/*** hasOwnProperty = Object.prototype.hasOwnProperty; 可遍歷屬性
RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
}; 沒有這些屬性
**/
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
遷移 children
/*** 翻譯
子項可以是多個參數(shù),并且這些子項被轉(zhuǎn)移到新分配的props對象上渴析。
**/
// 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;
}
生成并返回 react元素
return ReactElement(element.type, key, ref, self, source, owner, props);
接下來是講解 ReactElement
方法晚伙。 packages/react/src/ReactElement.js
找到 ReactElement
方法之后吮龄,首先看到的就是這一大串注釋
我感覺挺有用,先翻譯一波咆疗。
/***
* 方法介紹翻譯:
一個用于創(chuàng)造 react元素的 格式化(???這么說應(yīng)該沒啥毛病)方法漓帚。
這里不再遵循class聲明,請不要使用 new 來調(diào)用它民傻。
另外胰默,檢查某個元素是否為react元素也不可以使用 instanceof 。
可以使用 $$typeof 是否為 Symbol.for('react.element')漓踢。
*
* @param {*} type
* @param {*} props
* @param {*} key
* @param {string|object} ref
* @param {*} owner
* @param {*} self 當(dāng)調(diào)用React.createElement時牵署,一個*臨時*助手,用于檢測“this”與“owner”是否不同喧半,以便我們發(fā)出警告奴迅。我們希望去掉owner并用箭頭函數(shù)替換字符串ref,只要this和owner相同挺据,行為就不會改變取具。
* @param {*} source 表示文件名、行號或其它信息的注釋對象(由transpiler或其它方式添加)扁耐。
* @internal
**/
接下來看正式的代碼暇检,整個代碼只有一個聲明,一個返回婉称,所以只看聲明就OK了块仆。
const element = {
/*** 翻譯: 這個標簽允許我們將其唯一地標識為一個React元素 **/
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
/*** REACT_ELEMENT_TYPE = Symbol.for('react.element') **/
/*** 翻譯: 屬于元素的內(nèi)置屬性 **/
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
/*** 翻譯: 記錄創(chuàng)建此元素的組件。 **/
// Record the component responsible for creating this element.
_owner: owner,
};
/*** 中間有一堆開發(fā)模式才有代碼王暗,這個先無視悔据,下面再講 **/
return element;
看到這里,有點輕微懵逼俗壹,竟然不是直接返回元素科汗,返回的竟然是個對象,沒有任何dom節(jié)點返回绷雏,設(shè)想:這里返回的對象头滔,在經(jīng)過框架的某個函數(shù),將變成 真實的dom涎显。
稍微提一下屬性修飾詞: MDN
- 形參1:obj : 要定義屬性的對象拙毫。
- 形參2:prop :要定義或修改的屬性的名稱或 Symbol
-
形參3:desc:要定義或修改的屬性描述符。
| desc描述 | 描述 | 默認值|
| ----------- | ----------- | -----------|
| configurable | 該屬性的描述符才能夠被改變棺禾,同時該屬性是否可以被刪除 | false |
| enumerable | 是否可枚舉,遍歷的時候是否顯示這個屬性 | false |
| value | 該屬性的值 | undefined |
| writable | value值是否可以被改變 | false|
看一下開發(fā)模式才有的這些屬性修飾詞吧
if (__DEV__) {
/*** 翻譯:
驗證標志現(xiàn)在是可變的峭跳。我們把它放在一個外部后備存儲器上膘婶,這樣我們就可以凍結(jié)整個對象缺前。一旦它們在常用的開發(fā)環(huán)境中執(zhí)行(難道是在說變化?)悬襟,就可以用WeakMap來代替它們衅码。
**/
// The validation flag is currently mutative. We put it on
// an external backing store so that we can freeze the whole object.
// This can be replaced with a WeakMap once they are implemented in
// commonly used development environments.
element._store = {};
/*** 翻譯:
為了使比較ReactElements更容易用于測試,我們將驗證標志設(shè)為不可枚舉(在可能的情況下脊岳,它應(yīng)該包括我們運行測試的每個環(huán)境)逝段,因此測試框架忽略它。
**/
// To make comparing ReactElements easier for testing purposes, we make
// the validation flag non-enumerable (where possible, which should
// include every environment we run tests in), so the test framework
// ignores it.
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
/*** _self屬性:不可枚舉割捅,不可刪除奶躯,不可賦值 **/
// self and source are DEV only properties.
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
/*** 翻譯:
在兩個不同的地方創(chuàng)建的兩個元素在測試時應(yīng)該被認為是相等的,因此我們將其隱藏在枚舉中亿驾。 **/
// Two elements created in two different places should be considered
// equal for testing purposes and therefore we hide it from enumeration.
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
/*** 如果可以嘹黔,凍結(jié) 元素和 元素的 props 屬性 **/
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}