我正在参加「掘金·启航计划」
一、认识JSX
<!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>Document</title>
</head>
<body>
<div id="app"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
const element = <div>123</div>
const app = ReactDOM.createRoot(document.querySelector('#app'))
app.render(element)
</script>
</body>
</html>
这段element变量的声明右侧赋值的标签语法是什么呢?
它不是一段字符串(因为没有使用引号包裹);它看起来是一段HTML元素,但是我们能在js中直接给一个变量赋值html吗? 其实是不可以的,如果我们将 type="text/babel" 去除掉,那么就会出现语法错误:
其实它是一段JSX的语法,那么JSX是什么呢?
- JSX是一种JavaScript的语法扩展 (eXtension) ,也在很多地方称之为JavaScript XML,因为看起就是一段XML语法
- 它用于描述我们的UI界面,并且可以和JavaScript融合在一起使用
- 它不同于Vue中的模块语法,你不需要专门学习模块语法中的一些指令 (比如v-for、v-if、v-else、v-bind)
二、为什么React选择了JSX
React认为渲染逻辑本质上与其他UI逻辑存在内在耦合 :
- 比如UI需要绑定事件(button、a原生等等)
- 比如UI中需要展示数据状态
- 比如在某些状态发生改变时,又需要改变UI
他们之间是密不可分,所以React没有将标记分离到不同的文件中,而是将它们组合到了一起,这个地方就是组件(Component)
三、JSX书写规范
<!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>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
// 1.定义App根组件
class App extends React.Component {
constructor() {
super()
this.state = {
message: "Hello World"
}
}
render() {
const { message } = this.state
return (
<div>
<div>
<h2>{message}</h2>
<br/>
</div>
<div>哈哈哈</div>
</div>
)
}
}
// 2.创建root并且渲染App组件
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App></App>)
</script>
</body>
</html>
- JSX结构中只能有一个根元素
- JSX结构通常会包裹一个(), 将整个jsx当做一个整体, 实现换行
- JSX可以是单标签, 也可以双标签, 但是单标签必须以 /> 结尾
四、JSX中的注释
在JSX中注释内容要包含在'{}'
中:
class App extends React.Component {
constructor() {
super()
this.state = {
message: "Hello World"
}
}
render() {
const { message } = this.state
return (
<div>
{ /* JSX的注释写法 */ }
<h2>{message}</h2>
</div>
)
}
}
五、JSX中插入内容
插入Number/String/Array
当在JSX中插入Number/String/Array时,内容可以直接显示
class App extends React.Component {
constructor() {
super()
this.state = {
count: 100,
message: 'hello',
array: ['a', 'b', 'c']
}
}
render() {
const { count, message, array } = this.state
return (
<div>
<h1>{count}</h1>
<h1>{message}</h1>
<h1>{array}</h1>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)
数组中的内容会拼接到一起显示出来
插入Null/Undefined/Boolean
当插入Null/Undefined/Boolean时,内容将展示为空,如果想展示出来那么需要转成字符串
- 通过String(xx)
- 使用.toString()
- 或在后面+""
class App extends React.Component {
constructor() {
super()
this.state = {
a: null,
b: undefined,
c: true
}
}
render() {
const { a, b, c } = this.state
return (
<div>
<h1>{a + ""}</h1>
<h1>{String(b)}</h1>
<h1>{c.toString()}</h1>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)
需要注意的是:
如果是null
或undefined
时,在转换成字符串时不要通过.toString()进行转换,会报错;对于Boolean
来说,以上三种方式都可以
Object类型的数据不能被插入
当一个数据为Object
类型时不能被插入,会报错
- 一般我们会去展示其中的某一属性值或key
class App extends React.Component {
constructor() {
super()
this.state = {
friend: { name: "zhangsan" },
}
}
render() {
const { friend } = this.state
return (
<div>
{ /* <h1>{friend}</h1> 不能这样,会报错*/}
<h1>{friend.name}</h1>
<h1>{Object.keys(friend)[0]}</h1>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)
插入表达式
在JSX中可以嵌入三元表达式
class App extends React.Component {
constructor() {
super()
this.state = {
age: 7,
}
}
render() {
const { age } = this.state
const ageText = age >= 18 ? '成年' : '未成年'
return (
<div>
<h1>{ age >= 18 ? '成年' : '未成年'}</h1>
<h1>{ ageText }</h1>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)
也可以嵌入运算表达式
class App extends React.Component {
constructor() {
super()
this.state = {
count1: 10,
count2: 20
}
}
render() {
const { count1, count2} = this.state
return (
<div>
<h1>sum: {count1 + count2}</h1>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)
也可以调用函数
class App extends React.Component {
constructor() {
super()
this.state = {
movies: ["流浪地球", "星际穿越", "独行月球"]
}
}
render() {
const { movies } = this.state
return (
<div>
<ul>{ movies.map(item => <li>{item}</li>)}</ul>
<ul>{ this.getMovieEls() }</ul>
</div>
)
}
getMovieEls() {
const liEls = this.state.movies.map(movie => <li>{movie}</li>)
return liEls
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)
六、JSX中绑定属性
绑定title/href/src属性
class App extends React.Component {
constructor() {
super()
this.state = {
title: '123',
imgURL: "https://ts1.cn.mm.bing.net/th/id/R-C.95bc299c3f1f0e69b9eb1d0772b14a98?rik=W5QLhXiERW4nLQ&riu=http%3a%2f%2f20178405.s21i.faiusr.com%2f2%2fABUIABACGAAgoeLO-wUo4I3o2gEw8Qs4uAg.jpg&ehk=N7Bxe9nqM08w4evC2kK6yyC%2bxIWTjdd6HgXsQYPbMj0%3d&risl=&pid=ImgRaw&r=0",
href: "https://www.baidu.com",
}
}
render() {
const { title, imgURL, href} = this.state
return (
<div>
<h2 title={title}>我是h2元素</h2>
<img src={imgURL} alt=""/>
<a href={href}>百度一下</a>
</div>
)
}
getMovieEls() {
const liEls = this.state.movies.map(movie => <li>{movie}</li>)
return liEls
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App/>)
绑定class属性
在绑定class时,使用className来进行绑定
方式一:使用字符串拼接
render() {
const {isActive} = this.state
const classStr = `abc cba ${isActive ? 'active': ''}`
return (
<div>
<h2 className={classStr}>123</h2>
</div>
)
方式二:将class放入数组中,在通过join拼接
render() {
const classList = ["abc", "cba"]
if (isActive) classList.push("active")
return (
<div>
<h2 className={classList.join(" ")}>123</h2>
</div>
)
绑定style属性
class App extends React.Component {
constructor() {
super()
this.state = {
objStyle: {color: "red", fontSize: "30px"}
}
}
render() {
const { objStyle } = this.state
return (
<div>
<h2 style={{color: "red", fontSize: "30px"}}>123</h2>
<h2 style={objStyle}>123</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
需要注意的是,不要将style={{....}}
看成vue中的模板语法,这里指的是{}中包裹style对象
七、事件绑定
React中的事件命名采用驼峰式,而不是纯小写
- 需要通过
{}
来包裹传入的处理事件函数,该函数会在事件发生时被执行
this指向问题
但是需要注意的是,如果在{}中直接通过this.xx来传入函数时,该函数中的this指向undefined
class App extends React.Component {
btnClick(){
console.log("btnClick", this);
}
render() {
return (
<div>
<button onClick={this.btnClick}>按钮</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
当进行点击按钮时会发现输出undefined,这是为什么呢?
这是因为<button ...>这段JSX代码本质上是:React.createElement('button', { onClick: this.btnClick })
当按钮发生点击的时候会拿到赋值给onClick的函数直接进行调用
相当于:
const click = config.onClick
click()
也就是说内部在调用传入的函数时是独立调用的,而class在转换代码时是默认开启严格模式的,所以this指向undefined
那么如何解决this问题呢?
方式一:可以对传入的函数绑定this
render() {
return (
<div>
<button onClick={this.btnClick.bind(this)}>按钮</button>
</div>
)
}
也可以在constructor中统一进行绑定(也是官方建议的方式)
class App extends React.Component {
constructor() {
super()
this.state = {
counter: 100
}
this.btnClick = this.btnClick.bind(this)
}
btnClick() {
console.log("btnClick", this);
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div>
<button onClick={this.btn1Click}>按钮</button>
<h2>当前计数: {this.state.counter}</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
方式二:将函数写为箭头函数形式
class App extends React.Component {
constructor() {
super()
this.state = {
counter: 100
}
}
btnClick = () => {
this.setState({ counter: 1000 })
}
render() {
return (
<div>
<button onClick={this.btnClick}>按钮2</button>
<h2>当前计数: {this.state.counter}</h2>
</div>
)
}
}
由于箭头函数本身不绑定this,它会去其上层作用域去找this
这里它的上层作用域是该类的作用域,该类的作用域指向当前class即该组件App的实例,所以通过这种方式找到的this就是当前组件的实例
方式三:直接传入一个箭头函数
<button onClick={() => console.log("123")}>按钮</button>
既然直接可以传入一个箭头函数,所以也可以写成以下方式:
class App extends React.Component {
constructor() {
super()
this.state = {
counter: 100
}
}
btnClick() {
console.log("btnClick", this);
this.setState({ counter: 9999 })
}
render() {
return (
<div>
<button onClick={() => this.btnClick()}>按钮</button>
<h2>当前计数: {this.state.counter}</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
采用箭头函数的方式,可以在箭头函数中调用实例方法并执行
通过this.xx的方式执行该方法时,this便会绑定为当前组件实例。推荐这种方式,因为方便传递参数
传递event和其他参数
有时候当事件发生时需要将参数传递过去
- 在React中事件发生时同样也会默认传递event对象
<!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>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
}
btnClick(event) {
console.log("btnClick:", event)
}
render() {
return (
<div>
<button onClick={this.btnClick.bind(this)}>按钮1</button>
<button onClick={(event) => this.btnClick(event)}>按钮2</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
</script>
</body>
</html>
需要注意的是,如果要通过箭头函数的方式来绑定事件处理函数时,要手动传入event:
<button onClick={(event) => this.btnClick(event)}>按钮2</button>
也可以传递额外参数:
class App extends React.Component {
btnClick(event, name, age) {
console.log("btnClick:", event, this)
console.log("name, age:", name, age)
}
render() {
const { message } = this.state
return (
<div>
<button onClick={(event) => this.btnClick(event, "zs", 18)}>按钮4</button>
</div>
)
}
}
案例
实现一个电影列表,点击某一项时,字体颜色变成红色
<!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>Document</title>
<style>
.active {
color: red;
}
</style>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
movies: ["星际穿越", "盗梦空间", "大话西游", "流浪地球"],
currentIndex: 0 // 记录当前选中的项
}
}
selectMovie(index) {
this.setState({
currentIndex: index
})
}
render() {
const { movies, currentIndex } = this.state
return (
<ul>
{movies.map((item, index) => {
return (
<li
key={item}
className={currentIndex === index ? 'active' : ''}
onClick={() => this.selectMovie(index)}
>
{item}
</li>
)
})}
</ul>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
</script>
</body>
</html>
也可以将movies.map抽取出来:
render() {
const { movies, currentIndex } = this.state
const liEls = movies.map((item, index) => {
return (
<li
className={ currentIndex === index ? 'active': '' }
key={item}
onClick={() => this.itemClick(index)}
>
{item}
</li>
)
})
return (
<div>
<ul>{liEls}</ul>
</div>
)
}
或者将map中的方法抽取出来:
render() {
const { movies, currentIndex } = this.state
const itemHandle = (item, index) => {
return (
<li
className={ currentIndex === index ? 'active': '' }
key={item}
onClick={() => this.itemClick(index)}
>
{item}
</li>
)
}
return (
<div>
<ul>{movies.map(itemHandle)}</ul>
</div>
)
}
八、JSX中的条件渲染
某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容:
- 在vue中,我们会通过指令来控制:比如v-if、v-show
- 而在React中,所有的条件判断都和普通的JavaScript代码一致
方式一:根据不同条件为变量赋值不同的内容
class App extends React.Component {
constructor() {
super()
this.state = {
flag: 1
}
}
render() {
let showElement;
if (this.state.flag === 1) {
showElement = <h1>Hello World!</h1>
} else {
showElement = <h1>Hello React!</h1>
}
return showElement;
}
}
方式二:使用三元表达式
render() {
return (
<h1>{this.state.flag === 1 ? 'Hello World!' : 'Hello React!'}</h1>
)
}
方式三:使用逻辑与运算符'&&'
场景:当该值可能为null或undefined时,我们可以采用&&来决定是否展示其中的内容
class App extends React.Component {
constructor() {
super()
this.state = {
person: undefined
}
}
render() {
const { person } = this.state
return (
<div>
name: {person && <h1>{person.name}</h1>}
</div>
)
}
}
案例
案例:通过按钮点击来切换内容是否展示
<!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>Document</title>
<style>
.active {
color: red;
}
</style>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
isShow: true
}
}
changeShow() {
this.setState({ isShow: !this.state.isShow })
}
render() {
const { isShow } = this.state
return (
<div>
<button onClick={() => this.changeShow()}>切换</button>
{isShow && <h1>hi</h1>}
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
</script>
</body>
</html>
也可以为通过添加style属性来控制元素是否展示从而达到vue中v-show的效果:
render() {
const { isShow } = this.state
return (
<div>
<button onClick={() => this.changeShow()}>切换</button>
<h1 style={{ display: isShow ? 'block' : 'none' }}>hi</h1>
</div>
)
}
九、JSX中的列表渲染
在React中,展示列表最多的方式就是使用数组的map高阶函数,但是在很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理:
- 比如过滤掉一些内容:filter函数
- 比如截取数组中的一部分内容:slice函数
比如,获取大于90分的同学,并展示前两名:
<!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>Document</title>
<style>
.item {
border: 1px solid green;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
students: [
{ id: 111, name: "zs", score: 99 },
{ id: 112, name: "ls", score: 100 },
{ id: 113, name: "ww", score: 80 },
{ id: 114, name: "zl", score: 90 },
]
}
}
render() {
const { students } = this.state
return (
<div>
{
students.filter(stu => stu.score > 90).slice(0, 2).map(stu => {
return (
<div className="item" key={stu.id}>
<h2>学号:{stu.id}</h2>
<h2>姓名:{stu.name}</h2>
<h2>分数:{stu.score}</h2>
</div>
)
})
}
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
</script>
</body>
</html>
需要注意的是,在遍历元素时要为元素增加key属性,否则会报如下警告:
这个警告是告诉我们需要在列表展示的JSX中添加一个key
- key主要的作用是为了提高diff算法时的效率
十、JSX的本质
JSX最终会被转换成什么
实际上,JSX仅仅只是 React.createElement(type, config, children) 函数的语法糖
Babel在每遇到一个标签时都会进行转换:
所有的JSX最终都会被转换成React.createElement的函数调用
createElement需要传递三个参数:
参数一:type ,表示当前ReactElement的类型
如果是标签元素,那么就使用字符串表示 如“div”
如果是组件元素,那么就直接使用组件的名称
参数二:config
所有jsx中的属性都在config中以对象的形式存储属性和值
- 比如传入className作为元素的class
参数三:children
存放在标签中的内容,以children数组的方式进行存储
可以在Babel官网查看JSX代码的转换过程:
可见每个元素都会转换为React.createElement
- /#PURE/这个注释表示该函数调用是一个纯函数调用,有利于摇树优化
如果将JSX写法换成React.createElement的形式便不需要Babel进行转换了,此时也不需要在script标签上添加type="text/babel"标识了:
<!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>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script>
class App extends React.Component {
constructor() {
super()
this.state = {
message: "Hello World"
}
}
render() {
const { message } = this.state
const element = React.createElement("div", null,
/*#__PURE__*/React.createElement("div", {
className: "header"
}, "Header"),
/*#__PURE__*/React.createElement("div", {
className: "content"
}, "Content",
/*#__PURE__*/React.createElement("ul", null,
/*#__PURE__*/React.createElement("li", null, "1"),
/*#__PURE__*/React.createElement("li", null, "2"),
/*#__PURE__*/React.createElement("li", null, "3")
)));
console.log(element)
return element
}
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(React.createElement(App, null))
</script>
</body>
</html>
打印创建出的最终的element:
通过React.createElement函数会创建出一个ReactElement对象,这些嵌套的ReactElement对象会组成一个JavaScript对象树
- 这个JavaScript对象树就是虚拟DOM
- 这个虚拟DOM最终会被渲染为真实DOM展示出来
JSX -> 虚拟DOM -> 真实DOM
React的解析过程相对于Vue来说更加的直观:
在Vue中有专门自己的解析库:
- 将template解析为render函数,而且解析过程比较麻烦,因为还要解析各种指令,如v-if v-for v-model等等
- 在render函数中调用了大量的h函数
- h函数调用完毕后才变成一个个的JS对象
- 这些对象之间产生联系最终变为虚拟DOM,最后再变成真实DOM
- 当然Vue中也可以写JSX,当写成JSX后也是交由Babel去解析的,我们平常所写的template则是由Vue自己的解析库来解析的
而React中对JSX的解析是交给Babel的,一遇到标签就会解析为React.createElement,更加简单明了
为什么要使用虚拟DOM
- 在使用了虚拟DOM后,一旦数据更新了之后就不用将整段结构都重新渲染(方便快速进行diff)
在数据更新后,会重新执行render方法生成新的虚拟DOM,此时就会拿新的虚拟DOM与旧的虚拟DOM进行diff,diff时会发现更新了哪部分内容,只会去重新渲染更新部分的内容
- 由于虚拟DOM本质是JS对象,那么既可以通过React渲染成为Web端的节点,又可以渲染成为手机端的控件,从而做到跨平台
- 虚拟DOM帮助我们从命令式编程转到了声明式编程的模式
React官方的说法:Virtual DOM 是一种编程理念。
在这个理念中,UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象,我们可以通过ReactDOM.render让 虚拟DOM 和 真实DOM同步起来。
这种编程的方式赋予了React声明式的API:
- 只需要告诉React希望让UI是什么状态
- React来确保DOM和这些状态是匹配的
- 你不需要直接进行DOM操作,就可以从手动更改DOM、属性操作、事件处理中解放出来