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)
书写规范
-
JSX的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div元素 或 Fragment组件
-
为了方便阅读,我们通常在jsx的外层包裹一个小括号()
- 这样可以方便阅读,并且让JSX可以进行换行书写
- 如果不加小括号,后续的第一个元素的开始标签必须紧跟在return后边
return ( <div> Hello World </div> )
// 如果不加小括号,那么后边第一个元素的开始标签必须紧跟在return关键字后边 // 否则会被解析为 return undefined // 所以推荐return 后边返回的JSX元素使用小括号进行包裹,以提升代码的可读性 return <div> Hello World <div>
-
JSX中的标签可以是单标签,也可以是双标签, 但是如果使用单标签,必须严格闭合标签
插入内容
-
当变量是Number、String、Array类型时,可以直接显示
- 当变量是Array类型的时候,会调用
Array.prototype.join(‘’)
后,再将结果输出
- 当变量是Array类型的时候,会调用
-
当变量是null、undefined、Boolean类型时,内容为空
- 如果希望可以显示null、undefined、Boolean,那么需要转成字符串
- 转换的方式有很多,比如toString方法、和空字符串拼接,String(变量)等方式
- 对于Boolean类型,无论结果为true还是false,其最终内容都不会在界面上进行渲染
-
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中的每一个节点都是一一对应的
虚拟节点的作用
- 虚拟DOM帮助我们从命令式编程转到了声明式编程的模式
- UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象
- 可以通过ReactDOM.render让 虚拟DOM 和 真实DOM同步起来,这个过程中叫做协调(Reconciliation)
- 这种编程的方式赋予了React声明式的API:
- 你只需要告诉React希望让UI是什么状态
- React来确保DOM和这些状态是匹配的
- 你不需要直接进行DOM操作,就可以从手动更改DOM、属性操作、事件处理中解放出来
- 虚拟DOM是在内存中存在的,可以根据不同平台将虚拟DOM渲染成不同的内容
- web端 将虚拟DOM渲染成真实DOM
- APP端 将虚拟DOM渲染为原生控件
- 因为虚拟DOM是在内存中存在的,所以我们可以保存旧的数据状态,和最新的数据状态进行比较 (也就是常说的DIFF算法)
- 从而找到修改DOM的最优方案并以最⼩的代价对DOM进行批量更新,从而最大程度的避免页面的回流和重绘,提升渲染性能
- 因为有新旧虚拟DOM树的存在,所以可以借助一些DevTools工具来追踪对应的数据流的改变,方便我们进行调试和维护
阶段案例
- 在界面上以表格的形式,显示一些书籍的数据
- 在底部显示书籍的总价格
- 点击+或者-可以增加或减少书籍数量(如果为1,那么不能继续-)
- 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~)
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)
}
}