美文网首页
理解JSX 和虚拟 DOM

理解JSX 和虚拟 DOM

作者: _stan | 来源:发表于2021-06-10 13:02 被阅读0次

    前言

    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中:

    • 组件在更新中的优先级
    • 组件的state
    • 组件被打上的用于Renderer的标记
    • 这些内容都包含在Fiber节点中。

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

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

    相关文章

      网友评论

          本文标题:理解JSX 和虚拟 DOM

          本文链接:https://www.haomeiwen.com/subject/koleeltx.html