React18 - JSX

352 阅读9分钟

React认为渲染逻辑本质上与其他UI逻辑存在内在耦合

他们之间是密不可分,所以React没有将界面和逻辑代码分离开来,而是通过组件(Component)将他们组合在了一起

所以为了方便在JS中编写类似于html的语法,也就是html in js, react提供了JSX语法

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

JSX本质就是嵌入到JavaScript中的一种结构语法

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

JSX不同于Vue中的模块语法,所以不需要专门学习模块语法中的一些指令

const root = ReactDOM.createRoot(document.querySelector('#root'))
// babel会将JSX转换为React.createElement方式的代码
// 所以JSX代码最终会被编译为JS对象,也就是虚拟DOM
// 所以JSX和字面量一样赋值给某个常量或变量
const message = <h2>Hello World</h2>
root.render(message)

书写规范

  1. JSX的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div元素 或 Fragment组件

  2. 为了方便阅读,我们通常在jsx的外层包裹一个小括号()

    • 这样可以方便阅读,并且让JSX可以进行换行书写
    • 如果不加小括号,后续的第一个元素的开始标签必须紧跟在return后边
    return (
    	<div>
      	Hello World
      </div>
    )
    
    // 如果不加小括号,那么后边第一个元素的开始标签必须紧跟在return关键字后边
    // 否则会被解析为 return undefined
    // 所以推荐return 后边返回的JSX元素使用小括号进行包裹,以提升代码的可读性
    return <div>
      Hello World
    <div>
    
  3. JSX中的标签可以是单标签,也可以是双标签, 但是如果使用单标签,必须严格闭合标签

插入内容

  1. 当变量是Number、String、Array类型时,可以直接显示

    • 当变量是Array类型的时候,会调用Array.prototype.join(‘’)后,再将结果输出
  2. 当变量是null、undefined、Boolean类型时,内容为空

    • 如果希望可以显示null、undefined、Boolean,那么需要转成字符串
    • 转换的方式有很多,比如toString方法、和空字符串拼接,String(变量)等方式
    • 对于Boolean类型,无论结果为true还是false,其最终内容都不会在界面上进行渲染
  3. Object对象类型不能作为子元素(not valid as a React child)

JSX中不仅仅可以插入变量,也可以插入任何合法的JS表达式

class App extends React.Component {
  constructor() {
    super()

    this.state = {
      firstname: 'Klaus',
      lastname: 'Wang',
      age: 21,
      users: ['Klaus', 'Alex', 'Steven']
    }
  }

  render() {
    this.fullName = this.state.firstname + ' ' + this.state.lastname

    return (
      <div>
        <div>{ this.fullName }</div>
        {/* 合法的JS表达式 */}
        <div>{ this.state.firstname + ' ' + this.state.lastname }</div>
        {/* 三目运算符 */}
        <div>{ this.state.age > 18 ? '成年' : '未成年' }</div>
        {/* 函数调用 */}
        <div>{ this.getUserList() }</div>
      </div>
    )
  }

  getUserList() {
    return this.state.users.map(user =>  <li key={user}>{ user }</li>)
  }
}

属性绑定

class App extends React.Component {
  constructor() {
    super()

    this.state = {
      title: 'title info',
      isActive: true
    }
  }

  render() {
    return (
      <div>
        <div title={ this.state.title }>普通属性绑定</div>

        {/*
          class是js中的关键字, 所以在JSX中使用className来定义对应的样式
          同样的labe元素中的for属性,需要写成htmlFor
        */}
        <div className={`foo baz ${this.state.isActive ? 'active' : ''}`}>className</div>
        <div className={['foo', 'baz', this.state.isActive ? 'active' : ''].join(' ')}>className</div>

        {/* 下面这种style的编写方式在JSX中是不支持的  */}
        {/* <div style="color: red;">style</div> */}

        {/* 如果我们希望在JSX中编写对应的样式,需要以对象的形式进行编写 */}
        {/* tips: 如果css属性名由多个单词组成的时候,必须使用小驼峰,如果使用中划线写法,必须使用引号进行包裹 */}
        {/* font-size -- error  fontSize -- success  'font-size' -- success */}
        <div style={{ color: 'red', fontSize: '20px', backgroundColor: 'skyblue' }}>style</div>
      </div>
    )
  }
}

事件绑定

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

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

修正this指向

class App extends React.Component {
  constructor(props) {
    super(props)

    // 1. 在构造函数中,修正this指向
    this.handleBtn1Click = this.handleBtn1Click.bind(this)
  }

  handleBtn1Click() {
    console.log(this)
  }

  // 2. 使用class field 修正this
  handleBtn2Click = () => {
    console.log(this)
  }

