React 系列
React 简单模拟语法 (一)
Jsx, 合成事件与 Refs(二)
virtualdom diff 算法实现分析(三)
从 Mixin 到 HOC 再到 HOOKS(四)
前言
因为之前写过一些文章分别关于怎么模拟 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
相似, 也继承自它的原型. 但不继承其自身的属性方法.