React源码系列一之createElement

1次阅读

共计 7883 个字符,预计需要花费 20 分钟才能阅读完成。

前言:使用 react 也有二年多了,一直停留在使用层次。虽然很多时候这样是够了。但是总觉得不深入理解其背后是的实现逻辑,很难体会框架的精髓。最近会写一些相关的一些文章,来记录学习的过程。
备注:react 和 react-dom 源码版本为 16.8.6 本文适合使用过 REact 进行开发,并有一定经验的人阅读。
好了闲话少说,我们一起来看源码吧写过 react 知道,我们使用 react 编写代码都离不开 webpack 和 babel,因为 React 要求我们使用的是 class 定义组件,并且使用了 JSX 语法编写 HTML。浏览器是不支持 JSX 并且对于 class 的支持也不好,所以我们都是需要使用 webpack 的 jsx-loader 对 jsx 的语法做一个转换,并且对于 ES6 的语法和 react 的语法通过 babel 的 babel/preset-react、babel/env 和 @babel/plugin-proposal-class-properties 等进行转义。不熟悉怎么从头搭建 react 的可以看一下这篇文章
好了, 我们从一个最简单实例 demo 来看 react 到底做了什么
1、createElement
下面是我们的代码
import React from “react”;
import ReactDOM from “react-dom”;
ReactDOM.render(
<h1 style={{color:’red’}} >11111</h1>,
document.getElementById(“root”)
);
这是页面上的效果
我们现在看看在浏览器中的代码是如何实现的:
react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, {
style: {
color: ‘red’
}
}, “11111”), document.getElementById(“root”));
最终经过编译后的代码是这样的,发现原本的 <h1>11111</h1> 变成了一个 react.createElement 的函数,其中原生标签的类型,内容都变成了参数传入这个函数中. 这个时候我们大胆的猜测 react.createElement 接受三个参数,分别是元素的类型、元素的属性、子元素。好了带着我们的猜想来看一下源码。
我们不难找到,源码位置在位置 ./node_modules/react/umd/react.development.js:1941
function createElement(type, config, children) {
var propName = void 0;

// Reserved names are extracted
var props = {};

var key = null;
var ref = null;
var self = null;
var source = null;

if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = ” + config.key;
}

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$1.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.
var childrenLength = arguments.length – 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
{
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}

// Resolve default props
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
{
if (key || ref) {
var displayName = typeof type === ‘function’ ? type.displayName || type.name || ‘Unknown’ : type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
首先我们来看一下它的三个参数第一个 type:我们想一下这个 type 的可能取值有哪些?

第一种就是我们上面写的原生的标签类型 (例如 h1、div,span 等);
第二种就是我们 React 组件了,就是这面这种 App

class App extends React.Component {

static defaultProps = {
text: ‘DEMO’
}
render() {
return (<h1>222{this.props.text}</h1>)
}
}
第二个 config:这个就是我们传递的一些属性第三个 children:这个就是子元素,最开始我们猜想就三个参数,其实后面看了源码就知道这里其实不止三个。
接下来我们来看看 react.createElement 这个函数里面会帮我们做什么事情。1、首先会初始化一些列的变量,之后会判断我们传入的元素中是否带有有效的 key 和 ref 的属性,这两个属性对于 react 是有特殊意义的(key 是可以优化 React 的渲染速度的,ref 是可以获取到 React 渲染后的真实 DOM 节点的),如果检测到有传入 key,ref,__self 和__source 这 4 个属性值,会将其保存起来。
2、接着对传入的 config 做处理,遍历 config 对象,并且剔除掉 4 个内置的保留属性 (key,ref,__self,__source),之后重新组装新的 config 为 props。这个 RESERVED_PROPS 是定义保留属性的地方。
var RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true
};
3、之后会检测传入的参数的长度,如果 childrenLength 等于 1 的情况下,那么就代表着当前 createElement 的元素只有一个子元素,那么将内容赋值到 props.children。那什么时候 childrenLength 会大于 1 呢?那就是当你的元素里面涉及到多个子元素的时候,那么 children 将会有多个传入到 createElement 函数中。例如:
ReactDOM.render(
<h1 style={{color:’red’}} key=’22’>
<div>111</div>
<div>222</div>
</h1>,
document.getElementById(“root”)
);
编译后是什么样呢?
react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, {
style: {
color: ‘red’
},
key: “22”
},
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“div”, null, “111”),
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“div”, null, “222”)),
document.getElementById(“root”)
);
这个时候 react.createElement 拿到的 arguments.length 就大于 3 了。也就是 childrenLength 大于 1。这个时候我们就遍历把这些子元素添加到 props.children 中。4、接着函数将会检测是否存在 defaultProps 这个参数,因为现在的是一个最简单的 demo,而且传入的只是原生元素,所以没有 defaultProps 这个参数。那么我们来看下面的例子:
import React, {Component} from “react”;
import ReactDOM from “react-dom”;
class App extends Component {
static defaultProps = {
text: ‘33333’
}
render() {
return (<h1>222{this.props.text}</h1>)
}
}
ReactDOM.render(
<App/>,
document.getElementById(“root”)
);
编译后的
var App =
/*#__PURE__*/
function (_Component) {
_inherits(App, _Component);

function App() {
_classCallCheck(this, App);

return _possibleConstructorReturn(this, _getPrototypeOf(App).apply(this, arguments));
}

_createClass(App, [{
key: “render”,
value: function render() {
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(“h1”, null, “222”, this.props.text);
}
}]);

return App;
}(react__WEBPACK_IMPORTED_MODULE_0__[“Component”]);

