02认识JSX和虚拟DOM

158 阅读3分钟

本节的重点是 jsx,虚拟DOM以及源码中如何将jsx转为DOM。

  1. 认识jsx
  2. 体验虚拟DOM
  3. 区分新老babel转换jsx的不同之处

jsx 中的三个重点问题(面试):

  • jsx 的本质是什么,它和 js 之间到底是什么关系?
  • 为什么要用 jsx,不用的后果是什么?
  • jsx 背后的功能模块是什么,这个功能模块都做了哪些事情?

jsx 的本质是 JavaScript 的一种语法扩展,它和模版语法很接近,但是它充分具备 JavaScript 的能力。

Facebook 公司给 jsx 的定位是:jsx 是 JavaScript 的“扩展”,而非 JavaScript 的某个版本,这就直接决定了浏览器并不会像天然支持 JavaScript 一样地支持 JSX。

自己编写的jsx代码可以通过babel转为一个JavaScript函数的调用。你可以在- babel进行在线转换代码,查看自己写的jsx转为JavaScript方法调用的结果。

参考下图: 新版转换(React17及以后) image.png

老版转换

image.png

JSX及其本质

React17后不用再主动引入 React 而直接而使用在文件中 jsx 语法。因为新版项目中将 jsx不再转为 React.createElement,但是babel转换后的代码在浏览器中的执行结果是一样的——虚拟DOM对象。

 //在React17以前,babel转换是老的写法
 const babel = require('@babel/core');
 const sourceCode = `
 <h1>
   hello<span style={{ color: 'red' }}>world</span>
 </h1>
 `;
 const result = babel.transform(sourceCode, {
   plugins: [
     ["@babel/plugin-transform-react-jsx", { runtime: 'classic' }]
   ]
 });
 ​
 console.log(result.code);
 ​
 // 打印代码如下
 React.createElement("h1", null, "hello", React.createElement("span", {
   style: {
     color: 'red'
   }
 }, "world"));

React17后的babel编译结果:

 const babel = require('@babel/core');
 const sourceCode = `
 <h1>
   hello<span style={{ color: 'red' }}>world</span>
 </h1>
 `;
 const result = babel.transform(sourceCode, {
   plugins: [
     ["@babel/plugin-transform-react-jsx", { runtime: 'automatic' }]
   ]
 });
 console.log(result.code);
 ​
 // 打印代码如下
 import { jsx } from "react/jsx-runtime";
 jsx("h1", {
   children: ["hello", jsx("span", {
     style: {
       color: 'red'
     },
     children: "world"
   })]
 });
 ​
 // React.createElement=jsx

新的编译模式后,子节点直接以对象props中的children属性值存在,以前的字节点是作为第3个及其往后参数的方式传入createElement,在createElement中在将他们作为props的children属性的属性值。

之前项目的情况:

 import React from 'react';
 ​
 const App = (props) => {
   return <div>hello world!</div>;
 };
 ​
 export default App;
 ​
 // 上面的代码不引入React的话,在之前的项目中无法编译。
 //因为jsx最后被转为React.createElement形式,
 //所以看似没有依赖React,实则是有依赖的。  eslint中提示React引入而未使用的尴尬。

新版的情况:

 const App = (props) => {
   return <div>hello world!</div>;
 };
 ​
 export default App;
 ​
 // 可以直接这么写,编译时会自动引入一个包   import jsx from 'jsx'  最后将jsx转为 jsx()函数调用的方式。

JSX的编译和后续执行:

image.png

image.png

包含jsxDEV函数调用的代码在发送到浏览器后,浏览器会根据源码中定义的该jsxDEV方法的代码逻辑进行执行,最后返回一个虚拟DOM。

 function ReactElement(type, key, ref, props) {
   return {//这就是React元素,也被称为虚拟DOM
     $$typeof: REACT_ELEMENT_TYPE,
     type,//h1 span
     key,//唯一标识
     ref,//后面再讲,是用来获取真实DOM元素
     props//属性 children,style,id
   }
 }

jsxEDV函数在浏览器中调用后生成的结构

 <h1>
   hello<span style={{ color: 'red' }}>world</span>
 </h1>

每个虚拟DOM节点会有一个类型属性——$$typeof

image.png 对于一个虚拟DOM节点的children属性,可能是一个字符串,数字,对象或者数组,其中数组的中的每一项元素可以是前面3中的某一种。

有一点需要注意,上面编写的jsx都是直接使用的原生的标签,如h1,span等等,所以它们生成的函数调用的第一个参数都是字符串的'h1'或者'span'等。

如果你编写的使用一个函数组件,如下代码:


function FunctionComponent(){
  return (<h1>
    hello <span style={{ color: 'red' }}>world!</span>
  </h1>)
}

let element = <FunctionComponent number={1}></FunctionComponent>

// 编译后的结果
function FunctionComponent() {
  return /*#__PURE__*/React.createElement("h1", null, "hello ", /*#__PURE__*/React.createElement("span", {
    style: {
      color: 'red'
    }
  }, "world!"));
}
var element = /*#__PURE__*/React.createElement(FunctionComponent, {
  number: 1
});

读者一定要注意对于函数组件的jsx,生成的函数React.createElement调用中第一个参数就是函数本身,也就是说它是可以直接被执行的。