React源码系列一之createElement
前言:使用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); }
后续更多文章将在我的github第一时间发布,欢迎关注。