_defineProperty(App, “defaultProps”, {
text: ‘33333’
});

react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(App, null),
document.getElementById(“root”)
);
发现传入 react.createElement 的是一个 App 的函数,class 经过 babel 转换后会变成一个构造函数。有兴趣可以自己去看 babel 对于 class 的转换,这里就不解析转换过程,总得来说就是返回一个 App 的构造函数传入到 react.createElement 中. 如果 type 传的东西是个对象,且 type 有 defaultProps 这个东西并且 props 中对应的值是 undefined,那就 defaultProps 的值也塞 props 里面。这就是我们组价默认属性的由来。
5、检测 key 和 ref 是否有赋值,如果有将会执行 defineKeyPropWarningGetter 和 defineRefPropWarningGetter 两个函数。
function defineKeyPropWarningGetter(props, displayName) {
var warnAboutAccessingKey = function () {
if (!specialPropKeyWarningShown) {
specialPropKeyWarningShown = true;
warningWithoutStack$1(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;
Object.defineProperty(props, ‘key’, {
get: warnAboutAccessingKey,
configurable: true
});
}

function defineRefPropWarningGetter(props, displayName) {
var warnAboutAccessingRef = function () {
if (!specialPropRefWarningShown) {
specialPropRefWarningShown = true;
warningWithoutStack$1(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 添加了警告。这个应该只是在开发环境才有其中 isReactWarning 就是上面判断 key 与 ref 是否有效的一个标记。6、最后将一系列组装好的数据传入 ReactElement 函数中。
2、ReactElement
var ReactElement = function (type, key, ref, self, source, owner, props) {
var element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner
};

{
element._store = {};
Object.defineProperty(element._store, ‘validated’, {
configurable: false,
enumerable: false,
writable: true,
value: false
});

Object.defineProperty(element, ‘_self’, {
configurable: false,
enumerable: false,
writable: false,
value: self
});

Object.defineProperty(element, ‘_source’, {
configurable: false,
enumerable: false,
writable: false,
value: source
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}

return element;
};
其实里面非常简单,就是将传进来的值都包装在一个 element 对象中
$$typeof: 其中 REACT_ELEMENT_TYPE 是一个常量,用来标识该对象是一个 ReactElement
var hasSymbol = typeof Symbol === ‘function’ && Symbol.for;
var REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for(‘react.element’) : 0xeac7;
从代码上看如果支持 Symbol 就会用 Symbol.for 方法创建一个 key 为 react.element 的 symbol, 否则就会返回一个 0xeac7

type -> tagName 或者是一个函数
key -> 渲染元素的 key
ref -> 渲染元素的 ref
props -> 渲染元素的 props
_owner -> Record the component responsible for creating this element.(记录负责创建此元素的组件, 默认为 null)
_store -> 新的对象

_store 中添加了一个新的对象 validated(可写入),element 对象中添加了_self 和_source 属性(只读),最后冻结了 element.props 和 element。这样就解释了为什么我们在子组件内修改 props 是没有效果的,只有在父级修改了 props 后子组件才会生效
最后就将组装好的 element 对象返回了出来,提供给 ReactDOM.render 使用。到这有关的主要内容我们看完了。下面我们来补充一下知识点
Object.freeze
Object.freeze 方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。
const obj = {
a: 1,
b: 2
};

Object.freeze(obj);

obj.a = 3; // 修改无效
需要注意的是冻结中能冻结当前对象的属性,如果 obj 中有一个另外的对象,那么该对象还是可以修改的。所以 React 才会需要冻结 element 和 element.props。
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}

正文完
 0