React学习 --- JSX

349 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

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

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

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

JSX的书写规范:

  • JSX的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div元素或使用Fragment标签

  • 为了方便阅读,我们通常在jsx的外层包裹一个小括号(),这样可以方便阅读

  • JSX中的标签可以是单标签,也可以是双标签,但如果是单标签,必须以/>结尾;

  • 和原生HTML标签不一样的是,JSX是严格区分大小写的(当一个标签的首字母是大写的,该标签会被作为组件来进行解析)

// 这里的<h2>Hello World</h2>就是JSX语法
// 其本质会被babel转换为ReactRenderObject对象,
// 也就是其本质上是一个对象,可以认为jsx其实是react提供的一种特殊的值
const message = <h2>Hello World</h2>
ReactDOM.render(message, document.getElementById('app'))

注释

{/* 这是JSX中的注释,JSX中只有单行注释 */}

嵌入变量

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

class App extends  React.Component {
  constructor() {
    super()
    this.state = {
      num: 18,
      name: 'Klaus',
      friends: ['Alex', 'Steven', 'Jhon']
    }
  }

  render() {
    return (
      <div>
        {/* 18 */}
        <h2>{ this.state.num }</h2>

        {/* Klaus */}
        <h2>{ this.state.name }</h2>

        {/* AlexStevenJhon */}
        <h2>{ this.state.friends }</h2>
      </div>
    )
  }
}

当变量是null、undefined、Boolean(无论是true还是flase),Symbol类型时,内容为空

  • 如果希望可以显示null、undefined、Boolean,那么需要转成字符串
  • 可以使用toString方法进行转换,但是null和undefined是没有toString方法的
  • 所以比较通用的方法是,使用String方法或者直接拼接上一个空字符串
class App extends  React.Component {
  constructor() {
    super()
    this.state = {
      test1: null,
      test2: undefined,
      test3: true,
      test4: false,
      sym: Symbol('test')
    }
  }

  render() {
    return (
      <div>
        {/* 没有任何显示,值被忽略 */}
        <h2>{ this.state.test1 }</h2>

        {/* 没有任何显示,值被忽略 */}
        <h2>{ this.state.test2 }</h2>

        {/* 没有任何显示,值被忽略 */}
        <h2>{ this.state.test3 }</h2>

        {/* 没有任何显示,值被忽略 */}
        <h2>{ this.state.test4 }</h2>
        
        {/* 没有任何显示,值被忽略 */}
        <h2>{ this.state.sym }</h2>
      </div>
    )
  }
}

对象类型不能作为子元素(not valid as a React child)

class App extends  React.Component {
  constructor() {
    super()
    this.state = {
      user: {
        name: 'Klaus',
        age: 23
      }
    }
  }

  render() {
    return (
      <div>
        {/* error, 报错 */}
        <h2>{ this.state.user }</h2>
      </div>
    )
  }
}

如果需要输出的值是NaN的时候,React会将NaN以字符串的形式进行展示,并报警告

如果的确需要展示NaN,需要将其显示(手动)将其转换为字符串后再进行显示

嵌入表达式

在大括号语法中,不单单可以使用变量,任何合法的JS表达式都是可以使用的

class App extends  React.Component {
  constructor() {
    super()
    this.state = {
      firstName: 'Klaus',
      lastName: 'Wang',
      isLogin:  true
    }
  }

  render() {
    return (
      <div>
        {/* 算术运算符 */}
        <h2>{ this.state.firstName + ' ' +  this.lastName }</h2>

        {/* 三目运算符 */}
        <h2>{ this.state.isLogin ? '登录成功' : '请先登录' }</h2>

        {/* 函数调用表达式 */}
        <h2>{ this.getFullName() }</h2>
      </div>
    )
  }

  getFullName() {
    // 这个函数是在render方法中被调用的
    // 并不是作为事件的回调函数交给React内部来进行处理
    // 所以在这个函数的内部是可以正确获取到this的值,即为当前组件类的实例对象
    return this.state.firstName + ' ' + this.state.lastName
  }
}

属性绑定

普通属性

class App extends  React.Component {
  constructor() {
    super()
    this.state = {
      title: 'DIV的title'
    }
  }

  render() {
    return (
      <div title={this.state.title}>title</div>
    )
  }
}

绑定class

class App extends  React.Component {
  constructor() {
    super()
    this.state = {
      active: true
    }
  }

