blog icon indicating copy to clipboard operation
blog copied to clipboard

React的jsx是如何渲染成dom节点?

Open rudyxu1102 opened this issue 4 years ago • 0 comments

前言

本次阅读的React源码版本是17.0.1,了解一下JSX的相关知识

比较好奇的地方

  • JSX被编译后生成了什么?
  • JSX生成的对象有哪些属性?
  • JSX编译生成的ReactElement对象的作用是什么?

JSX被编译后生成了什么?

使用plugin-transform-react-JSX的babel插件,可以将JSX语法转换成js语法,转换成React.createElement函数去执行。

例如,假设JSX代码如下

<div>hello world</div>

经过babel编译后,会转换成js代码。可以去babel官网直接进行转换。

React.createElement("div", null, "hello world");

JSX生成的对象有哪些属性?

React17对JSX进行优化,不再需要React环境,经过babel编译后会自动引入JSX运行时,即React.createElement方法。 React17将React.createElement单独抽离成一个文件,放在React目录下的JSX-runtime.js文件。

假如JSX代码如下

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

下面是最新JSX被转换编译的结果

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

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

下面我们来看一下React.createElement方法的源码,即jsx-runtime.js中的暴露的JSX方法。

// 即 React.createElement方法
function JSXDEV(type, config, maybeKey, source, self) {
  {
    var propName; // Reserved names are extracted

    var props = {};
    var key = null;
    var ref = null; // Currently, key can be spread in as a prop. This causes a potential
    // issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
    // or <div key="Hi" {...props} /> ). We want to deprecate key spread,
    // but as an intermediary step, we will use JSXDEV for everything except
    // <div {...props} key="Hi" />, because we aren't currently able to tell if
    // key is explicitly declared to be undefined or not.

    if (maybeKey !== undefined) {
      key = '' + maybeKey;
    }

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

    if (hasValidRef(config)) {
      ref = config.ref;
      warnIfStringRefCannotBeAutoConverted(config, self);
    } // Remaining properties are added to a new props object


    for (propName in config) {
      if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    } // 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);
  }
}

可以看到,经过对传入参数的简单修饰,最后会调用ReactElement方法,而ReactElement方法会返回一个对象,即JSX对象。

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    // This tag allows us to uniquely identify this as a React 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
  };

  {
    // 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;
};

JSX对象有以下几个属性:

  • $$typeof。使用Symbol来标记每个React元素,防止XSS攻击。
  • type。组件标签名称
  • key。组件的key属性
  • ref。组件的ref属性
  • props。组件的属性
  • _owner。拥有这个组件的父级fiber节点。

比如

<marquee bgcolor="#ffa7c4">hi</marquee>

转换成如下的JSX对象

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
}

编译生成的ReactElement对象的作用是什么?

JSX语法会被编译成js语法。而JSX语法主要在以下两个地方使用,React.createElement也就是在这时候执行。

  • ReactDom.render里面传入的参数<APP/>
  • 组件的render方法返回的JSX

下面我们来看一下第二种情况,即组件render方法执行之后,生成的ReactElement对象用来做什么?

首次渲染会生成workInProgress fiber树,render方法在生成组件fiber节点的时候调用的,具体是在finishClassComponent方法中执行。

function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
  //...
  var nextChildren;

  if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {
    nextChildren = null;

    {
      stopProfilerTimerIfRunning();
    }
  } else {
    {
      setIsRendering(true);
      // 执行组件的render方法
      nextChildren = instance.render();

      if ( workInProgress.mode & StrictMode) {
        disableLogs();

        try {
          instance.render();
        } finally {
          reenableLogs();
        }
      }

      setIsRendering(false);
    }
  } 


  workInProgress.flags |= PerformedWork;

  if (current !== null && didCaptureError) {
    // If we're recovering from an error, reconcile without reusing any of
    // the existing children. Conceptually, the normal children and the children
    // that are shown on error are two different sets, so we shouldn't reuse
    // normal children even if their identities match.
    forceUnmountCurrentAndReconcile(current, workInProgress, nextChildren, renderLanes);
  } else {
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  } 

  workInProgress.memoizedState = instance.state; // The context might have changed so we need to recalculate it.

  if (hasContext) {
    invalidateContextProvider(workInProgress, Component, true);
  }

  return workInProgress.child;
}

然后执行reconcileChildren方法,最后经过一长串的调用栈,通过调用createFiberFromElement方法,将ReactElement对象转换成组件的fiber节点。

function createFiberFromElement(element, mode, lanes) {
  var owner = null;

  {
    owner = element._owner;
  }

  var type = element.type;
  var key = element.key;
  var pendingProps = element.props;
  var fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes);

  {
    fiber._debugSource = element._source;
    fiber._debugOwner = element._owner;
  }

  return fiber;
}

最后,转换生成的fiber节点会在commit阶段渲染成相应的dom节点。

参考链接

rudyxu1102 avatar Dec 05 '20 07:12 rudyxu1102