  handleBtn3Click() {
    console.log(this)
  }

  render() {
    return (
      <div>
        <button onClick={this.handleBtn1Click}>btn1</button>
        <button onClick={this.handleBtn2Click}>btn1</button>
        {/* 3. 使用箭头函数修正this */}
        <button onClick={() => { this.handleBtn3Click() }}>btn1</button>
      </div>
    )
  }
}

参数传递

class App extends React.Component {
  constructor(props) {
    super(props)

    this.handleBtn1Click = this.handleBtn1Click.bind(this)
  }

  // 事件在被调用的时候,默认会传入一个事件处理对象 event
  // react的事件处理函数 是一个在原生事件对象上进行了二次封装的对象
  // 如果需要获取原始对象 e.nativeEvent
  handleBtn1Click(name, age, e) {
    console.log(name, age, e)
  }

  // 这里之所以即可以接收到自己传入的参数,也可以获取到react传入的事件对象
  // 是因为bind方法在绑定this的时候可以传入对应的参数,在函数调用的时候,也可以传入对应的参数, 最终 这两部分参数会被合并
  handleBtn2Click = (name, age, e) => {
    console.log(name, age, e)
  }

  handleBtn3Click(name, age, e) {
    console.log(name, age, e)
  }

  render() {
    return (
      <div>
        <button onClick={this.handleBtn1Click.bind(this, 'Klaus', 'age')}>btn1</button>
        <button onClick={this.handleBtn2Click.bind(this, 'Klaus', 'age')}>btn1</button>
        <button onClick={e => { this.handleBtn3Click('Klaus', 'age', e) }}>btn1</button>
      </div>
    )
  }
}

条件渲染

某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容

  • 在Vue中,我们会通过指令来控制:比如v-if、v-show
  • 在React中,所有的条件判断都和普通的JavaScript代码一致
class App extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      isLogin: true,
      isLoading: true,
      user: {
        name: 'Klaus',
        age: 23
      }
    }
  }

  // 普通条件判断语句
  isLogin() {
    if (this.state.isLogin) {
      return <h3>欢迎回来</h3>
    } else {
      return <h3>请先登录</h3>
    }
  }

  render() {
    const { isLoading, user } = this.state

    return (
      <div>
        <div>{ this.isLogin() }</div>

        {/* 三元运算符 */}
        <div>{ isLoading ? <h2>加载中。。。</h2> : <h2>加载完成</h2> }</div>

        {/* &&逻辑与 */}
        <div>{ user && user.name }</div>
      </div>
    )
  }
}

列表渲染

真实开发中我们会从服务器请求到大量的数据,数据会以列表的形式存储

在React中并没有像Vue模块语法中的v-for指令,而且需要我们通过JavaScript代码的方式组织数据,转成JSX

class App extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      users: [
        {
          name: 'Klaus',
          age: 12
        },
        {
          name: 'Steven',
          age: 23
        },
        {
          name: 'Alex',
          age: 22
        }
      ]
    }
  }

  render() {
    return (
      <ul>
        {/*
              在JSX循环中,每一个循环项也是需要添加上对应的key属性
              目的也是元素发生更新的时候,可以通过key属性来提升diff算法的性能
            */}
        {
          this.state.users.filter(user => user.age >= 18).map(user => <li key={user}>{user}</li>)
        }
      </ul>
    )
  }
}

JSX本质

实际上,jsx 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖

所有的jsx最终都会被转换成React.createElement的函数调用

也就是说当我们使用React.createElement来编写react代码的前提条件下,我们可以不用引入babel再来进行转义

但是React.createElement编写起来过于繁琐,且可读性和可维护性都相对较差,所以在实际开发中,我们使用JSX来简化我们的编码方式

JSX本质上就是React.createElement的语法糖写法

参数说明
type当前ReactElement的类型
1. 如果是标签元素,那么就使用字符串表示 “div”
2. 如果是组件元素,那么就直接使用组件的类 如 App
config所有jsx中的属性都在config中以对象的属性和值的形式存储
比如传入className作为元素的class
如果没有可以传入null进行占位
children存放在标签中的内容,以children数组的方式进行存储
实际传入的时候可以以剩余参数的形式进行传入,react内部会自动通过arguments将第三个参数开始后续所有参数给整合成一个数组,然后作为props.children属性的值存储起来

有以下JSX代码

<div>
  <header>header</header>
  <Cpn className="content">content</Cpn>
  <footer>footer</footer>
</div>

经过babel编译后会形成

// 经过babel转换的代码会自动开启严格模式
"use strict";

