React 学习笔记 --- JSX的本质

525 阅读6分钟

一、JSX的转换

<div id="app"></div>
<div id="app1"></div>

<script type="text/babel">
    const msg1 = <h2>Hello React</h2>
    {/*
      上面的是JSX语法 其是React.createElement的语法糖
      JSX 语法是没有办法被浏览器所直接识别的
      其需要使用Babel将JSX转换为React.createElement的形式进行再交由浏览器来进行解析和调用
    */}
    const msg2 = React.createElement('h2', null, 'Hello React')

    ReactDOM.render(msg1, document.getElementById('app'))
    ReactDOM.render(msg2, document.getElementById('app1'))
</script>
/*
	@param1: type: 当前ReactElement的类型
				如果是标签元素,那么就使用字符串表示 “div”
				如果是组件元素,那么就直接使用组件的名称 App
	@param2: config
		所有jsx中的属性都在config中以对象的属性和值的形式存储
	@param3: children
		存放在标签中的内容(子组件),以children数组的方式进行存储;
*/
React.createElement(type, config, children)

使用Babel官网进行转换

转换前 --- JSX

<main>
    <header className="header">我是头部</header>
    <section className="section">
        <div>我是content</div>
        <button>我是按钮</button>
    </section>
    <footer className="footer">我是footer</footer>
</main>

转换后 --- React.createElement

"use strict";

/*#__PURE__*/
React.createElement("main", null, /*#__PURE__*/React.createElement("header", {
  className: "header"
}, "\u6211\u662F\u5934\u90E8"), /*#__PURE__*/React.createElement("section", {
  className: "section"
}, /*#__PURE__*/React.createElement("div", null, "\u6211\u662Fcontent"), /*#__PURE__*/React.createElement("button", null, "\u6211\u662F\u6309\u94AE")), /*#__PURE__*/React.createElement("footer", {
  className: "footer"
}, "\u6211\u662Ffooter"));

从上述的转换可以看出以下几点:

  1. 在使用BabelJSX代码进行转换的时候,其会开启严格模式

  2. 上述的代码最终被转换为了类似于React.createElement(type, config, child1, child2, child3 ....)的形式

    每一个child最后都会被继续遵循React.createElement的形式进行转换

    1. 在每一个React.createElement的代码进行转换的过程中,每一个函数之前都有一个/*__PURE__*/;来表示是一个转换后的组件的开始

但是 Babel转换后的形式是React.createElement(type, config, child1, child2, child3 ....)

但是 ReactcreateElement定义的方式是 React.createElement(type, config, children)

其并没有使用 扩展参数, 那么其是怎么获取到所有的子组件的哪?

其内部其实使用了arguments参数, React中定义和使用的方式如下

createElement中是如何组合children参数

1.1 ReactElement

在我们调用React.createElement方法来创建对象的时候,其会返回一个ReactElement类型的对象

其上存在当前的组件的类型type,属性,子组件等相关信息

所以ReactElement类型的对象也就是我们常说的虚拟DOM

React利用ReactElement对象组成了一个JavaScript的对象树

所有的DOM操作都会在这个vdom树上来进行相应的操作,执行完毕以后在将这棵VDOM树渲染为真实DOM

