小前端读源码 - React16.7.0(一)

2022-09-26 10:36:55 浏览数 (1)

2019年,前端的情况暂时还是三足鼎立的局面,React,Vue和Angular。平常开发中我们基本上离不开框架的使用,但是大部分人也只是了解如何使用,或者深入一点的就是知道用什么框架做什么样的功能会有什么样的坑(经验所谈)。但是又有多少人愿意去认真读一读框架的源码,深入理解背后的逻辑呢?

因为现在所在公司使用的是React,那么我将会一连串的写好几篇关于React的源码阅读文章,一步一步深入去了解React背后的一些原理。

了解源码我们就从我们怎么去写一个页面一步一步去看每个Api里面到底做了什么,我们就先从一个最简单的DEMO开始去了解一个页面是怎么从react渲染出来到浏览器中的。

在阅读之前我们先要知道的是,我们使用react编写代码都离不开webpack和babel,因为React要求我们使用的是class定义组件,并且使用了JSX语法编写HTML。浏览器是不支持JSX并且对于class的支持也不好,所以我们都是需要使用webpack的jsx-loader对jsx的语法做一个转换,并且对于ES6的语法和react的语法通过babel的babel/preset-react、babel/preset-react和babel/preset-stage-0等进行转义(babel)。

备注:react和react-dom源码版本为16.7.0

react.createElement

最简单的就是直接使用ReactDOM.render渲染一个原生的HTML元素到页面中。

代码语言:javascript复制
import React from 'react';
import ReactDOM from 'react-dom';


ReactDOM.render(
    <h1>DEMO</h1>,
    document.getElementById('root')
)

渲染效果:

我们现在看看在浏览器中的代码是如何实现的:

最终经过编译后的代码是这样的,发现原本的<h1>DEMO</h1>变成了一个react.createElement的函数,其中原生标签的类型,内容都变成了参数传入这个函数中。发现第二个参数是null,那么我们改变一下DEMO的代码。

代码语言:javascript复制
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
    <h1 style={{coloe:'red'}} data-url='123123'>DEMO</h1>,
    document.getElementById('root')
)

再运行一下

那就很清晰的知道react.createElement函数有3个参数,分别是元素的类型,元素的属性,元素的innerText。为了印证我的想法,去看看源码。

  • 位置 - ./node_modules/react/umd/react.development.js:1892

接下来我们来看看react.createElement这个函数里面会帮我们做什么事情。

  1. 首先会初始化一些列的变量,之后会判断我们传入的元素中是否带有key和ref的属性,这两个属性对于react是有特殊意义的,如果检测到有传入key,ref,__self__source这4个属性值,会将其保存起来。
  2. 之后对传入的config做处理,循环config对象,并且剔除掉4个内置属性值(key,ref,__self,__source),之后重新组装新的config为props。
  3. 之后会检测传入的参数的长度,如果childrenLength等于1的情况下,那么就代表着当前createElement的元素没有子元素,只有文字或者是空,那么将内容赋值到props.children。那什么时候childrenLength会大于1呢?那就是当你的元素里面涉及到嵌套子元素的时候,那么children将会有多个传入到createElement函数中,这个之后再做详解,现在只针对最简单的DEMO去说明。
  4. 接着函数将会检测是否存在defaultProps这个参数,因为现在的DEMO是一个最简单的DEMO,而且传入的只是原生元素,所以没有defaultProps这个参数,我们先忽略。
  5. 检测key和ref是否有赋值,如果有将会执行defineKeyPropWarningGetterdefineRefPropWarningGetter两个函数。
  6. 最后将一系列组装好的数据传入ReactElement函数中。

以上就是最简单的DEMO中的createElement的流程,在上面的例子中有几点没有详细去说明:

  • defineKeyPropWarningGetterdefineRefPropWarningGetter两个函数是干什么的?
  • 什么情况下childrenLength大于1?
  • ReactElement是干嘛的?

问题1:defineKeyPropWarningGetterdefineRefPropWarningGetter两个函数是干什么的呢,首先我们知道key是可以优化React的渲染速度的,ref是可以获取到React渲染后的真实DOM节点的。难道在react.createElement里面就处理了吗?当然不是,我们看一下代码就知道了。

代码语言:javascript复制
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添加到即将传入ReactElement函数的props对象中而已。并且对get绑定了一个函数,当尝试获通过props获取key和ref的时候会出现警告。(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))。

问题2:什么情况下childrenLength大于1?

其实我们只需要将我们的DEMO改一下,将原来的h1改成一个div,并且嵌套2个h1元素就可以发现childrenLength大于1了。

