我对jsx转变过程的理解

100 阅读6分钟

引入

首先我们先写一段jsx代码,看看会被babel转化成什么:

    <div style={{ marginTop: "100px" }}>
      <div>hello world</div>
      <React.Fragment>
        <div>hello</div>
      </React.Fragment>
      hello
      {toLearn.map((item) => (
        <div key={item}>{item}</div>
      ))}
      {status ? <TextComponent></TextComponent> : <div>三元运算符</div>}
          {renderFoot()}
          {console.log(JSXComponent)}
    </div>

我们可以通过babel 编译为Babel · The compiler for next generation JavaScript (babeljs.io)

/*#__PURE__*/React.createElement("div", {
  style: {
    marginTop: "100px"
  }
}, /*#__PURE__*/React.createElement("div", null, "hello world"), /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", null, "hello")), "hello", toLearn.map(item => /*#__PURE__*/React.createElement("div", {
  key: item
}, item)), status ? /*#__PURE__*/React.createElement(TextComponent, null) : /*#__PURE__*/React.createElement("div", null, "\u4E09\u5143\u8FD0\u7B97\u7B26"), renderFoot(), console.log(JSXComponent));

我们可以看到无论是组件还是标签,他都会变为react.createElement()

那么其实我们可以来看看createElement是干什么。

正文

createElement

参考:React 顶层 API – React (reactjs.org)

定义: createElement(type,props,children)

  • type: 标签名(组件名或者DOM名)

    • 如果是DOM标签名,那么用字符串表示
    • 如果是组件名,那么直接写组件名
  • props:属性

    就是标签上面有没有附加的属性,例如,click,style等

    例子

    <h1 color=“white”>
        标题
    </h1>
    

    props;是一种对象形式

  • children:子元素

    如果这个组件他是父组件,那么他的所有子组件都将要放在children里面

那么现在我们来详细看一下createElement:react/ReactElement.js at 17.0.2 · facebook/react (github.com)

在看createElement之前,我们先来看一下reactElement,它是用来创建虚拟DOM元素的

const ReactElement = function (type, key, ref, self, source, owner, props) {
    const element = {
        $$typeof: REACT_ELEMENT_TYPE,
        // 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,
    };
    if (__DEV__) {
        // 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 = {};
        // 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 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,
        });
        if (Object.freeze) {
            Object.freeze(element.props);
            Object.freeze(element);
        }
    }

    return element;
};

大致意思:

在开发环境的时候,将_store,_self,_source设置为不可枚举的状态。

这边可以对Object.defineProperty进行一点复习Object.defineProperty() - JavaScript | MDN (mozilla.org),他就是在一个对象定义一个新的属性,并设置属性值。他添加的属性,默认情况是不可以被修改的,除非改变的他的configurable或者writable

object.freeze():防止后续代码添加或者删除对象原型的属性,大概就是,只要被冻住了,啥也干不了。但是它只对一层有效,如果一个属性的值是个对象,则这个对象中的属性是可以修改的

const obj = {
  prop: 42,
  a:{
    value:3
  	}
};
const obj1 =  Object.freeze(obj);
console.log(obj)
console.log(obj1)

obj1.prop = 33;
obj.a.value = 4;
// Throws an error in strict mode
console.log(obj)
console.log(obj1)
> Object { prop: 42, a: Object { value: 3 } }
> Object { prop: 42, a: Object { value: 3 } }
> Object { prop: 42, a: Object { value: 4 } }
> Object { prop: 42, a: Object { value: 4 } }

那么现在我们看一下createElement

