React系列 --- 簡單模擬語法(一)
React系列 --- Jsx, 合成事件與Refs(二)
React系列 --- virtualdom diff算法實現分析(三)
React系列 --- 從Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement與Component部分源碼解析(五)
React系列 --- 從使用React瞭解Css的各類使用方案(六)
React系列 --- 從零構建狀態管理及Redux源碼解析(七)
React系列 --- 擴展狀態管理功能及Redux源碼解析(八)html
由於以前寫過一些文章分別關於怎麼模擬React語法,React基本知識和virtualdom diff實現思路,接下來就跟着React源碼大概瞭解一下怎麼一個過程,只是主邏輯代碼,忽略部分開發環境的代碼.
如下解析僅限於我當時理解,不必定準確.react
仍是用以前的例子git
<div className="num" index={1}> <span>123456</span> </div>
編譯成github
React.createElement("div", { className: "num", index: 1 }, React.createElement("span", null, "123456"));
咱們看下API的語法算法
React.createElement( type, [props], [...children] )
建立並返回給定類型的新 React element 。segmentfault
參數 | 描述 |
---|---|
type | 既能夠是一個標籤名稱字符串,也能夠是一個 React component 類型(一個類或一個函數),或者一個React fragment 類型 |
props | 各類屬性值 |
children | 子元素 |
由於有babel會編譯JSX,因此通常不多會直接調用這個方法.api
而後咱們進入找到對應的源碼位置查看代碼 react/packages/react/src/ReactElement.jsbabel
/** * 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; } // 暫時還沒聯繫上下文,保存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 < childrenLength; i++) { childArray[i] = arguments[i + 2]; } if (__DEV__) { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } if (__DEV__) { if (key || ref) { // 若是type是函數說明不是原生dom,因此能夠取一下幾個值 const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; // 定義key屬性的取值器,添加對應警告 if (key) { defineKeyPropWarningGetter(props, displayName); } // 定義ref屬性的取值器,添加對應警告 if (ref) { defineRefPropWarningGetter(props, displayName); } } } return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, ); }
代碼還比較簡單,能夠看出就是傳入參數以後它會幫你作些特殊處理而後導出給ReactElement方法使用,若是有部分代碼還不知道是幹嗎的話也不用擔憂,下面會有說到app
function hasValidRef(config) { // 開發環境下 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) { // 開發環境 if (__DEV__) { if (hasOwnProperty.call(config, 'key')) { const getter = Object.getOwnPropertyDescriptor(config, 'key').get; if (getter && getter.isReactWarning) { return false; } } } return config.key !== undefined; }
// 初始化標記 let specialPropKeyWarningShown, specialPropRefWarningShown; // 定義key的取值器 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; // 定義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, }); }
代碼來看是開發模式下限制了對應key
和ref
的取值器,使用時會執行對應方法進行報錯不讓讀取.dom
至此相關源碼基本瞭解了
上面其中核心方法介紹是這個
Object.getOwnPropertyDescriptor(obj, prop)
方法返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不須要從原型鏈上進行查找的屬性)
參數 | 描述 |
---|---|
obj | 須要查找的目標對象 |
prop | 目標對象內屬性名稱 |
返回值 | 若是指定的屬性存在於對象上,則返回其屬性描述符對象(property descriptor),不然返回 undefined |
該方法容許對一個屬性的描述進行檢索。在 Javascript 中, 屬性 由一個字符串類型的「名字」(name)和一個「屬性描述符」(property descriptor)對象構成。
一個屬性描述符是一個記錄,由下面屬性當中的某些組成的:
屬性 | 描述 |
---|---|
value | 該屬性的值(僅針對數據屬性描述符有效) |
writable | 當且僅當屬性的值能夠被改變時爲true。(僅針對數據屬性描述有效) |
get | 獲取該屬性的訪問器函數(getter)。若是沒有訪問器, 該值爲undefined。(僅針對包含訪問器或設置器的屬性描述有效) |
set | 獲取該屬性的設置器函數(setter)。 若是沒有設置器, 該值爲undefined。(僅針對包含訪問器或設置器的屬性描述有效) |
configurable | 當且僅當指定對象的屬性描述能夠被改變或者屬性可被刪除時,爲true。 |
enumerable | 當且僅當指定對象的屬性能夠被枚舉出時,爲 true。 |
而後再看看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, }; // 開發模式下增改部分屬性 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; };
整段代碼來看它是在開發環境下對字段做處理:
開發環境下
_store
屬性並配置其validated
的屬性描述符對象,達到方便調試React元素的目的_self
的屬性描述符對象,self
和source
只是DEV
的屬性_source
的屬性描述符對象,出於測試的目的,應該將在兩個不一樣位置建立的兩個元素視爲相等的,所以咱們將它從枚舉中隱藏起來。element
及其props
對象主要目的是爲提升測試環境下效率,將element的一些屬性配置爲不可枚舉,進行遍歷的時候跳過這些屬性。
其中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;
用來標識這個對象是一個ReactElement對象
,至此Jsx編譯成ReactElement對象的相關源碼大概知道了.
16.0.0之後已經廢棄,可忽略
從官方例子來看React.Component
class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
從源碼看這個類寫法作了什麼 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 = {};
結構比較簡單,結合註釋能夠知道只是基本賦值,裏面有個更新器後面再說,如今先記住是用來更新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'); };
註釋很長實際代碼很短,大概意思就是
this.state
會當即更新,因此調用方法以後可能獲取的舊數據this.state
會同步運行,可能最終他們會批量組合執行,能夠提供一個可選完成回調當更新以後再執行/** * 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'); };
這是強制更新視圖的方法
setState
的時候調用shouldComponentUpdate
但會執行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()斷言程序假定爲真的狀態,僅用於開發,會從生產環境中剝離保證不受影響
接下來咱們再看看 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;
裏面提供了三個函數,分別是
實際裏面都是調用同一個方法warnNoop
,設置首參數都同樣.
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; } }
目測應該是給傳入的React組件實例設置componentName和KEY, 在裏面咱們再次看到同一個方法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
可是隻有不知足條件的時候纔會打印出警告.這能夠用於在關鍵路徑中記錄開發環境中的問題.生產環境下會移除日誌代碼保證正常邏輯.代碼只是一些基本的條件設定和優雅降級代碼.
還有一個相似的繼承類PureComponent
,能夠用於組件進行淺對比決定是否須要更新
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
類似,也繼承自它的原型.但不繼承其自身的屬性方法.