Mini React 之JXS介绍及实现

282 阅读1分钟

1. 什么是JSX?

const element = <h1>Hello, world!</h1>;

JSX以类似模板语法的方式描述UI的一种语法。它是JS扩展语法,具有 JavaScript 的全部功能,可以在其中使用变量或表达式、函数等。它不能直接在浏览器运行,需要经过编译成标准的js语法才行。

2. 使用JSX的好处

React的重要特性之一就是声明式渲染,JSX是实现声明渲染途径,将DOM结构和逻辑 共同存放在称之为“组件”的松散耦合单元中,在视觉上有辅助作用

3. JSX的本质

  1. JSX其实只是一种语法糖,最终会通过Babel转译成React.createElement(type, props, children)语法
  2. React.createElement会返回一个React元素(虚拟dom)
  3. React元素事实上是普通的JS对象,用来描述你在屏幕上看到的内容
  4. ReactDOM来确保浏览器中的真实DOM数据和React元素保持一致

JSX

<h1 className="title" style={{color:'red'}}>hello</h1>

转译后的代码:

React.createElement("h1", {
  className: "title",
  style: {
    color: 'red'
  }
}, "hello");

执行后返回的结果:

{
  type:'h1',
  props:{
    className: "title",
    style: {
      color: 'red'
    },
    children:"hello"
  }
}

4. 实现JSX

4.1 项目编译配置

配置打包编译和开发服务器

webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const resolve = (dir) => path.resolve(__dirname, dir);

module.exports = {
  mode: "production",
  entry: resolve("main.js"),
  devtool: false,
  output: {
    path: resolve("dist"),
    filename: "index.js",
  },
  devServer: {
    static: resolve("public"),
    port: 3000,
    open: true,
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: resolve("public/index.html"),
    }),
  ],
};

4.2 JSX实现过程

main.js里编写如下代码:

main.js

import React from './src/index'

const jsxELe = <h1 className="some-name">hello world!</h1>;

console.log("jsxELe=>", jsxELe);

npx webpack serve启动开发服务器

发现webpack编译报错了:

jsxerror

原来是在没有装jsx插件的情况下,不能识别jsx语法,需要安装@babel/plugin-transform-react-jsx这个Babel插件对JSX语法糖进行转换。

pnpm install @babel/plugin-transform-react-jsx

安装好后配置插件:

.babelrc

{
  "plugins": ["@babel/plugin-transform-react-jsx"]
}

然后重启服务,可以看到编译没报错,能识别jsx语法了,但是浏览器运行报错了:

creatElementErr

原因是JSX最终会编译成React.createElement()方法,我们还没有实现这个方法。我们在src/下面新建一个jsx.js文件,然后实现createElement()方法,最后src/index.js里面导入scr/jsxcreateElement()方法并export出去:

src/jsx.js

export function createElement(type, props, children) {
  return {type, props, children}
}

终于没有报错了!这时我们可以清晰的看到,我们写的jsx,最终经过编译,执行之后,得到的是一个描述真实dom的js对象,也就是我们常说的虚拟DOM。

createElement

4.3 增加虚拟DOM必要属性和处理子元素

当我们把main.js里的jsx改成了多层嵌套的时候,我们发现得到的虚拟dom对象都是在一个平面上:

const jsxELe = (
  <h1 className="title" style={{ color: "red" }}>
    h1:文本内容
    <span>span:文本内容</span>
    <div >
      <p>p:文本内容</p>
    </div>
  </h1>
);

得到的虚拟dom对象

createElement

所以还要对后面的子节点进行children处理,此外为了区分一个object是普通对象还是jsx虚拟DOM,我们还得在这个虚拟DOM上加$$typeof属性。把stringnumber基本类型包装成虚拟dom,然后还要加上key属性来标识每个虚拟dom对象,便于后面做diff处理。还有要加上ref属性,方便后面实现ref相关功能。代码如下:

src下创建element.js,用于存放各种标识及常量

src/element.js:

export const REACT_ELEMENT = Symbol('react.element');
export const REACT_ELEMENT = Symbol('REACT_TEXT');

src下创建utils.js,用于存放工具函数

src/utils.js:

import { REACT_TEXT } from "./element";
// 将基本类型转成对象类型,为了方便后续的diff
export function wrapToVdom(element) {
  return typeof element === "string" || typeof element === "number"
    ? { type: REACT_TEXT, props: element }
    : element;
}

然后把 src/jsx.js 里面createElement()返回的虚拟dom对象做一些调整:

src/jsx.js

import { REACT_ELEMENT } from "./element";
import { wrapToVdom } from "./utils";

export function createElement(type, props, ...kids) {
  props = props || {}; // props可能为null
  props.children = kids.length === 1 ? wrapToVdom(kids[0]) : kids.map(wrapToVdom); // 把kids的基本元素包装成Vdom
  return {
    $$typeof: REACT_ELEMENT,
    type,
    ref: props.ref,
    key: props.key,
    props,
  };
}

测试一下,有这么一段jsx:

import React from "./src/index";

const jsxELe = (
  <h1 className="title" style={{ color: "red" }}>
    h1的文本内容
    <div onClick={() => alert("hi~")}>
      <p>p的文本内容</p>
    </div>
  </h1>
);

console.log("jsxELe=>", jsxELe);

我们来看一下它的输出结果:

jsx-reslut

完美~,jsx已经基本实现了。下一节我们将学习如何把虚拟DOM渲染到页面上。

代码地址: github.com/sunnyxujian…