小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
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需要传递三个参数:
- type --- 当前ReactElement的类型
- 如果是标签元素,那么就使用字符串表示 如“div” --- 标签名
- 如果是组件元素,那么就直接使用组件的名称 如App --- 组件实例对象
- config --- 属性
- 所有jsx中的属性都在config中以对象的属性和值的形式存储
- 没有属性可以传递null
- 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存在的原因
-
原有的开发模式,我们很难跟踪到状态发生的改变,不方便针对我们应用程序进行调试
但是使用虚拟DOM之后,因为虚拟DOM是一个对象,所以我们可以很方便的存储新旧节点
可以对数据的变化进行检测,方便我们对代码进行调试操作
-
传统的开发模式会进行频繁的DOM操作,而这一的做法性能非常的低
-
document.createElement本身创建出来的就是一个非常复杂的对象
-
DOM操作容易引起浏览器的回流和重绘,所以在开发中应该避免频繁的DOM操作
-
虚拟DOM让UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象
-
我们对于DOM的频繁操作可以直接先在内存中对VDOM进行相应的操作,操作完毕以后在统一映射到真实DOM上
也就是将DOM操作进行批量化处理
-
-
虚拟DOM帮助我们从命令式编程转到了声明式编程的模式
- 只需要告诉React希望让UI是什么状态
- React来确保DOM和这些状态是匹配的
- 不需要直接进行DOM操作,只可以从手动更改DOM、属性操作、事件处理中解放出来
阶段案例
<!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>