资讯专栏INFORMATION COLUMN

React源码系列一之createElement

Zhuxy / 2314人阅读

前言:使用react也有二年多了,一直停留在使用层次。虽然很多时候这样是够了。但是总觉得不深入理解其背后是的实现逻辑,很难体会框架的精髓。最近会写一些相关的一些文章,来记录学习的过程。

备注:react和react-dom源码版本为16.8.6 本文适合使用过React进行开发,并有一定经验的人阅读。

好了闲话少说,我们一起来看源码吧
写过react知道,我们使用react编写代码都离不开webpackbabel,因为React要求我们使用的是class定义组件,并且使用了JSX语法编写HTML。浏览器是不支持JSX并且对于class的支持也不好,所以我们都是需要使用webpack的jsx-loaderjsx的语法做一个转换,并且对于ES6的语法和react的语法通过babelbabel/preset-reactbabel/env@babel/plugin-proposal-class-properties等进行转义。不熟悉怎么从头搭建react的我的示例代码就放在这。

好了,我们从一个最简单实例demo来看react到底做了什么

1、createElement

下面是我们的代码

import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(
    

11111

, 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"));

最终经过编译后的代码是这样的,发现原本的

11111

变成了一个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的可能取值有哪些?

第一种就是我们上面写的原生的标签类型(例如h1div,span等);

第二种就是我们React组件了,就是这面这种App

class App extends React.Component {

    static defaultProps = {
        text: "DEMO"
    }
    render() {
        return (

222{this.props.text}

) } }

第二个config:这个就是我们传递的一些属性
第三个children:这个就是子元素,最开始我们猜想就三个参数,其实后面看了源码就知道这里其实不止三个。

接下来我们来看看react.createElement这个函数里面会帮我们做什么事情。
1、首先会初始化一些列的变量,之后会判断我们传入的元素中是否带有有效的keyref的属性,这两个属性对于react是有特殊意义的(key是可以优化React的渲染速度的,ref是可以获取到React渲染后的真实DOM节点的),如果检测到有传入key,ref,__self__source这4个属性值,会将其保存起来。

2、接着对传入的config做处理,遍历config对象,并且剔除掉4个内置的保留属性(key,ref,__self,__source),之后重新组装新的configprops。这个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(
        

111
222

, 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 (

222{this.props.text}

) } } ReactDOM.render( , 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传的东西是个对象,且typedefaultProps这个东西并且props中对应的值是undefined,那就defaultProps的值也塞props里面。这就是我们组价默认属性的由来。

5、 检测keyref是否有赋值,如果有将会执行defineKeyPropWarningGetterdefineRefPropWarningGetter两个函数。

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
  });
}

我么可以看出这个二个方法就是给keyref添加了警告。这个应该只是在开发环境才有其中isReactWarning就是上面判断keyref是否有效的一个标记。
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方法创建一个keyreact.elementsymbol,否则就会返回一个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.propselement
这样就解释了为什么我们在子组件内修改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第一时间发布,欢迎关注。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103283.html

相关文章

  • React 源码深度解读(四):首次自定义组件渲染 - Part 1

    摘要:本篇开始介绍自定义组件是如何渲染的。组件将自定义组件命名为,结构如下经过编译后,生成如下代码构建顶层包装组件跟普通元素渲染一样,第一步先会执行创建为的。调用顺序已在代码中注释。先看图,这部分内容将在下回分解 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非...

    Warren 评论0 收藏0
  • React 源码深度解读(一):首次DOM元素渲染 - Part 1

    摘要:调用栈是这样的这里生成的我们将其命名为,它将作为参数传入到。整个的调用栈是这样的组件间的层级结构是这样的到此为止,顶层对象已经构造完毕,下一步就是调用来自的方法,进行页面的渲染了。通过表达的结构最终会转化为一个纯对象,用于下一步的渲染。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言...

    daydream 评论0 收藏0
  • 零基础如何学爬虫技术

    摘要:楚江数据是专业的互联网数据技术服务,现整理出零基础如何学爬虫技术以供学习,。本文来源知乎作者路人甲链接楚江数据提供网站数据采集和爬虫软件定制开发服务,服务范围涵盖社交网络电子商务分类信息学术研究等。 楚江数据是专业的互联网数据技术服务,现整理出零基础如何学爬虫技术以供学习,http://www.chujiangdata.com。 第一:Python爬虫学习系列教程(来源于某博主:htt...

    KunMinX 评论0 收藏0
  • 剖析 React 源码:先热个身

    摘要:我们先来看下这个函数的一些神奇用法对于上述代码,也就是函数来说返回值是。不管你第二个参数的函数返回值是几维嵌套数组,函数都能帮你摊平到一维数组,并且每次遍历后返回的数组中的元素个数代表了同一个节点需要复制几次。这是我的 React 源码解读课的第一篇文章,首先来说说为啥要写这个系列文章: 现在工作中基本都用 React 了,由此想了解下内部原理 市面上 Vue 的源码解读数不胜数,但是反观...

    sean 评论0 收藏0
  • React系列 --- createElement, ReactElement与Component部

    摘要:仅针对数据属性描述有效获取该属性的访问器函数。当且仅当指定对象的属性可以被枚举出时,为。冻结及其对象主要目的是为提高测试环境下效率,将的一些属性配置为不可枚举,进行遍历的时候跳过这些属性。 React系列 React系列 --- 简单模拟语法(一)React系列 --- Jsx, 合成事件与Refs(二)React系列 --- virtualdom diff算法实现分析(三)React...

    xuhong 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<