  render() {
    return (
      <div>
        {/*
          因为class在js中是关键字,所以这里的class属性的名称需要修改为className
          同样的<label for="">中的for在js中也是关键字,所以需要使用htmlFor来进行替代
        */}

        {/*
           需要注意的是加法的优先级要比三目运算符的优先级高, 所以这里三目运算符的括号是不可以省略的
        */}
        <div className={ 'title foo ' + (this.state.active ? 'active' : '')  }>title</div>
      </div>
    )
  }
}

绑定style

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

  render() {
    return (
      <div>
        {/*  绑定style的时候,第一个括号是大括号语法,第二个style是js的对象的大括号  */}
        {/*
           表示style的对象中,
             key --- 如果是多个单词组成,需要使用引号包裹或者使用小驼峰
             value --- 字符串,或存储了值的变量
        */}
        <div style={{
           color: 'red',
           fontSize: '20px',
           'background-color': 'gray'
          }}>title</div>
      </div>
    )
  }
}

事件绑定

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写
  • 我们需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行;
class App extends  React.Component {
  constructor() {
    super()
  }

  render() {
    return (
      <button onClick={ this.btnClick }>click me</button>
    )
  }

  btnClick() {
    console.log('按钮被点击了')
  }
}

但是React的dom事件在调用的时候,会显示的使用call方法,将this修改为undefined

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

  render() {
    return (
      <button onClick={ this.btnClick }>click me</button>
    )
  }

  btnClick() {
    console.log(this) // => undefined
  }
}

而我们实际在执行对应的事件的时候,我们实际需要的this其实是当前组件类的实例对象,所以我们需要手动对事件中的this进行修正

解决方法一 ----使用bind函数

因为在显示绑定中,bind函数的优先级比call函数的优先级高,所以我们可以通过bind方法来修正this指向

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

    this.state = {
      message: 'Hello React'
    }
  }

  render() {
    return (
      <button onClick={ this.btnClick.bind(this) }>click me</button>
    )
  }

  btnClick() {
    console.log(this.state.message)
  }
}

但是此时如果btnClick方法如果被多次调用,那么也就意味着需要多次绑定this的值,因此我们可以在state中直接修改函数体

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

    this.btnClick = this.btnClick.bind(this)

    this.state = {
      message: 'Hello React'
    }
  }

  render() {
    return (
      <button onClick={ this.btnClick }>click me</button>
    )
  }

  btnClick() {
    console.log(this.state.message)
  }
}

解决方法二 --- 使用箭头函数和class fields

也就是在函数定义的时候,直接使用箭头函数来定义函数体

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

    this.state = {
      message: 'Hello React'
    }
  }

  render() {
    return (
      <button onClick={ this.btnClick }>click me</button>
    )
  }

  // 执行btnClick的时候,因为函数内部是没有this指向的
  // 所以其会去上层作用域查找,而class内部函数的this的值默认就是class所对应的实例对象
  // 所以此时的this就是App组件的实例对象
  btnClick = () => console.log(this.state.message)
}

解决方法三 --- 在函数调用的外部包裹一层箭头函数

其实现原理和使用箭头函数与class filelds来修正this的原理是一致的

但是使用这种方式去修正this,更有利于在执行函数的时候进行参数的传递

所以这是最为推荐的一种作法

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

    this.state = {
      message: 'Hello React'
    }
  }

  render() {
    return (
      <div>
        {/* 箭头函数内部的函数需要被调用,也就是看似调用箭头函数,实际是执行内部的函数 */}
        <button onClick={ () => this.btnClick(this) }>click me</button>
      </div>
    )
  }

  btnClick() {
    console.log(this.state.message)
  }
}

事件参数传递

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

react在执行DOM事件的时候,会默认将事件对象作为参数传递过来

但是这个事件对象并不是浏览器原生的事件对象,而是react中对原生事件对象进行扩展后的新对象

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

  render() {
    return (
      <div>
        <button onClick={ this.btnClick.bind() }>click me</button>
      </div>
    )
  }

  btnClick(e) {
    // 获取react中的事件合成对象
    console.log(e)

    // 获取原生的事件对象
    console.log(e.nativeEvent)
  }
}

很多时候,我们不单单需要获取事件对象,同时我们需要获取调用事件时候手动传入的参数

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

  render() {
    return (
      <div>
        <button onClick={ this.btnClick.bind(this, 'Klaus') }>click me</button>
      </div>
    )
  }

  // 事件参数会作为最后一个参数被传入
  btnClick(name, e) {
    console.log(name)
    console.log(e)
  }
}
class App extends  React.Component {
  constructor() {
    super()
  }

