理解JSX 和虚拟 DOM

2021-06-10  本文已影响0人  _stan

前言

jsx和虚拟dom一直都是react面试中老生常谈的问题,但面试题背归背,只有把问题弄懂了才能转换成自己的真正的实力。

什么是JSX?

JSX 是一个 JavaScript 的语法扩展,在react项目中可以像这样声明一个变量。

const element = <div class='a'>hello world!</div>

// 在babel中会被编译为
const element = /*#__PURE__*/React.createElement("div", {
  class: "a"
}, "hello world!");

所以JSX其实是React.createElement()的语法糖,JSX在编译时会被Babel编译为React.createElement方法。

这也是为什么在每个使用JSX的JS文件中,你必须显式的声明

import React from 'react'

不过,React 17 在 React 的 package 中引入了两个新入口,这些入口只会被 Babel 和 TypeScript 等编译器使用。新的 JSX 转换不会将 JSX 转换为React.createElement,而是自动从 React 的 package 中引入新的入口函数并调用。

// 假设你的源代码如下
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

React.createElement内部会调用ReactElement函数最后返回一个对象。

export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    // 将 config 处理后赋值给 props
    // ...省略
  }

  const childrenLength = arguments.length - 2;
  // 处理 children,会被赋值给props.children
  // ...省略

  // 处理 defaultProps
  // ...省略

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

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 标记这是个 React Element
    $$typeof: REACT_ELEMENT_TYPE,

    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  return element;
};

ReactElement最终会返回一个包含组件数据的js对象,这就是经常说的虚拟dom对象,也是一个react元素,其中$$typeof属性被赋值为REACT_ELEMENT_TYPE的常量标记这个对象是一个合法的react元素。

并且react提供了全局API用于校验对象是否为合法的react元素

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

$$typeof

$$typeof 属性也起到了防止 XSS 攻击的作用。
如果服务器允许用户储存任意的 JSON 数据的情况下。那么就可以手动构建 React Element 对象传入到元素中。例如:

// Server could have a hole that lets user store JSON
let expectedTextButGotJSON = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* put your exploit here */'
    },
  },
  // ...
};
let message = { text: expectedTextButGotJSON };

// Dangerous in React 0.13
<p>
  {message.text}
</p>

REACT_ELEMENT_TYPE值的定义

if (typeof Symbol === 'function' && Symbol.for) {
  const symbolFor = Symbol.for;
  REACT_ELEMENT_TYPE = symbolFor('react.element');
  REACT_PORTAL_TYPE = symbolFor('react.portal');
  REACT_FRAGMENT_TYPE = symbolFor('react.fragment');
  ...
}

使用 Symbol 类型是因为 JSON 中无法传递 Symbol。React 会检查 element.$$typeof 然后拒绝处理非法的元素,这样就可以规避这个问题。

React Component和 React Element

使用class component或者function component的时候component都会被当作React.createElement函数中的第一个参数type传入

class ClassComp {
  render() {
    return <div>ClassComp</div>
  }
}

const FuncComp = () => {
  return <div>FuncComp</div>
}

const element1 = <ClassComp />
const element2 = <FuncComp />

// 在babel中会被编译为
class ClassComp {
  render() {
    return /*#__PURE__*/React.createElement("div", null, "ClassComp");
  }
}

const FuncComp = () => {
  return /*#__PURE__*/React.createElement("div", null, "FuncComp");
};

const element1 = /*#__PURE__*/React.createElement(ClassComp, null);
const element2 = /*#__PURE__*/React.createElement(FuncComp, null);

注意点
如果自定义组件的命名不是大写开头的话,babel只会转换成普通的标签,也称为HostComponent.

const funcComp = () => {
  return <div>FuncComp</div>
}

const element1 = <funcComp />

// 在babel中会被编译为
const funcComp = () => {
  return /*#__PURE__*/React.createElement("div", null, "FuncComp");
};

const element1 = /*#__PURE__*/React.createElement("funcComp", null);

结果type会变成"funcComp"而不是funcComp组件,这也是react自定义组件为什么要大写开头的原因。

JSX与Fiber节点

从上面的内容我们可以发现,JSX是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。

比如如下信息就不包括在JSX中:

所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。

在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。

上一篇下一篇

猜你喜欢

热点阅读