文章目录
- 认识JSX语法
- JSX是什么
- 为什么Rect选择了JSX
- JSX书写规范
- JSX注释编写
- JSX的基本使用
- JSX的事件绑定
- this绑定问题
- 参数传递问题
- JSX的条件渲染
- 常见的条件渲染方式
- JSX的列表渲染
- JSX的原理和本质
- JSX的本质
- 虚拟DOM的创建过程
- 案例练习
认识JSX语法
// 1. 定义根组件
const element = <div>Hello World</div>// 2. 渲染根组件
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(element)
上述代码中element变量的声明右侧赋值的标签语法其实就是一段JSX的语法。
- 它不是一段字符串(因为没有使用引号包裹)
- 它看起来是一段HTML元素,但是我们能在js中直接给一个变量赋值HTML吗?其实是不可以的,如果我们将
type = "text/babel"
去除掉,那么就会出现语法错误。
JSX是什么
JSX是一种JavaScript的语法扩展,也在很多地方称之为JavaScript XML,因为看起来就是一段XML语法,它用于描述我们的UI界面,并且其完成可以和JavaScript融合在一起使用,它不同于Vue中的模块语法,不需要专门学习模块语法中的一些指令(比如v-if、v-for、v-bind等)
为什么Rect选择了JSX
React认为渲染逻辑本质上与其他UI逻辑存在内在耦合。比如UI需要绑定事件(button、a原生等等)、比如UI中需要展示数据的状态、比如在某些状态发生改变时,又需要改变UI,它们之间是密不可分的,所以React没有将标记分离到不同的文件中,而是将它们组合到了一起,这个地方就是组件。
JSX书写规范
- JSX的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div元素(或者Fragment)
- 为了方便阅读,我们通常在jsx的外层包裹一个小括号(),这样可以方便阅读,并且jsx可以进行换行书写
- JSX中的标签可以是单标签,也可以是双标签(如果是单标签,必须以/>结尾)
return (<div><div><h2>{message}</h2></div><div>哈哈哈</div></div>
)
JSX注释编写
return (<div>{/* JSX的注释写法*/}<h2>{message}</h2></div>
)
JSX的基本使用
-
JSX嵌入变量作为子元素
- 情况一:当变量是Number、String、Array类型时,可以直接显示
- 情况二:当变量是null、undefined、Boolean类型时,内容为空
- 如果希望可以显示null、undefined、Boolean,那么需要转成字符串
- 转换的方式有很多,比如toString方法、和空字符串拼接、String等方式
- 情况三:Object对象类型不能作为子元素
-
JSX嵌入表达式
- 运算表达式
- 三元运算符
- 执行一个函数
-
JSX绑定属性
- 比如元素都会有title属性
- 比如img元素会有src属性
- 比如a元素会有href属性
- 比如元素可能需要绑定class
- 比如元素使用内联样式style
JSX的事件绑定
如果原生DOM有一个监听事件,我们可以如何操作?
- 方式一:获取DOM元素,添加监听事件
- 方式二:在HTML原生中,直接绑定onclick
在React中是如何操作呢?
- React事件的命名采用小驼峰(cameClass),而不是纯小写
- 我们需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行
<body><div id="root"></div><script type="text/babel">// 类组件class App extends React.Component {// 组件数据constructor() {super()this.state = {message: "Hello World"}}// 组件方法btnClick() {// 内部完成了两件事:// 1. 将state中的message值修改掉// 2. 自动重新执行render函数this.setState({message: "Hello React"})}// 渲染内容render() {// 简单数据// return <h2>Hello World</h2>// 复杂数据return (<div><h2>{this.state.message}</h2><button onClick={this.btnClick.bind(this)}>修改文本</button></div>)}// 为什么这里要bind一下? 因为类中的方法中的this默认指向undefined, 所以要将render函数中的this给方法}// 将组件渲染到界面上const container = document.getElementById('root');const root = ReactDOM.createRoot(container);root.render(<App />);</script>
</body>
这种数据的数据是定义在当前对象的state中的,我们可以通过构造函数中的this.state={定义的数据},然后当我们数据发生变化时,我们可以调用this.setState来更新数据,并且通知React进行update操作,在进行update操作时,会重新调用render函数,并且使用最新的数据来渲染界面。
this绑定问题
上述代码还存在this绑定问题
在事件执行后,我们可能需要获取当前类的对象中相关的属性,这个时候需要用到this。
在类中直接定义一个函数,并且将这个函数绑定到元素的onClick事件上,当前这个函数的this指向是谁呢?
默认情况下是undefined,因为在正常的DOM操作中,监听点击,监听函数中的this其实是节点对象(比如说button对象),但是因为React并不是直接渲染成真实DOM,我们所编写的button只是一个语法糖,它的本质React的Element对象,那么在这里发生监听的时候,react在执行函数时并没有绑定this,默认情况下就是一个undefined。
我们在绑定的函数中,可能想要使用当前对象,比如执行this.setState函数,就必须拿到当前对象的this,我们就需要在传入函数时,给这个函数直接绑定this:
- 方案一:bind给btnClick显式绑定this
// 写法一
<button onClick={this.btnClick.bind(this)}>修改文本</button>// 写法二
constructor() {super()this.state = {message: "Hello World"}this.btnClick = this.btnClick.bind(this)
}
- 方案二:使用ES6 class fields 语法
- 方案三:事件监听时传入箭头函数(推荐)
<body><div id="root"></div><script type="text/babel">/*this的四种绑定规则:1. 默认绑定 独立执行 2. 隐式绑定 作为对象中的方法执行3. 显式绑定 call apply bind4. new绑定*/class App extends React.Component {constructor() {super()this.state = {counter: 100}}btn1Click() {console.log("btn1Click")this.setState({ counter: this.state.counter + 1 })}// 所有实例都会有btn2Click这个字段,将一个箭头函数赋值给btn2Click,而箭头函数没有this,就会去上层作用域中查找,找到的是当前类的作用域的this,而this正好指向当前实例btn2Click = () => {console.log("btn2Click")this.setState({ counter: this.state.counter + 1 })}btn3Click = () => {console.log("btn3Click")this.setState({ counter: this.state.counter + 1 })}render() {return (<div><h2>当前计数:{this.state.counter}</h2>{/*1. this绑定方式一:bind绑定*/}<button onClick={this.btn1Click.bind(this)}>按钮1</button>{/*2. this绑定方式二:ES6 class fields*/}<button onClick={this.btn2Click}>按钮2</button>{/*3. this绑定方式三:直接传入一个箭头函数(重要)*/}<button onClick={() => this.btn3Click()}>按钮3</button>{/*怎么做的?形成一个隐式绑定,this是当前实例,btn3Click作为当前实例的方法被调用*/}</div>)}}const container = document.getElementById('root');const root = ReactDOM.createRoot(container);root.render(<App />);</script>
</body>
参数传递问题
- 情况一:获取event对象
- 很多时候我们需要拿到event对象来做一些事情(比如阻止默认行为)
- 那么默认情况下,event对象有被直接传入,函数就可以获取到event对象
- 情况二:获取更多参数
- 有更多参数时,最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数
<body><div id="root"></div><script type="text/babel">class App extends React.Component {constructor() {super()this.state = {counter: 100}}btn1Click(event) {console.log("btn1Click", event)}btn2Click(event, name, age) {console.log("btn2Click", event, name, age)}render() {return (<div><h2>当前计数:{this.state.counter}</h2>{/*1. event参数的传递 */}<button onClick={this.btn1Click.bind(this)}>按钮1</button><button onClick={(event) => this.btn1Click(event)}>按钮1</button>{/*2. 额外的参数传递*/}<button onClick={this.btn2Click.bind(this,)}>按钮2</button><button onClick={(event) => this.btn1Click(event, "zy", 18)}>按钮2</button></div>)}}const container = document.getElementById('root');const root = ReactDOM.createRoot(container);root.render(<App />);</script>
</body>
JSX的条件渲染
某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容:
- 在vue中,我们会通过指令来控制:比如v-if、v-show
- 在React中,所有的条件判断都和普通的JavaScript代码一致
常见的条件渲染方式
- 方式一:条件判断语句,适合逻辑较多的情况
- 方式二:三元运算符,适合逻辑比较简单
- 方式三:与运算符&&,适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染
<body><div id="root"></div><script type="text/babel">class App extends React.Component {constructor() {super()this.state = {isReady: true}}render() {const { isReady } = this.statelet showElement = nullif (isReady) {showElement = <h2>准备开始比赛吧</h2>} else {showElement = <h2>请提前做好准备!</h2>}return (<div>{/*1. 方式一:根据条件给变量赋值不同的内容*/}<div>{showElement}</div>{/*2. 方式二:三元运算符*/}<div>{isReady ? <button>开始战斗!</button> : <h3>准备</h3>}</div>{/*3. 方式三:&&逻辑与运算*/}{/*场景:当某一个值,有可能为undefined时,使用&&进行条件判断*/}<div>{info && <div>{info.name + " " + info.desc}</div>}</div></div >)}}const container = document.getElementById('root');const root = ReactDOM.createRoot(container);root.render(<App />);</script>
</body>
JSX的列表渲染
真实开发中我们会从服务器请求到大量数据,数据会以列表的形式存储:
- 比如歌手、歌曲、排行榜列表的数据
- 比如商品、购物车、评论列表的数据
- 比如好友消息、动态、联系人列表数据
在React中并没有像Vue模块语法中的v-for指令,而且需要我们通过JavaScript代码的方式组织数据,转成JSX,那到底如何展示列表呢?在React中,展示列表最多的方式就是使用数组的map高阶函数。很多时候我们在展示一个数据之前,需要先对它进行一些处理:比如过滤掉一些内容:filter函数;比如截取数组中的一部分内容:slice函数。
<body><div id="root"></div><script type="text/babel">class App extends React.Component {constructor() {super()this.state = {students: [{ id: 1, name: "zy", score: 99 },{ id: 2, name: "lgc", score: 88 },{ id: 3, name: "mxy", score: 100 },{ id: 4, name: "jack", score: 66 },{ id: 5, name: "kobe", score: 92 },]}}render() {const { students } = this.statereturn (<div><h2>学生列表数据</h2><div className="list">{// 选出成绩大于90分的人students.filter(item => item.score > 90).map(item => {return (<div className="item" key={item.id}><h2>学号:{item.id}</h2><h2>姓名:{item.name}</h2><h2>分数:{item.score}</h2></div>)})}</div></div>)}}const container = document.getElementById('root');const root = ReactDOM.createRoot(container);root.render(<App />);</script>
</body>
JSX的原理和本质
JSX的本质
实际上,jsx仅仅只是
React.createElement(component, props, ...children)
函数的语法糖,所有的jsx最终都会被转换成React.createElement
的函数调用
React.createElement
需要传递三个参数:
- 参数一:type
- 当前ReactElement的类型
- 如果是标签元素,那么就使用字符串表示“div”
- 如果是组件元素,那么就直接使用组件的名称
- 参数二:config
- 所有jsx中的属性都在config中以对象的属性和值的形式存储
- 比如传入className作为元素的class
- 参数三:children
- 存放在标签中的内容,以children数组的方式进行存储
- 如果是多个元素,React内部有对它们进行处理
虚拟DOM的创建过程
我们通过React.createElement最终创建出来一个ReactElement对象,这个ReactElement对象是什么作用呢?React为什么要创建它呢?这是因为React利用ReactElement对象组成了一个JavaScript的对象树,JavaScript的对象树就是虚拟DOM。
如何查看ReactElement的树结构呢?我们可以将之前的就实现返回结果进行打印:
jsx-虚拟DOM-真实DOM
案例练习
要求:
- 在界面上以表格的形式,显示一些书籍的数据
- 在底部显示书籍的总价格
- 点击+或者-可以增加或减少书籍(如果为1,那么不能继续-)
- 点击移除按钮,可以将书籍移除(当所有书籍移除完毕时,显示:购物车为空~)
<!DOCTYPE html>
<html><head><meta charset="UTF-8" /><title>购物车</title><!-- 添加依赖 --><script src="https://unpkg.com/react@18/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script><!-- babel --><script src="https://unpkg.com/@babel/standalone/babel.min.js"></script><style>table {border-collapse: collapse;text-align: center;}thead {background-color: #f2f2f2;}td,th {padding: 10px 16px;border: 1px solid black;}</style>
</head><body><div id="root"></div><script src="./data.js"></script><script type="text/babel">class App extends React.Component {constructor() {super()this.state = {books: books}}format(price) {return "¥" + Number(price).toFixed(2);}// 方法increment(index) {const newBooks = [...this.state.books]newBooks[index].count += 1this.setState({ books: newBooks })}decrement(index) {const newBooks = [...this.state.books]newBooks[index].count -= 1this.setState({ books: newBooks })}removeItem(index) {const newBooks = [...this.state.books]newBooks.splice(index, 1)this.setState({ books: newBooks })}render() {const { books } = this.state// 计算总价let totalPrice = 0for (let i = 0; i < books.length; i++) {totalPrice += books[i].count * books[i].price}return (<div><table><thead><tr><th>序号</th><th>书籍名称</th><th>出版日期</th><th>价格</th><th>购买数量</th><th>操作</th></tr></thead><tbody >{books.map((item, index) => {return (<tr key={item.id}><td>{item.id}</td><td>{item.name}</td><td>{item.data}</td><td>{this.format(item.price)}</td><td><button disabled={item.count <= 1} onClick={() => this.decrement(index)}>-</button>{item.count}<button onClick={() => this.increment(index)}>+</button></td><td><button onClick={() => this.removeItem(index)}>移除</button></td></tr>)})}</tbody></table><h1>总价格:{this.format(totalPrice)}</h1></div>)}}const container = document.getElementById('root');const root = ReactDOM.createRoot(container);root.render(<App />);</script>
</body></html>
// data.js
const books = [{id: 1,name: '《算法导论》',data: '2006-9',price: 85.0,count: 1,},{id: 2,name: '《UNIX编程艺术》',data: '2006-9',price: 59.0,count: 1,},{id: 3,name: '《编程珠玑》',data: '2006-9',price: 39.0,count: 1,},{id: 4,name: '《代码大全》',data: '2006-9',price: 128.0,count: 1,},
];
container);
root.render();
// data.js
const books = [{id: 1,name: '《算法导论》',data: '2006-9',price: 85.0,count: 1,},{id: 2,name: '《UNIX编程艺术》',data: '2006-9',price: 59.0,count: 1,},{id: 3,name: '《编程珠玑》',data: '2006-9',price: 39.0,count: 1,},{id: 4,name: '《代码大全》',data: '2006-9',price: 128.0,count: 1,},
];