了解React并解析JSX语法

82 阅读13分钟

React

ReactFacebook来更新和维护,它是大量优秀程序员的思想结晶,React的流行不仅仅局限于普通开发工程师对它的认可

  • React可以说是前端的先驱者,它总是会引领整个前端的潮流

  • 大量流行的其他框架借鉴React的思想,比如Vue Composition API学习了React Hooks的思想

  • 官方对React的解释:用于构建用户界面的 JavaScript

  • React的官网文档:zh-hans.reactjs.org/

特点

  • 声明式编程:它允许我们只需要维护自己的状态,当状态改变时,React可以根据最新的状态去渲染我们的UI界面 image.png
  • 组件化开发:组件化开发页面是前端的流行趋势,会将复杂的界面拆分成一个个小的组件
  • 多平台适配
    • 2013年,React发布之初主要是开发Web页面

    • 2015年,Facebook推出了ReactNative,用于开发移动端跨平台;(虽然目前Flutter非常火爆,但是还是有很多公司在使用 ReactNative

    • 2017年,Facebook推出ReactVR,用于开发虚拟现实Web应用程序;

依赖

开发React必须依赖三个库:

  • react:包含react所必须的核心代码

  • react-domreact渲染在不同平台所需要的核心代码

  • babel:将jsx转换成React代码的工具,

    • 默认情况下开发React其实可以不使用babel

    • 但是前提是我们自己使用 React.createElement 来编写源代码,非常繁琐可读性差

    • 可以直接编写jsx(JavaScript XML)的语法,并且让babel帮助我们转换成React.createElement

这三个库是各司其职的,目的就是让每一个库只单纯做自己的事情; 在React的0.14版本之前是没有react-dom这个概念的,所有功能都包含在react里;

后来为什么要进行拆分呢?原因就是react-native

  • react包中包含了react webreact-native所共同拥有的核心代码

  • react-dom针对webnative所完成的事情不同:

    • webreact-dom会将jsx最终渲染成真实的DOM,显示在浏览器中

    • nativereact-dom会将jsx最终渲染成原生的控件(比如Android``中的ButtoniOS中的UIButton

依赖引入

  • 方式一:直接CDN引入

  • 方式二:下载后,添加本地依赖

  • 方式三:通过npm管理(脚手架使用)

  • 暂时直接通过CDN引入:

    • <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin>

    • <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin>

    • <script src="https://unpkg.com/babel-standalone@6/babel.min.js">

    • 这里有一个 crossorigin的属性,这个属性的目的是为了拿到跨域脚本的错误信息

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>第一个React程序</title>
    </head>
    <body>
      <div id="root"></div>
      <!-- cdn引入 crossorigin解决跨域-->
      <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
      <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
      <!-- babel引入 -->
      <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    
      <script type="text/babel"> //type="text/babel"这时才会babel才会解析
        /* 18之前用render在这里用会报错 ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17.*/
        // ReactDOM.render(<h2>Hello World</h2>,document.querySelector('#root'))
        /* 18之后 createRoot只需要写渲染到哪里就可以 */
        let root = ReactDOM.createRoot(document.querySelector('#root'))
        root.render(<h2>Hello World</h2>)
      </script>
    
    </body>
    </html>
    

组件化

定义

React中,如何封装一个组件呢?这里暂时使用类的方式封装组件:

  • 定义一个类(类名大写,组件的名称是必须大写的,小写会被认为是HTML元素),继承自React.Component

  • 实现当前组件的render函数render当中返回的jsx内容,就是之后React会帮助我们渲染的内容

数据依赖

组件化问题一:数据在哪里定义?

  • 在组件中的数据,可以分成两类:
    • 参与界面更新的数据:当数据变化时,需要更新组件渲染的内容

    • 不参与界面更新的数据:当数据变化时,不需要更新将组件渲染的内容

  • 参与界面更新的数据可以称之为是参与数据流,这个数据是定义在当前对象的state
    • 可以通过在构造函数中 this.state = {定义的数据}

    • 当数据发生变化时,可以调用 this.setState 来更新数据,并且通知React进行update操作

    • 在进行update操作时,就会重新调用render函数,并且使用最新的数据来渲染界面

事件绑定

组件化问题二:事件绑定中的this

  • 在类中直接定义一个函数,并且将这个函数绑定到元素的onClick事件上,当前这个函数的this指向的是谁呢?
    • 默认情况下是undefined,很奇怪居然是undefined

    • 因为在正常的DOM操作中,监听点击函数中的this其实是节点对象(比如说是button对象)

    • 这次因为 React并不是直接渲染成真实的DOM,编写的button只是一个语法糖,它的本质是ReactElement对象

    • 那么在这里发生监听的时候,react在执行函数时并没有绑定this,默认情况下就是一个undefined

  • 在绑定的函数中,可能想要使用当前对象,比如执行 this.setState 函数,就必须拿到当前对象的this
    • 需要在传入函数时,给这个函数直接绑定this

    • 可以在构造函数里提前给点击函数绑定thisthis.changeText = this.changeText.bind(this)

    • 也可以在按钮上绑定this<button onClick={this.changeText.bind(this)}>改变文本</button>

整体代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- cdn引入 crossorigin解决跨域 -->
    <script
      src="https://unpkg.com/react@18/umd/react.development.js"
      crossorigin
    ></script>
    <script
      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
      crossorigin
    ></script>
    <!-- babel引入 -->
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

    <script type="text/babel">
      const root = ReactDOM.createRoot(document.getElementById("root"));
      // let message = 'Hello World'
      // function btnClick() {
      //   message = 'Hello React'
      //   rootRender() // 修改react不会自动重新刷新页面需要手动修改
      // }
      // rootRender() // 函数有空间提升
      // function rootRender() {
      //   root.render(
      //     <div>
      //       <h2>{message}</h2>
      //       <button onClick={btnClick}>修改文本</button>
      //     </div>
      //   )
      // }

      /* 使用类组件方式重构 */
      class App extends React.Component {
        constructor() {
          super();
          this.state = {
            message: "Hello World",
          };
          // 另一种写法提前给方法绑定this
          // this.btnClick = this.btnClick.bind(this)
        }
        btnClick() {
          console.log(this); // undefined 因为babel转换
          // 修改message值,自动执行render函数
          this.setState({ message: "Hello React" });
          // 现在这个this不一定代表这个组件实例
        }
        render() {
          return (
            <div>
              <h2>{this.state.message}</h2>
              <button onClick={this.btnClick.bind(this)}>修改文本</button>
            </div>
          );
          /* bind(this)表示把render的this绑定过this,都是组件实例 错误写法:表示方法的调用,this.btnClick()默认返回undefined,把undefined绑定给onClick肯定报错   <button onClick={this.btnClick()}>修改文本</button> */
        }
      }
      /* const app = new App()
      const foo = app.btnClick
      foo() // 这里this是默认绑定,默认指向window,但严格模式下指向undefined,es6 class类定义的默认是严格模式,babel转换默认undefined
      function fo() {
        console.log(this); // 这里this指向undefined,因为用了babel,关闭babel指向window
      }
      fo() */
      root.render(<App />); // app根组件
    </script>
  </body>
</html>

JSX

  • JSX是一种JavaScript的语法扩展(extension,也在很多地方称之为JavaScript XML,因为看起就是一段XML语法

  • 用于描述我们的UI界面,并且其完全可以和JavaScript融合在一起使用

  • 它不同于Vue中的模块语法,你不需要专门学习模块语法中的一些指令(比如v-for、v-if、v-bind

规范

  • JSX的顶层只能有一个根元素,所以很多时候会在外层包裹一个div元素(或者使用后面学习的Fragment

  • JSX 的外层需要包裹一个() 方便阅读,可以进行换行书写

  • JSX 的标签可以是单标签,也可以是双标签,单标签必须以<xxx/>结尾

  • JSX 中的写注释 {/* JSX注释 */}

  • JSX 嵌入变量作为子元素

    • 情况一:当变量是 Number、String、Array 类型时,可以直接显示

    • 情况二:当变量是 null、undefined、Boolean 类型时,内容为空

      如果希望显示 null、undefined、Boolean 则需要转成字符串,转换的方式有很多,比如 toString 方法、和空字符串拼接,String(变量)等方式

    • 情况三:Object 对象类型不能作为子元素not valid as a React child

  • JSX嵌入表达式:运算表达式、三元运算符、执行一个函数

事件绑定

如果原生DOM原生有一个监听事件,可以如何操作呢?

  • 方式一:获取DOM原生,添加监听事件

  • 方式二:HTML原生中,直接绑定onclick

React中是如何操作呢?我们来实现一下React中的事件监听,这里主要有两点不同

  • React 事件的命名采用小驼峰式camelCase),而不是纯小写

  • 再需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行

  • 在事件执行后,可能需要获取当前类的对象中相关的属性,这个时候需要用到this

  • 解决this的问题三种方案

    • bindbtnClick显式绑定thisthis.changeText = this.changeText.bind(this)

    • 使用 ES6 class fields 语法:<button onClick={this.changeText}>{message}</button> | changeText = () => {}

    • 事件监听时传入箭头函数(推荐)<button onClick={() => this.changeIsHave()}>切换</button>

参数传递

在执行事件函数时,有可能我们需要获取一些参数信息:比如event对象、其他参数

  • 情况一:获取event对象
    • 很多时候需要拿到event对象来做一些事情(比如阻止默认行为)

    • 那么默认情况下,event对象有被直接传入,函数就可以获取到event对象

  • 情况二:获取更多参数
    • 有更多参数时,最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数

条件渲染

某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容,在vue中会通过指令来控制比如v-if、v-showReact中,所有的条件判断都和普通的JavaScript代码一致,常见的条件渲染的方式有哪些呢?

  • 方式一:条件判断语句:适合逻辑较多的情况

  • 方式二:三元运算符:适合逻辑比较简单

  • 方式三:与运算符&&:适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染

  • v-show的效果:主要是控制display属性是否为none

列表渲染

React中并没有像Vue模块语法中的v-for指令,而且需要通过JavaScript代码的方式组织数据转成JSXReact中的JSX正是因为和JavaScript无缝的衔接,让它可以更加的灵活

  • React中,展示列表最多的方式就是使用数组的map高阶函数

  • 很多时候在展示一个数组中的数据之前,需要先对它进行一些处理

    • 比如过滤掉一些内容:filter函数

    • 比如截取数组中的一部分内容:slice函数

  • vue中使用v-for进行列表渲染时需要绑定key值,react中也需要添加key,否则会报错,key主要的作用是为了提高diff算法时的效率 image.png

JSX知识代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JSX语法相关</title>
  </head>
  <body>
    <div id="root"></div>

    <style>
      .active {
        border: 1px red solid;
      }
    </style>

    <script src="./lib/babel.js"></script>
    <script src="./lib/react.js"></script>
    <script src="./lib/react-dom.js"></script>

    <script type="text/babel">
      // 1.定义App根组件
      class App extends React.Component {
        constructor() {
          super();
          this.state = {
            message: "Hello React",

            // JSX中String/Number/Array可以直接展示
            a: "11",
            b: 222,
            c: ["aa", "bb"],

            // JSX中null/undefined/Boolean内容为空,若展示需要转化为字符串
            aa: null,
            bb: undefined,
            cc: true,

            // JSX中对象不能作为子元素
            obj: { name: "哈哈哈" },

            currentIndex: 0,

            isHave: true,
            baidu: "https://www.baidu.com",
            style1: { color: "red" },

            list: [
              { id: 11, name: "哈哈", score: 90 },
              { id: 12, name: "啦啦", score: 78 },
              { id: 13, name: "哒哒", score: 65 },
              { id: 14, name: "你你", score: 59 },
              { id: 15, name: "塔塔", score: 73 },
            ],
          };
        }
        render() {
          const { message, list } = this.state;
          const { a, b, c } = this.state;
          const { aa, bb, cc } = this.state;
          const { obj } = this.state;
          const { currentIndex } = this.state;
          const { isHave, baidu, style1 } = this.state;
          const className1 = `aa bb ${isHave && a}`;
          const className2 = ["aa", "bb"];
          if (isHave) className2.push(a);
          // 基本使用
          // return (
          //   // JSX只有一个根元素
          //   <div>
          //     {/* JSX注释:渲染 */}

          //     <h2>{message}</h2>
          //     <h2>{a}</h2>
          //     <h2>{b}</h2>
          //     <h2>{c}</h2>
          //     <h2>{aa}</h2>
          //     <h2>{bb}</h2>
          //     <h2>{cc}</h2>
          //     {/* null和undefined没有toString方法,可以使用String(aa)和bb+'' */}
          //     <h2>{String(aa)}</h2>
          //     <h2>{bb + ""}</h2>
          //     <h2>{cc.toString()}</h2>
          //     {/* 对象会报错:Objects are not valid as a React child (found: object with keys {name}). If you meant to render a collection of children, use an array instead. */}
          //     {/* <h2>{obj}</h2> */}
          //     {/* JSX可以嵌入表达式比如电影列表例子/三元表达式/执行函数 */}
          //     <h2>{isHave ? "有好事" : "没好事"}</h2>
          //     <h2>{this.getData()}</h2>
          //     <a href={baidu}>{this.getData()}</a>

          //     {/* 绑定class */}
          //     {/* <h2 class={`aa bb ${a}`}>{this.getData()}</h2> 报错react-dom.js:73 Warning: Invalid DOM property `class`. Did you mean `className`?*/}
          //     <h2 className={`aa bb ${isHave && a}`}>{this.getData()}</h2>
          //     <h2 className={className1}>{this.getData()}</h2>
          //     <h2 className={className2.join(" ")}>{this.getData()}</h2>
          //     {/* 还有一种就是使用第三方库classnames -> npm install classnames */}

          //     {/* 绑定style */}
          //     <h2 style={{ color: "red" }}>{style1}</h2>
          //     <h2 style={style1}>style </h2>

          //   </div>
          // );

          // this绑定 第一种参考计数器
          // return (
          //   <div>
          //     {/* 第二种绑定箭头函数 */}
          //     <button onClick={this.click1}>{message}</button>
          //     {/* 第三种绑定(推荐掌握) */}
          //     <button onClick={(e) => this.click2(e, 11)}>{message}</button>
          //   </div>
          // );

          // 条件渲染两种实现
          let showEle = null;
          if (isHave) {
            showEle = <h1>条件渲染1</h1>;
          } else {
            showEle = <h2>条件渲染2</h2>;
          }
          // return <div>{showEle}</div>;
          // return <div>{isHave ? <h1>条件渲染1</h1> : <h2>条件渲染2</h2>}</div>;
          // return <div>{isHave && <h1>条件渲染1</h1>}</div>;
          // return (
          //   <div>
          //     {isHave && <h1>条件渲染1</h1>}
          //     <button onClick={() => this.changeIsHave()}>切换</button>
          //   </div>
          // );

          // v-show效果
          // return (
          //   <div>
          //     <h1 style={{ display: isHave ? "block" : "none" }}>
          //       条件渲染v-show
          //     </h1>
          //     <button onClick={() => this.changeIsHave()}>切换</button>
          //   </div>
          // );

          // 列表渲染
          // const ele = list
          //   .filter((f) => f.score > 70)
          //   .slice(0, 2)
          //   .map((m) => {
          //     return (
          //       <div key={m.score}>
          //         <h1>学号:{m.id}</h1>
          //         <h2>分数:{m.score}</h2>
          //         <h3>名字:{m.name}</h3>
          //       </div>
          //     );
          //   });
          return (
            <div>
              {/* ele */}
              {list
                .filter((f) => f.score > 70)
                .slice(0, 2)
                .map((m, index) => {
                  return (
                    <div
                      key={m.id}
                      className={currentIndex == index && "active"}
                      onClick={(e) => this.changeIndex(index)}
                    >
                      {/* 必须有一个包裹 */}
                      <h1>学号:{m.id}</h1>
                      <h2>名字:{m.name}</h2>
                      <h3>分数:{m.score}</h3>
                    </div>
                  );
                })}
            </div>
          );
        }
        click1 = () => {
          console.log("click1", this);
          this.setState({ message: "hello1" });
        };
        click2 = (e, num) => {
          console.log("click1", this, e, num);
          this.setState({ message: "hello2" });
        };
        changeIsHave() {
          this.setState({ isHave: !this.state.isHave });
        }
        changeIndex(currentIndex) {
          this.setState({
            currentIndex,
          });
        }
        getData() {
          return !this.state.isHave ? "有好事" : "没好事";
        }
      }
      // 创建root并渲染App组件
      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(<App />);
    </script>
  </body>
</html>

本质

实际上jsx 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖,所有的jsx最终都会被转换成React.createElement的函数调用,createElement需要传递三个参数:

  • 参数一:type
    • 当前ReactElement的类型

    • 如果是标签元素,那么就使用字符串表示 “div”

    • 如果是组件元素,那么就直接使用组件的名称

  • 参数二:config
    • 所有jsx中的属性都在config中以对象的属性和值的形式存储

    • 比如传入className作为元素的class

  • 参数三:children
    • 存放在标签中的内容,以children数组的方式进行存储

    • 当然如果是多个元素呢?React内部有对它们进行处理,处理的源码在下方

默认jsx是通过babel进行语法转换的,所以之前写的jsx代码都需要依赖babel,可以在babel的官网中快速查看转换的过程:babeljs.io/repl/#?pres…

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JSX语法相关</title>
  </head>
  <body>
    <div id="root"></div>

    <script src="./lib/babel.js"></script>
    <script src="./lib/react.js"></script>
    <script src="./lib/react-dom.js"></script>

    <script type="text/babel">
      // 1.定义App根组件
      class App extends React.Component {
        constructor() {
          super();
          this.state = {
            message: "Hello React",
            count: 100,
          };
        }
        render() {
          const { message } = this.state;
          /*
            JSX的本质仅仅是React.creatrElement(type, config, children)的语法糖
            type:是类型,比如div
            config:是对象比如:{title: 'Header', class: 'Header', src: ''},
            children:是子元素
            2023.07.31使用bebal转换如下:
                    import { jsx as _jsx } from "react/jsx-runtime";
                    import { jsxs as _jsxs } from "react/jsx-runtime";
                    _jsxs("div", {
                      className: "app",
                      children: [_jsx("div", {
                        title: "Header",
                        children: "Header"
                      }), _jsxs("div", {
                        children: [_jsx("button", {
                          onClick: e => this.changeCount(1),
                          children: "+1"
                        }), _jsxs("h2", {
                          children: ["\u5566\u5566\u5566\u5566", this.state.count]
                        }), _jsx("button", {
                          onClick: e => this.changeCount(-1),
                          children: "-1"
                        })]
                      }), _jsx("div", {
                        children: "Footer"
                      })]
                    });
          */
          const ele = React.createElement(
            "div",
            null,
            /*#__PURE__*/ React.createElement(
              "div",
              {
                className: "header",
              },
              "Header"
            ),
            /*#__PURE__*/ React.createElement(
              "div",
              {
                className: "Content",
              },
              /*#__PURE__*/ React.createElement("div", null, "Banner"),
              /*#__PURE__*/ React.createElement(
                "ul",
                null,
                /*#__PURE__*/ React.createElement(
                  "li",
                  null,
                  "\u5217\u8868\u6570\u636E1"
                ),
                /*#__PURE__*/ React.createElement(
                  "li",
                  null,
                  "\u5217\u8868\u6570\u636E2"
                ),
                /*#__PURE__*/ React.createElement(
                  "li",
                  null,
                  "\u5217\u8868\u6570\u636E3"
                ),
                /*#__PURE__*/ React.createElement(
                  "li",
                  null,
                  "\u5217\u8868\u6570\u636E4"
                ),
                /*#__PURE__*/ React.createElement(
                  "li",
                  null,
                  "\u5217\u8868\u6570\u636E5"
                )
              )
            ),
            /*#__PURE__*/ React.createElement(
              "div",
              {
                className: "footer",
              },
              "Footer"
            )
          );
          console.log(ele);
          // return (
          //   <div className="app">
          //     {/* JSX注释:渲染 */}
          //     <div title="Header">Header</div>
          //     <div>
          //       <button onClick={(e) => this.changeCount(1)}>+1</button>
          //       <h2>啦啦啦啦{this.state.count}</h2>
          //       <button onClick={(e) => this.changeCount(-1)}>-1</button>
          //     </div>
          //     <div>Footer</div>
          //   </div>
          // );
          return ele;
        }
        changeCount(num) {
          this.setState({
            count: this.state.count + num,
          });
        }
      }
      // 创建root并渲染App组件
      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(<App />);
    </script>
  </body>
</html>

虚拟DOM

通过 React.createElement 最终创建出来一个 ReactElement 对象,这个ReactElement对象是什么作用呢?React为什么要创建它呢?

  • 原因是React利用ReactElement对象组成了一个JavaScript的对象树

  • JavaScript的对象树就是虚拟DOM(Virtual DOM),上面代码打印的ele就可以看到结构

  • ReactElement最终形成的树结构就是Virtual DOM image.png

为什么使用虚拟DOM,⽽不是直接修改真实的DOM呢?

  • React官方的说法:Virtual DOM 是一种编程理念

  • 在这个理念中,UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象

  • 可以通过ReactDOM.render让虚拟DOM 和真实DOM同步起来,这个过程中叫做协调(Reconciliation

  • 这种编程的方式赋予了React声明式的API,只需要告诉React希望让UI是什么状态,React来确保DOM和这些状态是匹配的

  • 不需要直接进行DOM操作,就可以从手动更改DOM、属性操作、事件处理中解放出来

  • 原有的开发模式,很难跟踪到状态发⽣的改变,不⽅便针对应⽤程序进⾏调试

  • 操作真实DOM性能较低:传统的开发模式会进⾏频繁的DOM操作,⽽这⼀的做法性能⾮常的低

  • 虚拟DOM帮助我们从命令式编程转到了声明式编程的模式

  • 虚拟DOM有利于实现跨平台的能⼒,即⼀套代码可以打包出各个平台的应⽤

  • 关于虚拟DOM的一些其他内容,在后续的学习中还会再次讲到