export function createElement(type, config, children) {
    let propName;
    // Reserved names are extracted
    // 用于获取所有的标签属性
    const props = {};

    let key = null;
    let ref = null;
    let self = null;
    let source = null;
    //判断是不是由标签的属性之类的
    // 获取出合法的key,ref,self,source
    if (config != null) {
        // eslint-disable-next-line no-undef
        if (hasValidRef(config)) {
            ref = config.ref;
            if (__DEV__) {
                warnIfStringRefCannotBeAutoConverted(config);
            }
        }
        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
        // 并且将出来上面四个的属性都放到props对象里
        for (propName in config) {
            if (
                hasOwnProperty.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.
    // 判断开始和结束标签里面有多少组件(元素)
    const childrenLength = arguments.length - 2;
    // 当值为1时,说明,只有一个组件或元素
    // 那么直接复制到props.children就好了
    if (childrenLength === 1) {
        props.children = children;
    } else if (childrenLength > 1) {
        // 如果大于1,那么就说明,里面的组件不止一个,那么就用数组存起来
        const childArray = Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
            childArray[i] = arguments[i + 2];
        }
        if (__DEV__) {
            if (Object.freeze) {
                Object.freeze(childArray);
            }
        }
        props.children = childArray;
    }

    // Resolve default props
    // 看看这个标签有没有默认的属性,也加上去
    if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
            if (props[propName] === undefined) {
                props[propName] = defaultProps[propName];
            }
        }
    }
    if (__DEV__) {
        if (key || ref) {
            const 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,
    );
}

主要逻辑:

  • 他首先会从config获取到self,source,key,ref这四个,然后将它们保存起来,这个是需要传给reactElement

  • 然后我们会将config其他的属性,或者是标签的默认属性等全部都放到props对象里面。

  • 然后我们会判断这个元素是不是父组件children属性。其实判断的依据也比较好理解

    判断jsx被转变过来的babel的createElement里的参数个数

    <3:那么说明,只有typeconfig参数,没有子元素

    ==3:那么也就是,children占到了一个,那么就说明只有一个子元素,那就直接赋值过去

    >3:那么就说明子元素不止一个,那么propschildren就需要遍历往里面传

但是react17以后,jsx将不会转变为reactElement而是从babelpackage中引入新的入口,并调用。

function App() {
  return <h1>Hello World</h1>;
}

会转变为:

// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: 'Hello world' });
}

这个是由编译器转换的,不需要你手动的去进行。

他转化完就是可以直接供reactDOM.render()reactElement对象

Q:

这也就是为什么在react17前,react虽然没用到,但是也需要引入的原因

A:

因为当时jsx转换为reactElement对象的时候,需要借助React.createElement

但是react17以后,可以直接从新的入口引入,不需要借助react了。

但是官方还是建议,引入react的,以便使用react提供的Hooks或者其他导出

参考链接:

介绍全新的 JSX 转换 – React Blog (reactjs.org)

React17源码解析(2) —— jsx 转换及 React.createElement - 掘金 (juejin.cn)

最后会在协调和调度阶段,所有的react Element对象都会转变为filber对象

首先我们先来介绍一下,协调和调度

reconciliation:协调

我理解就是,协调其实就是和diff算法挂钩的

  1. props或者state发生改变

  2. 那么就会调用render()函数,生成一个VDOM

  3. 然后通过diff算法来比对这两个树,并进行更新

  4. 最后渲染为真实的DOM

diff算法

同层比较,不同的节点产生不同的树,react会优先比较两个树的根节点。

  • 当根节点类型不同时

    那么就会触发完整的重建流程,就是重新创建一个以新的节点为根节点的树

    并且会将旧的根节点以下的子节点,状态等全部删除。

    例如:当前节点是<h1>现在变为了<img>,那么就要重新构建

  • 当根节点类型相同时

    那么react就会保留这个DOM节点,仅对需要更改的属性进行更改

    例如:

    <div style={{color: 'red', fontWeight: 'bold'}} />
    
    <div style={{color: 'green', fontWeight: 'bold'}} />
    

    react在进行更新的时候,只会去更新color,其他都不会变

对子元素采用,递归,因为他只比较节点类型,所以在遍历的时候尽量使用key

当子元素有key的时候,react会根据key来判断

例如:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

类似于这样,他就不会将子元素属性改变,他会将20152016后移,在前面添加2014

schedule:调度

大概意思就是,不需要你去调用组件,react会调用,他会根据,优先级,时间来进行调用

在协调阶段,react会将react Element元素转变为fiber对象。

filber

每一个filber有都有三种关系

  • child:子节点
  • return:父节点
  • sibling:兄弟节点

大致流程:

  • 首先从根react Element开始遍历,并为他创建filber对象
  • 然后遍历他的子节点,为子节点也创建filber对象,直到到达了叶子节点
  • 然后去检查有没有兄弟节点,遍历兄弟节点,然后兄弟的子节点
  • 如果没有兄弟节点,返回父节点

对于类似于map便利的,他们会在外层加上fragment,map返回数组结构作为fragment的子节点

参考链接:

React Fiber 简介 —— React 背后的算法 - 掘金 (juejin.cn)

协调 – React (reactjs.org)