前言:今天偶有闲暇,突然想知道 虚拟DOM 是如何实现的,说到 虚拟DOM ,便不得不说说 createElement
方法。所以先看 createElement
方法,并于此记录一波。(下次在写这种源码指不定啥时候)
目前看的版本: 16.13.1
看源码准备工作
第一步:clone项目
第二步,搜索:createElement
, 搜索结果中,唯一能和定义沾边的就是这个 React.d.ts
文件了
第三步:根据 React.d.ts
,找到当前目录的 src
,接着找 React.js
(ts
语法我还没学过,不过看到 .ts,我就去找根目录下的同名文件,一般都能找到)。 看到这熟悉的声明语句,我就知道我找到了 。
const createElement = __DEV__ ? createElementWithValidation : createElementProd;
开始正式查看
createElement
方法在 开发模式中使用 createElementWithValidation
,在生产模式中使用 createElementProd
。(开发者模式和生产模式的区分变量 __DEV__
,为啥叫这个,一会我研究研究在发一篇文章,头一次考虑这么细)
一般在开发者模式中,注释和代码缩进都会很不错,所以看开发者模式的方法 createElementWithValidation
。
createElementWithValidation
, 顾名思义 创建_元素_经过_校验
。具体怎么校验和创建,咱们接着往下看。 (这个方法是相对引用的文件,怎么找我就不说了)
createElementWithValidation
方法第一句。玩呢。。上来就是一函数。。 先看名字 isValidElementType
,盲猜:是_有效_元素_类型
, 点击去瞅瞅。
得嘞,看完第一行的这个函数,咱们接着往下走。
为了方便,我直接在代码注释内写理解了
// We warn in this case but don't throw. We expect the element creation to
// succeed and there will likely be errors in render.
/***
在这种情况下我们会警告但不要抛出错误。我们希望元素创建成功,并且呈现中可能会有错误。
**/
if (!validType) { /*** 如果没有经过前面的类型校验 **/
let info = '';
/*** 如果是个 undefined 元素,或者 是个空对象, 我们就提示他:
您可能忘记从定义组件的文件中导出组件,或者您可能混淆了默认导入和命名导入。(不得不说这个提示是真的友好)
**/
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) {
/*** 如果存在引用信息,就提示出错路径和出错行数 **/
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;
}
/*** 如果是开发模式,就拼接错误,并输出 **/
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,第一大段落,错误验证就算结束了。接下来请看第二段,生成 element
元素
/*** 创建元素,一个大函数,容后再解释 **/
const element = createElement.apply(this, arguments);
// The result can be nullish if a mock or a custom function is used.
/*** 如果使用mock或自定义函数,则结果可能为空。 **/
// TODO: Drop this when these are no longer allowed as the type argument.
/*** 当不再允许这些参数作为类型参数时,删除此项。 **/
if (element == null) {
/*** 如果是 mock 或 自定义函数时, 返回 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.)
/***翻译
如果类型无效,则跳过密钥警告,因为我们的密钥验证逻辑不需要非字符串/函数类型,并且可能会引发混淆错误。
我们不希望生产模式和开发模式的异常处理有所不同。
(提示一条有帮助的消息,在类型被修复之前,将显示关键警告。)
**/
if (validType) {
for (let i = 2; i < arguments.length; i++) {
/*** 函数大意:
确保每个元素要么在静态位置传递,要么在定义了显式键属性的数组中传递,要么在具有有效键属性的对象文本中传递。 **/
validateChildKeys(arguments[i], type);
}
}
/*** 校验Props 有效性, 分为 fragment标签和 普通标签 **/
if (type === REACT_FRAGMENT_TYPE) {
validateFragmentProps(element);
} else {
validatePropTypes(element);
}
/*** 元素创建完毕,返回元素 **/
return element;
接下来,看重点。 createElement
方法 所在文件packages/react/src/ReactElement.js
首先,函数部分上来就是这些声明,这些声明也标志了一个 react组件
需要的参数
/*** props 名称 **/
let propName;
/*** 提取props ,放到 props变量内 **/
// Reserved names are extracted
const props = {};
let key = null; /*** 元素 key 值 **/
let ref = null; /*** 元素ref **/
let self = null; /*** 元素上一级 **/
let source = null; /*** 元素路径 **/
校验 ref
/*** 存在 ref ,并且有效 ,函数内 `Object.getOwnPropertyDescriptor(config, 'ref').get;`写的贼棒 **/
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
/*** 开发模式中,如果ref 无效,提示的内容,应该是新版才会用的 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
/*** 翻译
子项可以是多个参数,并且这些子项被转移到新分配的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
方法之后,首先看到的就是这一大串注释
我感觉挺有用,先翻译一波。
/***
* 方法介绍翻译:
一个用于创造 react元素的 格式化(???这么说应该没啥毛病)方法。
这里不再遵循class声明,请不要使用 new 来调用它。
另外,检查某个元素是否为react元素也不可以使用 instanceof 。
可以使用 $$typeof 是否为 Symbol.for('react.element')。
*
* @param {*} type
* @param {*} props
* @param {*} key
* @param {string|object} ref
* @param {*} owner
* @param {*} self 当调用React.createElement时,一个*临时*助手,用于检测“this”与“owner”是否不同,以便我们发出警告。我们希望去掉owner并用箭头函数替换字符串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') **/
/*** 翻译: 属于元素的内置属性 **/
// 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,
};
/*** 中间有一堆开发模式才有代码,这个先无视,下面再讲 **/
return element;
看到这里,有点轻微懵逼,竟然不是直接返回元素,返回的竟然是个对象,没有任何dom节点返回,设想:这里返回的对象,在经过框架的某个函数,将变成 真实的dom。
稍微提一下属性修饰词: MDN
- 形参1:obj : 要定义属性的对象。
- 形参2:prop :要定义或修改的属性的名称或 Symbol
-
形参3:desc:要定义或修改的属性描述符。
| desc描述 | 描述 | 默认值|
| ----------- | ----------- | -----------|
| configurable | 该属性的描述符才能够被改变,同时该属性是否可以被删除 | false |
| enumerable | 是否可枚举,遍历的时候是否显示这个属性 | false |
| value | 该属性的值 | undefined |
| writable | value值是否可以被改变 | false|
看一下开发模式才有的这些属性修饰词吧
if (__DEV__) {
/*** 翻译:
验证标志现在是可变的。我们把它放在一个外部后备存储器上,这样我们就可以冻结整个对象。一旦它们在常用的开发环境中执行(难道是在说变化?),就可以用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更容易用于测试,我们将验证标志设为不可枚举(在可能的情况下,它应该包括我们运行测试的每个环境),因此测试框架忽略它。
**/
// 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,
});
/*** 翻译:
在两个不同的地方创建的两个元素在测试时应该被认为是相等的,因此我们将其隐藏在枚举中。 **/
// 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,
});
/*** 如果可以,冻结 元素和 元素的 props 属性 **/
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
网友评论