代码语言:javascript复制
import React from 'react';
import ReactDOM from 'react-dom';


ReactDOM.render(
    <div style={{coloe:'red'}} data-url='123123'><h1>DEMO1</h1><h1>DEMO2</h1></div>,
    document.getElementById('root')
)

这个时候代码编译后会对每一个元素调用一次react.createElement函数。

代码语言:javascript复制
__WEBPACK_IMPORTED_MODULE_1_react_dom___default.a.render(
  __WEBPACK_IMPORTED_MODULE_0_react___default.a.createElement(
    "div", 
    {
      style: {
        coloe: 'red'
      },
      "data-url": "123123"
    }, 
    __WEBPACK_IMPORTED_MODULE_0_react___default.a.createElement("h1", null, "DEMO1"), 
    __WEBPACK_IMPORTED_MODULE_0_react___default.a.createElement("h1", null, "DEMO2")
  ), document.getElementById('root'));

这个时候react.createElement拿到的arguments.length就大于3了。

而children就是那两个嵌套的h1元素经过ReactElement函数后返回的数据结构了。(调试过React的你一定不陌生)

但是现在的都只是最简单的DEMO,我们在开发业务的时候,经常写的并不是一个原生元素,而是一个class,那么传入的是一个class会有什么不一样呢?我们来试试!

代码语言:javascript复制
import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {

    static defaultProps = {
        text: 'DEMO'
    }
    render() {
        return (<h1>{this.props.text}</h1>)
    }
}

ReactDOM.render(
    <App/>,
    document.getElementById('root')
)

浏览器的代码:

发现传入react.createElement的是一个App的函数,class经过babel转换后会变成一个构造函数。有兴趣可以自己去看babel对于class的转换,这里就不解析转换过程,总得来说就是返回一个App的够着函数传入到react.createElement中。

如果传入的react.createElement的type(第一个参数),如果是一个原生元素,那么将会是原生的tagName,是一个字符串,所以在react.createElement中尝试获取传入的type是否存在defaultProps是获取不到的,只有type是一个函数,那么该函数的静态变量defaultProps才会被获取得到并且循环defaultProps对象将key和value保存到props中,之后传入ReactElement函数中。

在执行的过程中,App的render其实也会在传入到ReactElement函数中后执行,其实也是调用react.createElement函数。阅读到这里,其实已经大概知道第一次渲染的时候,整个流程是怎样的了。

到这里为止,我们发现createElement最终都会将参数传入一个ReactElement的函数内,然后return出来一个对象,那么这个函数里面到底是做了什么呢?

其实里面非常简单,就是将传进来的值都包装在一个element对象中。element对象中包裹了以下属性:

  • $$typeof -> 标识react原生
  • type -> tagName或者是一个函数
  • key -> 渲染元素的key
  • ref -> 渲染元素的ref
  • props -> 渲染元素的props
  • _owner -> Record the component responsible for creating this element.(记录负责创建此元素的组件)
  • _store -> 新的对象

_store中添加了一个新的对象validated(可写入),element对象中添加了_self和_source属性(只读),最后冻结了element.props和element。这样就解释了为什么我们在子组件内修改props是没有效果的,只有在父级修改了props后子组件才会生效。

最后就将组装好的element对象返回了出来,提供给ReactDOM.render使用。

下一篇继续阅读ReactDOM.render如何将react.createElement返回出来的对象解析成虚拟DOM以及如何渲染到页面中。


补充知识

在阅读源码的时候会有一些平常比较少用的API,这里也做一些记录。

Symbol

Symbol是ES6新出的一个新的数据类型,返回的值是唯一属性标识。

代码语言:javascript复制
const a1 = Symbol('a');
const a2 = Symbol('a');
a1 == a2 // false
a1 === a2 // false

Symbol也提供全局共享标识的方法,分别是Symobl.for和Symobl.keyFor。大概的使用如下:

代码语言:javascript复制
const a1 = Symbol.for('a');
const a2 = Symbol.for('a');
a1 == a2 // true
a1 === a2 // true

Symbol.keyFor(a1) // a
Symbol.keyFor(a2) // a

Object.freeze

Object.freeze方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。

代码语言:javascript复制
const obj = {
    a: 1,
    b: 2
};

Object.freeze(obj);

obj.a = 3; // 修改无效

需要注意的是冻结中能冻结当前对象的属性,如果obj中有一个另外的对象,那么该对象还是可以修改的。所以React才会需要冻结element和element.props。

0 人点赞