  render() {
    return (
      <div>
        {/* 此时我们可以手动决定事件对象作为第几个参数被传入 */}
        <button onClick={ e => { this.btnClick('Klaus', e) } }>click me</button>
      </div>
    )
  }

  btnClick(name, e) {
    console.log(name)
    console.log(e)
  }
}

条件渲染

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

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

    this.state = {
      isLogin: false
    }
  }

  render() {
    const { isLogin } = this.state

    // 条件渲染方式一: if表达式
    let message  = null
    if (isLogin) {
      // JSX最后会被解析为对象
      // 所以JSX可以作为react中一种特殊的'值'进行相应的赋值操作
      message = <h2>登录成功</h2>
    } else {
      message = <h2>请先登录</h2>
    }


    return (
      <div>
        {message}
        
        {/* 条件渲染方式二: 三目运算符 */}
        <button onClick={() => this.change()}>{ isLogin ? '退出' : '登录' }</button>

        <hr />

        {/* 条件渲染方式三: 逻辑与 */}
        { isLogin && <h2>'Klaus'</h2> }
      </div>
    )
  }


  change() {
    this.setState({
      isLogin: !this.state.isLogin
    })
  }
}

模拟v-show

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

    this.state = {
      isLogin: false
    }
  }

  render() {
    const { isLogin } = this.state

    return (
      <div>
        <h2 style={{
            display: isLogin ? 'block' : 'none'
          }}>登录成功</h2>

        <button onClick={ () => this.change()}>change login status</button>
      </div>
    )
  }

  change() {
    this.setState({
      isLogin: !this.state.isLogin
    })
  }
}

列表渲染

在React中使用map, filter等js的高阶函数来实现列表的渲染

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

    this.state = {
      numbers: [113, 443, 556, 332, 23, 21, 5, 98, 34]
    }
  }

  render() {

    return (
      <ul>
        {
          this.state.numbers.filter(item => item >= 50)
            .map(item => <li>{ item }</li>)
         }
      </ul>
    )
  }
}

JSX的本质

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

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

createElement需要传递三个参数:

  1. type --- 当前ReactElement的类型
    • 如果是标签元素,那么就使用字符串表示 如“div” --- 标签名
    • 如果是组件元素,那么就直接使用组件的名称 如App --- 组件实例对象
  2. config --- 属性
    • 所有jsx中的属性都在config中以对象的属性和值的形式存储
    • 没有属性可以传递null
  3. children --- 子元素
    • 存放在标签中的内容,以剩余参数的方式依次将子元素进行传入
    • 如果只有一个子元素,那么这个子元素会直接作为props.children进行存储
    • 如果有多个子元素,那么这些子元素会被整合为一个数组,然后在作为props.children的值进行存储
const message = <h2>Hello React</h2>
ReactDOM.render(message, document.getElementById('app'))

上面的代码最终会被转换为下面的这种书写方式 ( 具体转换可以查看这里进行测试)

// 此时没有使用JSX,不需要babel进行转换,所以可以不使用babel
const message = React.createElement('h2', null, 'Hello React')
ReactDOM.render(message, document.getElementById('app'))
const message = (<div>
    <span>Hello</span>
    <span>World</span>
  </div>)
ReactDOM.render(message, document.getElementById('app'))

转换后

// babel在转换代码的时候,会自动开启严格模式
"use strict";

var message = /*#__PURE__*/ React.createElement(
  "div",
  null,
  /*#__PURE__*/ React.createElement("span", null, "Hello"),
  /*#__PURE__*/ React.createElement("span", null, "World")
);
ReactDOM.render(message, document.getElementById("app"));

React.createElement(component, props, ...children) 的children接收

// react源码中关于createElement方法接收children参数的代码
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
  props.children = children;
} else if (childrenLength > 1) {
  const childArray = Array(childrenLength);
  for (let i = 0; i < childrenLength; i++) {
    childArray[i] = arguments[i + 2];
  }
  props.children = childArray;
}

可见,传递children的时候是使用类似于剩余参数的形式进行传递的, 但是react在实际解析的时候会主动通过arguments来接收传入的各个子元素,将他们转换为一个数组

虚拟DOM

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

ReactElement对象本质上就是一个JS对象

ReactElement对象就是我们常说的虚拟节点(VNode)

而多个ReactElement对象组成了一个JavaScript的对象树,就是虚拟DOM(VDOM)

