likes
comments
collection
share

React系列: jsx转化为虚拟DOM

作者站长头像
站长
· 阅读数 21

直接进入主题。

const element = <div>copyer</div>

上面的标签语法既不是字符串也是HTML,被称为JSX,JavaScript的语法扩展。

React中推荐使用JSX语法。(当然,其他框架也是可以使用的,比如现在的vue3中,也是支持JSX语法的)。

为什么要使用JSX语法?

React 认为渲染逻辑本质上与其他 UI 逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。(官方原话)

那么使用JSX语法,react内部是怎么解析的呢?转化为真实的DOM。

React 16版本

jsx语法通过babel转化为React.createElement函数调用,生成虚拟对象。

Babel在线试一试

JSX形式

function App() {
  return <h1 className='ppt'>Hello World</h1>;
}

React.createElement函数形式

"use strict";

function App() {
  return /*#__PURE__*/React.createElement("h1", {
    className: "ppt"
  }, "Hello World");
}

React.createElement参数分析

/**
 *
 * @param {*} type 元素的类型
 * @param {*} config 配置对象
 * @param {*} children 第一个儿子,如果有多个,依次放在后面
 */
function createElement(type, config, children) {}

React 17版本

React 17 提供了一个全新的,重构过的 JSX 转换的版本。jsx语法不再化为 React.createElement函数,而是内部通过 react/jsx-runtimejsx函数生成虚拟对象。

官网解释JSX transform

jsx 新的转化,也是通过 babel完成的。两种方式

@babel/plugin-transform-react-jsx

// If you're using @babel/plugin-transform-react-jsx
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx", {
      "runtime": "automatic"
    }]
  ]
}

@babel/preset-react

// If you are using @babel/preset-react
{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic"
    }]
  ]

示例:

jsx形式

function App() {
  return <h1 className='ppt'>Hello World</h1>;
}

new JSX transform形式

// Inserted by a compiler (don't import it yourself!)
import {jsx as _jsx} from 'react/jsx-runtime';

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

对比

createElement函数与jsx函数的区别

参数的不同

第一个参数,都是元素的类型

  • createElement函数 第二个参数:元素的配置对象; 第三个参数,表示它的第一个子节点,如果有多个子节点,就依次的从四个参数往下方。(简单的来说,从第三个参数开始,都是元素的子节点)
  • jsx函数,第二个参数就是一个对象,里面包含着元素的配置对象({className: 'aa', children: []})。children属性就是表示该元素的子节点。如果只有一个,就是react元素;如果有多个,就是一个数组,里面存放着所有的子节点。
新的jsx transform的好处
  • 在React16版本,每个组件都必须导入React,不然就会报错。在新的jsx transform中不用导入。(因为新的 JSX 转换会自动引入必要的 react/jsx-runtime 函数,因此当你使用 JSX 时,将无需再引入 React。)
  • JSX 的编译输出可能会略微改善 bundle 的大小
createElement函数没有废弃

尽管新的 jsx 编译已经出来,但是并没有废弃 createElement函数。

如果想要使用js创建元素,还是要使用 createElement。

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

源码分析

createElement函数源码

packages/react/src/ReactElement.js

// 保留的props
const RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true,
};

// react元素的类型
const REACT_ELEMENT_TYPE = Symbol.for("react.element");

// 判断是不是有效的ref
function hasValidRef(config) {
  return config.ref !== undefined;
}

// 判断是不是有效的key
function hasValidKey(config) {
  return config.key !== undefined;
}

const ReactCurrentOwner = {
  current: null, // (null: null | Fiber)
};

// react元素对象(虚拟节点)
const ReactElement = function (type, key, ref, self, source, owner, props) {
  const element = {
    // react元素的唯一标识
    $$typeof: REACT_ELEMENT_TYPE,

    // react元素的属性
    type: type,
    key: key,
    ref: ref,
    props: props,

    // react元素的创建者
    _owner: owner,
  };
  return element;
};

/**
 *
 * @param {*} type 元素的类型
 * @param {*} config 配置对象
 * @param {*} children 第一个儿子,如果有多个,依次放在后面
 */
function createElement(type, config, children) {
  let propName;

  // 定义props对象
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let 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;
    // 保留属性,添加到一个新的对象中
    for (propName in config) {
      // 判断 config的属性是不是保留属性,不是保留属性,就添加到对象中
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName) // reserved_props
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // 把children也添加到新的对象中
  // 根据函数的参数个数,判断children是否存在
  const childrenLength = arguments.length - 2;
  // 如果为1,表明有一个children,就是直接赋值给新对象的children属性
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    // 如果有多个儿子,就放到一个数组中,children的value值就是该数组
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      // 去掉前面的两个参数
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 判断该元素有不有type和默认的props(针对类组件)
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  );
}
jsx函数源码

packages/react/src/ReactJSXElement.js

export function jsx(type, config, maybeKey) {   
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  
  // maybeKey 就是处理一种key写法的现象
  // <div {...props} key='hi'></div> 或则 <div key='hi' {...props}></div>
  // props中可能包含key,按照第二种写法,props中的key就会原本节点上的key(同一个对象上)
  // jsxDEV期望的是 <div {...props} key='hi'></div>,确定本节点上的key
  if (maybeKey !== undefined) {
    key = '' + maybeKey;
  }

  if (hasValidKey(config)) {
    key = '' + config.key;
  }

  if (hasValidRef(config)) {
    ref = config.ref;
  }
  
    
  // 过滤掉: 预留的props属性
  for (propName in config) {
    if (
      hasOwnProperty.call(config, propName) &&
      !RESERVED_PROPS.hasOwnProperty(propName)
    ) {
      props[propName] = config[propName];
    }
  }

  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  return ReactElement(
    type,
    key,
    ref,
    undefined,
    undefined,
    ReactCurrentOwner.current,
    props,
  );
}

发现没有,其实两个函数的源码基本上差不多,就是在处理子节点的有点不同。createElement是单独处理的,jsx函数根本就没有处理,直接赋值(因为 children并不预保留属性,所以会直接赋值给props)

总结

jsx被babel解析成虚拟对象

jsx如果包含子节点,子节点也会被解析,依次下去,就会形成一个深层次的对象,这个对象被称为虚拟DOM。所以虚拟DOM就是一个对象。

如果上面写的有误,请指教~~~