React系列 --- createElement, ReactElement与Component部分源码解析(五)
React系列
React系列 --- 简单模拟语法(一)
React系列 --- Jsx, 合成事件与Refs(二)
React系列 --- virtualdom diff算法实现分析(三)
React系列 --- 从Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement与Component部分源码解析(五)
React系列 --- 从使用React了解Css的各种使用方案(六)
前言
因为之前写过一些文章分别关于怎么模拟React语法,React基本知识和virtualdom diff实现思路,接下来就跟着React源码大概了解一下怎么一个过程,只是主逻辑代码,忽略部分开发环境的代码.
以下解析仅限于我当时理解,不一定准确.
JSX编译
还是用之前的例子
<div className="num" index={1}> <span>123456</span> </div>
编译成
React.createElement("div", { className: "num", index: 1 }, React.createElement("span", null, "123456"));
createElement
我们看下API的语法
React.createElement( type, [props], [...children] )
创建并返回给定类型的新 React element 。
参数 | 描述 |
---|---|
type | 既可以是一个标签名称字符串,也可以是一个 React component 类型(一个类或一个函数),或者一个React fragment 类型 |
props | 各种属性值 |
children | 子元素 |
因为有babel会编译JSX,所以一般很少会直接调用这个方法.
然后我们进入找到对应的源码位置查看代码 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; } // 暂时还没联系上下文,保存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方法使用,如果有部分代码还不知道是干嘛的话也不用担心,下面会有说到
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
的取值器,使用时会执行对应方法进行报错不让读取.
至此相关源码基本了解了
getOwnPropertyDescriptor
上面其中核心方法介绍是这个
Object.getOwnPropertyDescriptor(obj, prop)
方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
参数 | 描述 |
---|---|
obj | 需要查找的目标对象 |
prop | 目标对象内属性名称 |
返回值 | 如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined |
属性描述符对象(property descriptor)
该方法允许对一个属性的描述进行检索。在 Javascript 中, 属性 由一个字符串类型的“名字”(name)和一个“属性描述符”(property descriptor)对象构成。
一个属性描述符是一个记录,由下面属性当中的某些组成的:
属性 | 描述 |
---|---|
value | 该属性的值(仅针对数据属性描述符有效) |
writable | 当且仅当属性的值可以被改变时为true。(仅针对数据属性描述有效) |
get | 获取该属性的访问器函数(getter)。如果没有访问器, 该值为undefined。(仅针对包含访问器或设置器的属性描述有效) |
set | 获取该属性的设置器函数(setter)。 如果没有设置器, 该值为undefined。(仅针对包含访问器或设置器的属性描述有效) |
configurable | 当且仅当指定对象的属性描述可以被改变或者属性可被删除时,为true。 |
enumerable | 当且仅当指定对象的属性可以被枚举出时,为 true。 |
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, }; // 开发模式下增改部分属性 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; };
整段代码来看它是在开发环境下对字段作处理:
- 创建React元素,设置对应属性值
开发环境下
- 创建
_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对象的相关源码大概知道了.
React组件创建
createClass
16.0.0以后已经废弃,可忽略
ES6类继承
从官方例子来看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
会同步运行,可能最终他们会批量组合执行,可以提供一个可选完成回调当更新之后再执行 - 回调会在未来某个点执行,可以拿到最新的入参(state, props, context),不同于this.XX,因为它会在shouldComponentUpdate之前接收新的props属性之后执行,此时还没赋值给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'); };
这是强制更新视图的方法
- 这应该只在确定我们不是在一个DOM事务中时调用
- 这应该在当你知道某些深层嵌套组件状态已变但是没有执行
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;
里面提供了三个函数,分别是
- enqueueForceUpdate: 强制更新队列包装器
- enqueueReplaceState: 状态替换队列包装器
- enqueueSetState: 状态更新队列包装器
实际里面都是调用同一个方法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
相似,也继承自它的原型.但不继承其自身的属性方法.