1.2 React中dom的渲染过程

  1. 使用React.createElement方法来创建ReactElement对象(vdom对象

  2. VDOM 映射为真实DOM

    VDOM只是存在于内存中的一颗DOM树,所以其上的每一个节点,都是和真实DOM一一映射(对应)的

VDOM和真实DOM一一对应

1.3 为什么使用VDOM

在没有VDOM之前

  1. 需要手动操作dom,还要考虑浏览器兼容性等问题,于是出现了Jquery

  2. JQuery等库,简化了dom操作,我们也不需要考虑浏览器兼容性等问题,

  3. 随着前端项目的复杂,dom操作也变得复杂,我们既要考虑操作数据,也要考虑操作dom,于是出现了模板引擎

    但是模板引擎没有解决跟踪状态变化的问题,就是当数据发生变化后,无法获取上一次的状态,只好把界面上的元素删除,然后在重新创建,jquery写的一个列表,当新增删除排序时,添加一个过渡效果,操作时列表会先被删除,然后重建,耗费性能。模板引擎没有解决跟踪状态变化的问题,所以有了基于虚拟domMVVM框架

  4. 基于VDOMmvvm框架帮我们解决可视图和状态的同步问题。也就是当数据发生变化自动更新视图,当视图发生变化,自动更新数据

    ​ 例如在react中,我们只需要使用JSX告诉React,界面需要长成什么样,数据有哪些即可,react会自动帮助我们进行渲染,我们不需要进行任何的DOM操作

    如果数据发生了改变,我们只需要通过setState的方式来告诉react界面需要进行修改,其会自动进行界面的修改,不需要我们进行任何的DOM操作

使用VDOM的原因:

  1. 很难跟踪状态发生的改变:原有的开发模式,我们很难跟踪到状态发生的改变,不方便针对我们应用程序进行调试;
  2. 操作真实DOM性能较低:传统的开发模式会进行频繁的DOM操作,而这一的做法性能非常的低;
    1. document.createElement本身创建出来的就是一个非常复杂对象
    2. 频繁的DOM操作会导致页面需要经常的进行页面的重绘回流操作
    3. VDOM是在内存中进行批量操作的,运行时间和效率比较高
    4. VDOM中的更新使用了diff算法,以最为高效的方式去进行DOM的更新
    5. 虚拟DOM帮助我们从命令式编程转到了声明式编程的模式

举例:

比如我们有一组数组需要渲染:[1, 2, 3, 4,5],界面上显示的格式为

一个简单的列表

现在要修改为 [1, 2, 3, 4, 5, 6, 7, 8, 9,10]

​ 方式一:重新遍历整个数组(不推荐)

​ 方式二:在ul后面追加另外5个li

d0SRmD.png

上面这段代码的性能怎么样呢?非常低效

  • 因为我们通过 document.createElement 创建元素,再通 过 ul.appendChild(li) 渲染到DOM上,进行了多次DOM操作;

  • 对于批量操作的,最好的办法不是一次次修改DOM,而是对批量 的操作进行合并;(比如可以通过DocumentFragment进行合 并);

而我们正式可以通过 Virtual DOM来帮助我们解决上面的问题;

1.4 VDOM帮助我们从命令式编程转到了声明式编程

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

  • 在这个理念中,UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象
  • 我们可以通过ReactDOM.render虚拟DOM 真实DOM同步起来,这个过程中叫做协调(Reconciliation);
  • 这种编程的方式赋予了React声明式的API
    • 你只需要告诉React希望让UI是什么状态;
    • React来确保DOM和这些状态是匹配的; 你不需要直接进行DOM操作,可以从手动更改DOM、属性操作、事件处理中解放出来;

二 、React 阶段案例

d02E1P.gif

需求:

  1. 在界面上以表格的形式渲染一些数据
  2. 在底部显示表格的总价
  3. 点击+ 或者 - 号的时候,可以秀爱购买数量,并实时修改总价
  4. 如果购买数量为0的时候,其就无法在进行减去,-号按钮的交互变得不可变
  5. 点击移除按钮的时候,对应的行会被移除,总价也会发生相应的改变
  6. 如果列表数据全部移除后,在界面中显示 No Data
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    table {
      border: 1px solid #333;
      /*
        这是数组特有的样式,作用是将表格的各边框给合并折叠起来
       */
      border-collapse: collapse;
      width: 80%;
    }

    th {
      background-color: #eee;
    }

    th,
    td {
      border: 1px solid #333;
      text-align: center;
    }

    .count {
      margin: 0 10px;
    }
  </style>
</head>

<body>
  <div id="app"></div>

  <script src="./dist/react.development.js"></script>
  <script src="./dist/react-dom.development.js"></script>
  <script src="./dist/babel.min.js"></script>

  <script type="text/babel">
    class App extends React.Component{
        constructor() {
          super()

          this.state = {
            books: [
              {
                id: 1,
                title: '《算法导论》',
                date: '2006-09',
                price: 85,
                count: 1
              },

              {
                id: 2,
                title: '《UNIX编程艺术》',
                date: '2006-02',
                price: 59,
                count: 1
              },

              {
                id: 3,
                title: '《编程珠玑》',
                date: '2008-10',
                price: 39,
                count: 1
              },

              {
                id: 4,
                title: '《代码大全》',
                date: '2006-03',
                price: 128,
                count: 1
              }
            ]
          }
        }

        // 渲染函数写在render函数的前面

        /*
          有数据的时候,渲染table
         */
        renderTable() {
          const { books } = this.state

          return (
            <div>
              <table>
                <thead>
                  <tr>
                    <th> </th>
                    <th>书籍名称</th>
                    <th>出版日期</th>
                    <th>价格</th>
                    <th>购买数量</th>
                    <th>操作</th>
                  </tr>
                </thead>

                <tbody>
                  {
                    books.map((item, index) => (
                        <tr>
                          <td>{ item.id }</td>
                          <td>{ item.title }</td>
                          <td>{ item.date }</td>
                          <td>{ this.formatPrice(item.price) }</td>
                          <td>
                            <button disabled={item.count <= 0} onClick={ () => this.changeCount(index, -1) }> - </button>
                            <span className="count">{ item.count }</span>
                            {/*   事件中的callback需要使用箭头函数来修正this指向  */}
                            <button onClick={() =>  this.changeCount(index, 1) }> + </button>
                          </td>
                          <td>
                            <button onClick={ () => this.remove(item.id) }>移除</button>
                          </td>
                        </tr>
                      ))
                  }
                </tbody>
              </table>
              <p>TotalPrice: <strong> { this.formatPrice(this.getTotalPrice()) } </strong></p>
            </div>
          )
        }


        render() {
          return (
            <div>
              {/* 条件判断 */}
              { this.state.books.length > 0 ? this.renderTable() : <h4>Not Data</h4> }
            </div>
          )
        }

        // 功能性函数写在render函数后边
        remove(id) {
          // 注意: 在React中有一个最为重要的概念,即不要去直接修改state中的数据
          // 所以在这里没有使用splice对books中的数据进行修改,而是使用filter函数,返回了一个新的数组
          this.setState({
            books: this.state.books.filter(item => item.id != id)
          })
        }

        changeCount(index, v) {
          // 基于State中的数据不可变的特性
          // 在这里浅拷贝了一份新的数组,对新的数组上的操作,就不会直接修改原本state上的数组
          const newBooks = [... this.state.books]
          newBooks[index].count += v

          // 使用新的数组地址覆盖旧的数组地址
          this.setState({
            books: newBooks
          })
        }

        formatPrice(price) {
          // parseFloat 是将可以转换为数字的字符串转换为数字(因为只有数字类型的值才有toFixed方法)
          // 使用 A || 0 的作用是赋予一个默认值
          // 也就是如果price不可以转换为数字(例如 'aaa')那么其parseFloat函数调用后的返回值是NaN
          // 为了避免这种情况的发生,我们需要赋予其一个默认值,即为0
          return '$' + (parseFloat(price) || 0).toFixed(2)
        }

        getTotalPrice() {
          return this.state.books.reduce((preV, item) => preV + item.price * item.count, 0)
        }
      }

      ReactDOM.render(<App />, document.getElementById('app'))
  </script>
</body>

</html>

上一篇 React学习笔记 --- jsx核心语法(下) 下一篇 React学习笔记 --- React脚手架