React系列
React簡(jiǎn)單模擬語(yǔ)法(一)
Jsx, 合成事件與Refs(二)
virtualdom diff算法實(shí)現(xiàn)分析(三)
從Mixin到HOC再到HOOKS(四)
createElement, ReactElement與Component部分源碼解析(五)
前言
因?yàn)橹皩?xiě)過(guò)一些文章分別關(guān)于怎么模擬React語(yǔ)法,React基本知識(shí)和virtualdom diff實(shí)現(xiàn)思路,接下來(lái)就跟著React源碼大概了解一下怎么一個(gè)過(guò)程,只是主邏輯代碼,忽略部分開(kāi)發(fā)環(huán)境的代碼.
以下解析僅限于我當(dāng)時(shí)理解,不一定準(zhǔn)確.
JSX編譯
還是用之前的例子
123456
編譯成
React.createElement("div", {
className: "num",
index: 1
}, React.createElement("span", null, "123456"));
createElement
我們看下API的語(yǔ)法
React.createElement( type, [props], [...children] )
創(chuàng)建并返回給定類型的新 React element 。愛(ài)掏網(wǎng) - it200.com
- type: 既可以是一個(gè)標(biāo)簽名稱字符串,也可以是一個(gè) React component 類型(一個(gè)類或一個(gè)函數(shù)),或者一個(gè)React fragment 類型。愛(ài)掏網(wǎng) - it200.com
- props: 各種屬性值
- children: 子元素
因?yàn)橛衎abel會(huì)編譯JSX,所以一般很少會(huì)直接調(diào)用這個(gè)方法.
然后我們進(jìn)入找到對(duì)應(yīng)的源碼位置查看代碼react/packages/react/src/ReactElement.js
/**
* Create and return a new ReactElement of the given type.
* See https://reactjs.org/docs/react-api.html#createelement
*/
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// 有傳config的情況下
if (config != null) {
// 是否有有效的Ref
if (hasValidRef(config)) {
ref = config.ref;
}
// 是否有有效的Key
if (hasValidKey(config)) {
key = '' + config.key;
}
// 暫時(shí)還沒(méi)聯(lián)系上下文,保存self和source
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 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];
}
}
}
// 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
代碼還比較簡(jiǎn)單,可以看出就是傳入?yún)?shù)之后它會(huì)幫你做些特殊處理然后導(dǎo)出給ReactElement方法使用,如果有部分代碼還不知道是干嘛的話也不用擔(dān)心,下面會(huì)有說(shuō)到
function hasValidRef(config) {
// 開(kāi)發(fā)環(huán)境下
if (__DEV__) {
// 自身是否含有ref字段
if (hasOwnProperty.call(config, 'ref')) {
// 獲取它的取值器
const getter = Object.getOwnPropertyDescriptor(config, 'ref').get;
// 滿足條件的話為非法ref
if (getter && getter.isReactWarning) {
return false;
}
}
}
// 直接和undefined作比較判斷是否合法
return config.ref !== undefined;
}
同上
function hasValidKey(config) {
// 開(kāi)發(fā)環(huán)境
if (__DEV__) {
if (hasOwnProperty.call(config, 'key')) {
const getter = Object.getOwnPropertyDescriptor(config, 'key').get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.key !== undefined;
}
// 初始化標(biāo)記
let specialPropKeyWarningShown, specialPropRefWarningShown;
// 定義key的取值器
function defineKeyPropWarningGetter(props, displayName) {
const warnAboutAccessingKey = function() {
if (!specialPropKeyWarningShown) {
specialPropKeyWarningShown = true;
// 目測(cè)是警告提示
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,
);
}
};
// 是否已經(jīng)警告過(guò)
warnAboutAccessingKey.isReactWarning = true;
// 定義key字段
Object.defineProperty(props, 'key', {
get: warnAboutAccessingKey,
configurable: true,
});
}
// 同上
function defineRefPropWarningGetter(props, displayName) {
const warnAboutAccessingRef = function() {
if (!specialPropRefWarningShown) {
specialPropRefWarningShown = true;
warningWithoutStack(
false,
'%s: `ref` 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,
);
}
};
warnAboutAccessingRef.isReactWarning = true;
Object.defineProperty(props, 'ref', {
get: warnAboutAccessingRef,
configurable: true,
});
}
代碼來(lái)看是開(kāi)發(fā)模式下限制了對(duì)應(yīng)key和ref的取值器,使用時(shí)會(huì)執(zhí)行對(duì)應(yīng)方法進(jìn)行報(bào)錯(cuò)不讓讀取.
至此相關(guān)源碼基本了解了
getOwnPropertyDescriptor
上面其中核心方法介紹是這個(gè)
Object.getOwnPropertyDescriptor(obj, prop)
方法返回指定對(duì)象上一個(gè)自有屬性對(duì)應(yīng)的屬性描述符。愛(ài)掏網(wǎng) - it200.com(自有屬性指的是直接賦予該對(duì)象的屬性,不需要從原型鏈上進(jìn)行查找的屬性)
- obj
需要查找的目標(biāo)對(duì)象 - prop
目標(biāo)對(duì)象內(nèi)屬性名稱 - 返回值
如果指定的屬性存在于對(duì)象上,則返回其屬性描述符對(duì)象(property descriptor),否則返回 undefined。愛(ài)掏網(wǎng) - it200.com
屬性描述符對(duì)象(property descriptor)
該方法允許對(duì)一個(gè)屬性的描述進(jìn)行檢索。愛(ài)掏網(wǎng) - it200.com在 Javascript 中, 屬性 由一個(gè)字符串類型的“名字”(name)和一個(gè)“屬性描述符”(property descriptor)對(duì)象構(gòu)成。愛(ài)掏網(wǎng) - it200.com
一個(gè)屬性描述符是一個(gè)記錄,由下面屬性當(dāng)中的某些組成的:
value
該屬性的值(僅針對(duì)數(shù)據(jù)屬性描述符有效)
writable
當(dāng)且僅當(dāng)屬性的值可以被改變時(shí)為true。愛(ài)掏網(wǎng) - it200.com(僅針對(duì)數(shù)據(jù)屬性描述有效)
get
獲取該屬性的訪問(wèn)器函數(shù)(getter)。愛(ài)掏網(wǎng) - it200.com如果沒(méi)有訪問(wèn)器, 該值為undefined。愛(ài)掏網(wǎng) - it200.com(僅針對(duì)包含訪問(wèn)器或設(shè)置器的屬性描述有效)
set
獲取該屬性的設(shè)置器函數(shù)(setter)。愛(ài)掏網(wǎng) - it200.com 如果沒(méi)有設(shè)置器, 該值為undefined。愛(ài)掏網(wǎng) - it200.com(僅針對(duì)包含訪問(wèn)器或設(shè)置器的屬性描述有效)
configurable
當(dāng)且僅當(dāng)指定對(duì)象的屬性描述可以被改變或者屬性可被刪除時(shí),為true。愛(ài)掏網(wǎng) - it200.com
enumerable
當(dāng)且僅當(dāng)指定對(duì)象的屬性可以被枚舉出時(shí),為 true。愛(ài)掏網(wǎng) - it200.com
ReactElement
然后再看看ReactElement的源碼
/**
* Factory method to create a new React element. This no longer adheres to
* the class pattern, so do not use new to call it. Also, no instanceof check
* will work. Instead test $$typeof field against Symbol.for('react.element') to check
* if something is a React Element.
*
* @param {*} type
* @param {*} props
* @param {*} key
* @param {string|object} ref
* @param {*} owner
* @param {*} self A *temporary* helper to detect places where `this` is
* different from the `owner` when React.createElement is called, so that we
* can warn. We want to get rid of owner and replace string `ref`s with arrow
* functions, and as long as `this` and owner are the same, there will be no
* change in behavior.
* @param {*} source An annotation object (added by a transpiler or otherwise)
* indicating filename, line number, and/or other information.
* @internal
*/
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 element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
// 開(kāi)發(fā)模式下增改部分屬性
if (__DEV__) {
// 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 = {};
// 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 and source are DEV only properties.
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
// 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,
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
整段代碼來(lái)看它是在開(kāi)發(fā)環(huán)境下對(duì)字段作處理:
- 創(chuàng)建React元素,設(shè)置對(duì)應(yīng)屬性值
- 開(kāi)發(fā)環(huán)境下
- 創(chuàng)建
_store屬性并配置其validated的屬性描述符對(duì)象,達(dá)到方便調(diào)試React元素的目的 - 配置
_self的屬性描述符對(duì)象,self和source只是DEV的屬性 - 配置
_source的屬性描述符對(duì)象,出于測(cè)試的目的,應(yīng)該將在兩個(gè)不同位置創(chuàng)建的兩個(gè)元素視為相等的,因此我們將它從枚舉中隱藏起來(lái)。愛(ài)掏網(wǎng) - it200.com - 凍結(jié)
element及其props對(duì)象
- 創(chuàng)建
主要目的是為提高測(cè)試環(huán)境下效率,將element的一些屬性配置為不可枚舉,進(jìn)行遍歷的時(shí)候跳過(guò)這些屬性。愛(ài)掏網(wǎng) - it200.com
其中REACT_ELEMENT_TYPE是
// The Symbol used to tag the ReactElement type. If there is no native Symbol
// nor polyfill, then a plain number is used for performance.
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
0xeac7;
用來(lái)標(biāo)識(shí)這個(gè)對(duì)象是一個(gè)ReactElement對(duì)象,至此Jsx編譯成ReactElement對(duì)象的相關(guān)源碼大概知道了.
React組件創(chuàng)建
createClass
16.0.0以后已經(jīng)廢棄,可忽略
ES6類繼承
從官方例子來(lái)看React.Component
class Welcome extends React.Component {
render() {
return Hello, {this.props.name}
;
}
}
從源碼看這個(gè)類寫(xiě)法做了什么react/packages/react/src/ReactBaseClasses.js
const emptyObject = {};
if (__DEV__) {
Object.freeze(emptyObject);
}
/**
* Base class helpers for the updating state of a 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;
}
Component.prototype.isReactComponent = {};
結(jié)構(gòu)比較簡(jiǎn)單,結(jié)合注釋可以知道只是基本賦值,里面有個(gè)更新器后面再說(shuō),現(xiàn)在先記住是用來(lái)更新State就行了.
/**
* Sets a subset of the state. Always use this to mutate
* state. You should treat `this.state` as immutable.
*
* There is no guarantee that `this.state` will be immediately updated, so
* accessing `this.state` after calling this method may return the old value.
*
* There is no guarantee that calls to `setState` will run synchronously,
* as they may eventually be batched together. You can provide an optional
* callback that will be executed when the call to setState is actually
* completed.
*
* When a function is provided to setState, it will be called at some point in
* the future (not synchronously). It will be called with the up to date
* component arguments (state, props, context). These values can be different
* from this.* because your function may be called after receiveProps but before
* shouldComponentUpdate, and this new state, props, and context will not yet be
* assigned to this.
*
* @param {object|function} partialState Next partial state or function to
* produce next partial state to be merged with current state.
* @param {?function} callback Called after state is updated.
* @final
* @protected
*/
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');
};
注釋很長(zhǎng)實(shí)際代碼很短,大概意思就是
- 不保證
this.state會(huì)立即更新,所以調(diào)用方法之后可能獲取的舊數(shù)據(jù) - 不保證
this.state會(huì)同步運(yùn)行,可能最終他們會(huì)批量組合執(zhí)行,可以提供一個(gè)可選完成回調(diào)當(dāng)更新之后再執(zhí)行 - 回調(diào)會(huì)在未來(lái)某個(gè)點(diǎn)執(zhí)行,可以拿到最新的入?yún)?state, props, context),不同于this.XX,因?yàn)樗鼤?huì)在shouldComponentUpdate之前接收新的props屬性之后執(zhí)行,此時(shí)還沒(méi)賦值給this.
/**
* Forces an update. This should only be invoked when it is known with
* certainty that we are **not** in a DOM transaction.
*
* You may want to call this when you know that some deeper aspect of the
* component's state has changed but `setState` was not called.
*
* This will not invoke `shouldComponentUpdate`, but it will invoke
* `componentWillUpdate` and `componentDidUpdate`.
*
* @param {?function} callback Called after update is complete.
* @final
* @protected
*/
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
這是強(qiáng)制更新視圖的方法
- 這應(yīng)該只在確定我們不是在一個(gè)DOM事務(wù)中時(shí)調(diào)用
- 這應(yīng)該在當(dāng)你知道某些深層嵌套組件狀態(tài)已變但是沒(méi)有執(zhí)行
setState的時(shí)候調(diào)用 - 它不會(huì)執(zhí)行
shouldComponentUpdate但會(huì)執(zhí)行componentWillUpdate和componentDidUpdate生命周期
接著我們看源碼 react/packages/shared/invariant.js做了什么
/**
* Use invariant() to assert state which your program assumes to be true.
*
* Provide sprintf-style format (only %s is supported) and arguments
* to provide information about what broke and what you were
* expecting.
*
* The invariant message will be stripped in production, but the invariant
* will remain to ensure logic does not differ in production.
*/
export default function invariant(condition, format, a, b, c, d, e, f) {
throw new Error(
'Internal React error: invariant() is meant to be replaced at compile ' +
'time. There is no runtime version.',
);
}
使用constant()斷言程序假定為真的狀態(tài),僅用于開(kāi)發(fā),會(huì)從生產(chǎn)環(huán)境中剝離保證不受影響
接下來(lái)我們?cè)倏纯?react/packages/react/src/ReactNoopUpdateQueue.js
/**
* This is the abstract API for an update queue.
*/
const ReactNoopUpdateQueue = {
/**
* Checks whether or not this composite component is mounted.
* @param {ReactClass} publicInstance The instance we want to test.
* @return {boolean} True if mounted, false otherwise.
* @protected
* @final
*/
isMounted: function(publicInstance) {
return false;
},
/**
* Forces an update. This should only be invoked when it is known with
* certainty that we are **not** in a DOM transaction.
*
* You may want to call this when you know that some deeper aspect of the
* component's state has changed but `setState` was not called.
*
* This will not invoke `shouldComponentUpdate`, but it will invoke
* `componentWillUpdate` and `componentDidUpdate`.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {?function} callback Called after component is updated.
* @param {?string} callerName name of the calling function in the public API.
* @internal
*/
enqueueForceUpdate: function(publicInstance, callback, callerName) {
warnNoop(publicInstance, 'forceUpdate');
},
/**
* Replaces all of the state. Always use this or `setState` to mutate state.
* You should treat `this.state` as immutable.
*
* There is no guarantee that `this.state` will be immediately updated, so
* accessing `this.state` after calling this method may return the old value.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {object} completeState Next state.
* @param {?function} callback Called after component is updated.
* @param {?string} callerName name of the calling function in the public API.
* @internal
*/
enqueueReplaceState: function(
publicInstance,
completeState,
callback,
callerName,
) {
warnNoop(publicInstance, 'replaceState');
},
/**
* Sets a subset of the state. This only exists because _pendingState is
* internal. This provides a merging strategy that is not available to deep
* properties which is confusing. TODO: Expose pendingState or don't use it
* during the merge.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {object} partialState Next partial state to be merged with state.
* @param {?function} callback Called after component is updated.
* @param {?string} Name of the calling function in the public API.
* @internal
*/
enqueueSetState: function(
publicInstance,
partialState,
callback,
callerName,
) {
warnNoop(publicInstance, 'setState');
},
};
export default ReactNoopUpdateQueue;
里面提供了三個(gè)函數(shù),分別是
- enqueueForceUpdate: 強(qiáng)制更新隊(duì)列包裝器
- enqueueReplaceState: 狀態(tài)替換隊(duì)列包裝器
- enqueueSetState: 狀態(tài)更新隊(duì)列包裝器
實(shí)際里面都是調(diào)用同一個(gè)方法warnNoop,設(shè)置首參數(shù)都一樣.
const didWarnStateUpdateForUnmountedComponent = {};
function warnNoop(publicInstance, callerName) {
if (__DEV__) {
const constructor = publicInstance.constructor;
const componentName =
(constructor && (constructor.displayName || constructor.name)) ||
'ReactClass';
const warningKey = `${componentName}.${callerName}`;
if (didWarnStateUpdateForUnmountedComponent[warningKey]) {
return;
}
warningWithoutStack(
false,
"Can't call %s on a component that is not yet mounted. " +
'This is a no-op, but it might indicate a bug in your application. ' +
'Instead, assign to `this.state` directly or define a `state = {};` ' +
'class property with the desired state in the %s component.',
callerName,
componentName,
);
didWarnStateUpdateForUnmountedComponent[warningKey] = true;
}
}
目測(cè)應(yīng)該是給傳入的React組件實(shí)例設(shè)置componentName和KEY, 在里面我們?cè)俅慰吹酵粋€(gè)方法warningWithoutStack,我們直接看看他究竟做了什么. react/packages/shared/warningWithoutStack.js
/**
* Similar to invariant but only logs a warning if the condition is not met.
* This can be used to log issues in development environments in critical
* paths. Removing the logging code for production environments will keep the
* same logic and follow the same code paths.
*/
let warningWithoutStack = () => {};
if (__DEV__) {
warningWithoutStack = function(condition, format, ...args) {
if (format === undefined) {
throw new Error(
'`warningWithoutStack(condition, format, ...args)` requires a warning ' +
'message argument',
);
}
if (args.length > 8) {
// Check before the condition to catch violations early.
throw new Error(
'warningWithoutStack() currently supports at most 8 arguments.',
);
}
if (condition) {
return;
}
if (typeof console !== 'undefined') {
const argsWithFormat = args.map(item => '' + item);
argsWithFormat.unshift('Warning: ' + format);
// We intentionally don't use spread (or .apply) directly because it
// breaks IE9: https://github.com/facebook/react/issues/13610
Function.prototype.apply.call(console.error, console, argsWithFormat);
}
try {
// --- Welcome to debugging React ---
// This error was thrown as a convenience so that you can use this stack
// to find the callsite that caused this warning to fire.
let argIndex = 0;
const message =
'Warning: ' + format.replace(/%s/g, () => args[argIndex++]);
throw new Error(message);
} catch (x) {}
};
}
export default warningWithoutStack;
類似invariant但是只有不滿足條件的時(shí)候才會(huì)打印出警告.這可以用于在關(guān)鍵路徑中記錄開(kāi)發(fā)環(huán)境中的問(wèn)題.生產(chǎn)環(huán)境下會(huì)移除日志代碼保證正常邏輯.代碼只是一些基本的條件設(shè)定和優(yōu)雅降級(jí)代碼.
還有一個(gè)類似的繼承類PureComponent,可以用于組件進(jìn)行淺對(duì)比決定是否需要更新
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
/**
* Convenience component with default shallow equality check for sCU.
*/
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;
}
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相似,也繼承自它的原型.但不繼承其自身的屬性方法.