// /*#__PURE__*/这是魔法注释,用于告诉打包工具
// 该函数是纯函数,如果该函数没有被使用,可以直接被移除,并不会影响代码的正常执行
// 也就是说该注释的主要目的是为了方便进行tree shaking操作
/*#__PURE__*/
React.createElement(
  "div", // 普通html元素,传入html标签字符串
  null, // 如果没有属性传入null
  // 子元素 以剩余参数的形式进行传入
  /*#__PURE__*/ React.createElement("header", null, "header"),
  /*#__PURE__*/ React.createElement(
    Cpn, // 如果是组件,传入组件对应类
    {
      // 属性以key-value形式机械能传入
      className: "content"
    },
    "content"
  ),
  /*#__PURE__*/ React.createElement("footer", null, "footer")
);

我们通过 React.createElement 最终创建出来一个 ReactElement对象

而ReactElement对象通过children属性包含了其对应的子元素

最终ReactElement对象就形成了一颗轻量级的对象树

其对应的每一个节点和BOM中的每一个节点都是一一对应的

image.png

虚拟节点的作用

  1. 虚拟DOM帮助我们从命令式编程转到了声明式编程的模式
    • UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象
    • 可以通过ReactDOM.render让 虚拟DOM 和 真实DOM同步起来,这个过程中叫做协调(Reconciliation)
    • 这种编程的方式赋予了React声明式的API:
      • 你只需要告诉React希望让UI是什么状态
      • React来确保DOM和这些状态是匹配的
      • 你不需要直接进行DOM操作,就可以从手动更改DOM、属性操作、事件处理中解放出来
  2. 虚拟DOM是在内存中存在的,可以根据不同平台将虚拟DOM渲染成不同的内容
    • web端 将虚拟DOM渲染成真实DOM
    • APP端 将虚拟DOM渲染为原生控件
  3. 因为虚拟DOM是在内存中存在的,所以我们可以保存旧的数据状态,和最新的数据状态进行比较 (也就是常说的DIFF算法)
    • 从而找到修改DOM的最优方案并以最⼩的代价对DOM进行批量更新,从而最大程度的避免页面的回流和重绘,提升渲染性能
    • 因为有新旧虚拟DOM树的存在,所以可以借助一些DevTools工具来追踪对应的数据流的改变,方便我们进行调试和维护

阶段案例

  1. 在界面上以表格的形式,显示一些书籍的数据
  2. 在底部显示书籍的总价格
  3. 点击+或者-可以增加或减少书籍数量(如果为1,那么不能继续-)
  4. 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~)

image.png

class App extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      books
    }
  }

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

          <tbody>
            {
              this.state.books.map((book, index) => (
                <tr key={ book.id }>
                  <td>{ book.id }</td>
                  <td>{ book.name }</td>
                  <td>{ book.date }</td>
                  <td>{ this.formatPrice(book.price) }</td>
                  <td className="book-count">
                    <button onClick={() => this.changeCount(index, -1)} disabled={ book.count <= 1 + '' }>-1</button>
                    <div className="count">{ book.count }</div>
                    <button  onClick={() => this.changeCount(index, 1)}>+1</button>
                  </td>
                  <td>
                    <button onClick={() => this.removeBook(index)}>移除</button>
                  </td>
                </tr>
              ))
            }
          </tbody>
        </table>

         {/* 当books数据发生改变的时候,会重新执行render方法,也就会重新执行总价,这里的总价实际上有点类似于vue的计算属性 */}
        <div>总价格: { this.formatPrice(this.calcTotalprice()) }</div>
      </div>
    )
  }

  /*
		相关函数抽离位置:
			 1. 和页面渲染有关的函数
			 2. 渲染函数
			 3. 和页面逻辑有关的函数
	*/
  render() {
    return this.state.books.length ? this.renderTable() : <p>购物车为空</p>
  }

  formatPrice(price) {
    // price传入的可能不是数值类型,所以最好先转换为数值类型后在去调用对应的toFixed方法
    return '¥' + Number(price).toFixed(2)
  }

  changeCount(index, step) {
    // react中有一项非常重要的原则叫做数据不可变性
    // 所以修改state中的数据必须使用this.setState方法
    // 不要手动去修改对应的数据,这样会导致状态和界面无法保持一致
    // 所以在这里对state中的数据进行浅拷贝后再进行修改
    // 这里也可以借助一些高阶函数来完成相同的功能
    const books = [...this.state.books]

    books[index].count += step

    this.setState({
      books
    })
  }

  removeBook(index) {
    const books = [...this.state.books]
    books.splice(index, 1)

    this.setState({
      books
    })
  }

  calcTotalprice() {
    return this.state.books.reduce((sum, book) => sum + book.price * book.count, 0)
  }
}