虚拟DOM是在内存中模拟的DOM树,所有的操作全部在内存中执行完毕,所以执行效率相对较高

VDOM 和 DOM 本质上是一一对应的映射关系,VDOM是DOM树在内存中的模拟

React在实际解析的中,会将我们的JSX转换为VDOM,所有的操作全部在VDOM执行完毕后

再使用ReactDOM.render方法将VDOM,根据不同的平台渲染成不同的内容

VDOM存在的原因

  1. 原有的开发模式,我们很难跟踪到状态发生的改变,不方便针对我们应用程序进行调试

    但是使用虚拟DOM之后,因为虚拟DOM是一个对象,所以我们可以很方便的存储新旧节点

    可以对数据的变化进行检测,方便我们对代码进行调试操作

  2. 传统的开发模式会进行频繁的DOM操作,而这一的做法性能非常的低

    • document.createElement本身创建出来的就是一个非常复杂的对象

    • DOM操作容易引起浏览器的回流和重绘,所以在开发中应该避免频繁的DOM操作

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

    • 我们对于DOM的频繁操作可以直接先在内存中对VDOM进行相应的操作,操作完毕以后在统一映射到真实DOM上

      也就是将DOM操作进行批量化处理

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

    • 只需要告诉React希望让UI是什么状态
    • React来确保DOM和这些状态是匹配的
    • 不需要直接进行DOM操作,只可以从手动更改DOM、属性操作、事件处理中解放出来

阶段案例

IpnQvf.png

<!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>阶段案例</title>
  <script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
  <div id="app"></div>

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

        this.state = {
          books: [
            {
              id: 1,
              name: '《算法导论》',
              date: '2006-9',
              price: 85.00,
              count: 2
            },
            {
              id: 2,
              name: '《UNIX编程艺术》',
              date: '2006-2',
              price: 59.00,
              count: 1
            },
            {
              id: 3,
              name: '《编程珠玑》',
              date: '2008-10',
              price: 39.00,
              count: 1
            },
            {
              id: 4,
              name: '《代码大全》',
              date: '2006-3',
              price: 128.00,
              count: 1
            },
          ]
        }
      }

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

              <tbody>
                {
                  this.state.books.map((book, index) => (
                    <tr key={book.name}>
                      <td>{ book.name }</td>
                      <td>{ book.date }</td>
                      <td>{ this.formatPrice(book.price, book.count) }</td>
                      <td>
                        <button
                          onClick={() => this.changeBookCount(1, index)}>
                            +1
                        </button>
                        <span>{ book.count }</span>
                        <button
                          disabled={ this.state.books[index].count <= 1 }
                          onClick={() => this.changeBookCount(-1, index)}
                        >
                            -1
                        </button>
                      </td>
                      <td>
                        <button onClick={() => this.remove(index)}>移除</button>
                      </td>
                    </tr>
                  ))
                }
              </tbody>
            </table>
            <h4>总价格为: { this.getTotalPrice() }</h4>
          </div>
        )
      }
			
      // 渲染性质的函数(返回jsx的函数)一般推荐写在render函数的上边
      
      render() {
        return (
          this.state.books.length ? this.renderTable() : <h2>购物车为空~</h2>
        )
      }
      
      // 功能性质的函数(业务逻辑相关的函数) 一般推荐写在render函数的下边

      formatPrice(price = 0, count = 1) {
        if (typeof price !== 'number') {
          price = Number(price) || 0
        }
        return '¥' + (price * count).toFixed(2)
      }

      changeBookCount(tag, index) {
        // React有一个十分重要的原则: 数据的不可变性
        // 也就是 永远不要主动去显示修改state中存储的数据(状态)
        // 如果一定要修改state中的数据,一定要使用setState方法
        const books = [... this.state.books]
        
        // 这里一种实现方法是使用对象的浅拷贝
        // 另一种实现方式是使用map,filter等高阶函数结合index来进行实现
        books[index].count += tag

      	// 使用setState去更新数据,避免界面中对应的状态和逻辑中对应的状态不一致
        this.setState({
          books
        })
      }

      getTotalPrice() {
        // array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
        return this.formatPrice(this.state.books.reduce((total, curr) => total + curr.count  * curr.price, 0))
      }

      remove(i) {
        const books = [... this.state.books]
        // splice方法的返回值是由被删除的元素组成的一个数组。
        // 如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组
        books.splice(i, 1)

        this.setState({
          books
        })
      }
   }

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