JSX 全称是 JavaScriptXML,是一种类似于 XML 的 JavaScript 扩展语法。它允许 HTML 与 JavaScript 的混写,HTML 可以直接写在 JavaScript 之中。
遇到 HTML 标签(以 <
开头),就用 HTML 规则解析:若小写字母开头,则将标签转为 HTML 中的同名元素,若 HTML 中无该标签对应的同名元素,则报错;若大写字母开头, React 就去渲染对应的组件,若组件没有定义,则报错。 遇到代码块(以 {
开头),就用 JavaScript 规则解析。
XML 早期用于存储和传输数据,后来逐渐被 JSON 代替了。
// 使用 JSX 语法定义一个 React 元素
const element = <h1>Hello,React</h1>
// 渲染 React 元素到页面中
<div id="root"></div>
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(element)
为什么 React 会选择 JSX 这种语法?
React 认为渲染逻辑与 UI 逻辑存在很强的内在耦合。比如:在 UI 中需要展示数据状态、绑定事件;当某些数据状态改变时,需要改变 UI。因此 React 决定实现一种 HTML in JS 的模式,就是 JSX。
JSX 的本质:
JSX 本质上仅仅是 React.createElement(type, config, children)
的语法糖,所有的 JSX 最终都会被转换成 React.createElement()
函数的调用。JSX 就是用来简化创建虚拟 DOM 的写法的。
React.createElement()
的三个参数:
- type:当前 ReactElement 的类型。如果是 HTML 元素,就使用标签名;如果是 React 组件,就使用组件的名称。
- config:所有属性和属性值,以对象的形式存储。
- children:标签中的内容,以数组的形式存储。
JSX 的转换过程:
浏览器不识别 JSX 语法,可以使用 Babel 将 JSX 转换为浏览器可识别的 JS。Babel 每遇到一个标签就会将其转换成 React.createElement()
。
JSX 的语法:
JSX 中的标签可以是单标签,也可以是双标签。如果是单标签的话,必须以
/>
结尾。
为了方便阅读,通常在 JSX 的外层包裹一个小括号。
注释:
JSX 中的注释需要写在花括号中。
const element = <h1>{/* 注释 */}Hello,React</h1>,
JSX 中只能有一个根元素:
JSX 中只能有一个根元素,可以在根元素里包含多个子元素。
// 错误
const element = (
<h1>Hello,React</h1>
<h1>Hi,JavaScript</h1>
)
//正确
const element = (
<div>
<h1>Hello,React</h1>
<h1>Hi,JavaScript</h1>
</div>
)
JSX 也是一个表达式:
JSX 也是一个表达式,在编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。也就是说,可以在 if 语句和 for 循环的代码块中使用 JSX、将 JSX 赋值给变量、把 JSX 当作参数传入以及从函数中返回 JSX。
function getGreeting(name) {
if (user) {
return <h1>Hello, {name}!</h1>
}
}
给 JSX 中的元素嵌入内容和绑定属性:
给 JSX 中的元素嵌入内容:
dangerouslySetInnerHTML 是 React 中 innerHTML 的替代品。可以通过 dangerouslySetInnerHTML 在 JSX 中实现换行。
<div dangerouslySetInnerHTML={{__html: '你好<br />世界'}}></div>
- 当嵌入的内容是变量:
- 当嵌入的内容是 String、Number、Array 类型的变量时,直接显示。
- 当嵌入的内容是 Boolean、Null、Undefined 类型的变量时,显示为空。
- 当嵌入的内容是 Object 类型的变量时,会报错,Object 类型的变量不能作为内容嵌入。
const types = ['React', 'Vue', 'Angular'] const element = ( <div> <h1>{types}</h1> </div> )
- 当嵌入的内容是表达式:在 JSX 语法中,可以在大括号内放置任何有效的 JavaScript 表达式。
在 JSX 中可以嵌入 JS 表达式,一个表达式会产生一个值。
但是不能嵌入 JS 语句:不能使用if else
和switch case
语句,可以使用三元运算符来代替;不能使用 for 循环语句,可以使用map()
等方法来代替。const age = 20 const persons = [ { id: 1, name: 'Lee', }, { id: 2, name: 'Mary', }, ] const getMessage = () => { return 'Hello,React' } const element = ( <div> <h1>{10 + 20}</h1> <h1>{age > 18 ? '成年人' : '未成年人'}</h1> <h1> {persons.map(item => (<div key={item.id}>{item.name}</div>))} </h1> <h1>{getMessage()}</h1> </div> )
给 JSX 中的元素绑定属性:
可以使用 JavaScript 表达式作为 JSX 中元素的属性值,需要包裹在 {}
中。
因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 属性名要使用小驼峰命名。例如:JSX 里的 class 变成了 className。
小驼峰:首字母小写,后面每个单词的首字母都大写。
大驼峰:首字母大写,后面每个单词的首字母也都大写。
const imgUrl = 'https://img1.baidu.com/it/u=914516326,2749589618&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto'
const element = <img src={imgUrl} />
样式:
- 使用内联样式:style 接收一个对象
style={{key: value, key: value}}
,属性名要使用小驼峰形式。// 静态内联样式 <div style={{color: 'red', fontSize: 12}}></div> // 动态内联样式 <div style={{color: `${this.state.isActive ? 'red' : ''}`, fontSize: 12}}></div>
- 使用 class 类选择器:
// 静态 class <div className="font12 colorRed"></div> // 动态 class <div className={this.state.isActive ? 'colorRed' : ''}></div> // 静态 class 和动态 class 同时使用 <div className={`font12 ${this.state.isActive ? 'colorRed' : ''}`}></div>
条件渲染和列表渲染:
条件渲染:
React 中可以使用 if 条件语句、三元运算符或者逻辑与运算符,根据不同的状态渲染不同的内容。
- 使用 if 条件语句。
const isLogin = true // 使用 if 条件语句,根据不同的条件给变量赋不同的值值 let element if (isLogin) { element = <h1>已登录</h1> } else { element = <h1>未登录</h1> }
- 使用三元运算符。
const isLogin = true const element = <div>{isLogin ? '已登录' : '未登录'}</div>
- 使用逻辑与运算符:如果条件成立,渲染某个内容;如果条件不成立,什么内容也不渲染。
如果第一个操作数不是 boolean 类型,而是数值 0 时,会在 UI 中显示 0。因此最好使用
!!
转化一下。const isLogin = true const element = isLogin && <div>已登录</div>
列表渲染:
React 中进行列表渲染最常见的方式就是使用数组的 map()
方法。
使用 map()
方法遍历得到的元素需要设置 key 属性。key 属性在兄弟节点之间应该是独一无二的,但是它们不需要全局唯一。
key 属性是虚拟 DOM 对象的标识,在更新显示时 key 起着极其重要的作用。当状态中的数据发生变化时, React 会根据新数据生成新的虚拟 DOM,随后 React 进行新虚拟 DOM 与旧虚拟 DOM 的 Diff 比较,比较规则如下:
- 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key:
- 若虚拟 DOM 中内容没变,直接使用之前的真实 DOM。
- 若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM。
- 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key:
- 根据数据创建新的真实 DOM,随后渲染到页面。
const persons =[
{id:1, name:'小张', age:18},
{id:2, name:'小李', age:19},
]
const element = (
<ul>
{
persons.map(item => <li key={item.id}>{item.name}:{item.age}</li>)
}
</ul>
)
用 index 作为 key 可能会引发的问题:
- 若对数据进行逆序添加、逆序删除等破坏顺序操作,会产生没有必要的真实 DOM 的更新。界面没有问题,但效率低。
- 如果结构中还包含输入类的 DOM,会产生错误 DOM 的更新。界面会有问题。
class App extends React.Component{
state = {
persons:[
{id:1, name:'小张', age:18},
{id:2, name:'小李', age:19},
]
}
add = ()=>{
const {persons} = this.state
const p = {id:persons.length+1, name:'小王', age:20}
this.setState({persons:[p,...persons]})
}
render(){
const {persons} = this.state
return (
<div>
<button onClick={this.add}>添加一个小王</button>
<ul>
{
persons.map((item, index)=> (
<li key={index}>
{item.name}:{item.age}
<input type="text"/>
</li>
))
}
</ul>
</div>
)
}
}
在小张和小李后面的 <input>
中输入各自的信息,然后点击添加一个小王,会出现下面的情况,信息串行了。
原因是:旧的虚拟 DOM 与新的虚拟 DOM 进行对比,在旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key 值 0 和 1,React 逐层对比时发现 <li>
的文字内容变了,但是 input
这个标签节点没有变。导致了两个问题:原本能复用的小张和小李的个人信息的真实 DOM 都需要重新生成,导致了没有必要的真实 DOM 的更新;而且小张和小李的 <input>
由于复用导致了内容的串行。
// 初始数据:
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
// 初始虚拟 DOM:
<li key=0>小张:18<input type="text"/></li>
<li key=1>小李:19<input type="text"/></li>
// 更新后的数据:
{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
// 更新后的虚拟 DOM:
<li key=0>小王:20<input type="text"/></li>
<li key=1>小张:18<input type="text"/>
</li><li key=2>小李:19<input type="text"/></li>
事件:
React 中事件的命名采用小驼峰式,通过 {}
传入一个事件处理函数,事件处理函数会在事件发生时被执行
class App extends React.Component {
handleBtnClick = () => {
console.log(this)
}
render() {
return <button onClick={this.handleBtnClick}>按钮</button>
}
}
事件处理函数中的 this:
如果直接给元素的事件绑定事件处理函数,那么事件处理函数中的 this 指向 undefined。
class App extends React.Component {
handleBtn2Click() {
console.log(this) // undefined
}
render() {
return (
<div>
<button id='btn1'>按钮1</button>
// 将事件处理函数赋值给了 DOM 元素的一个事件,就相当于是赋值给了一个变量。调用的时候是直接调用,this 指向全局对象,但由于类默认都是严格模式,因此 this 指向 undefined
<button onClick={this.handleBtn2Click}>按钮2</button>
</div>
)
}
}
可以通过以下方式来解决:
- 通过 bind 方法给事件处理函数手动绑定 this。
class App extends React.Component { constructor() { super() // 在构造方法中给事件处理函数手动绑定 this this.handleBtnClick = this.handleBtnClick.bind(this) } handleBtn1Click() { console.log(this) // 实例对象 } handleBtn2Click() { console.log(this) // 实例对象 } render() { return ( <div> <button onClick={this.handleBtn1Click}>按钮1</button> <button onClick={this.handleBtn2Click.bind(this)}>按钮2</button> </div> ) } }
- 将事件处理函数写成箭头函数:箭头函数没有自己的 this,会从上一层作用域中继承。
class App extends React.Component { // 将事件处理函数写成箭头函数,箭头函数没有自己的 this,会从上一层作用域中继承 handleBtnClick = () => { console.log(this) // 实例对象 } render() { return ( <div> <button onClick={this.handleBtnClick}>按钮</button> </div> ) } }
- 给事件绑定一个箭头函数,在箭头函数中调用事件处理函数。
这种语法问题在于每次渲染组件时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。
class App extends React.Component { handleBtnClick() { console.log(this) } render() { return ( // 事件需要绑定一个事件处理函数。因此可以给 onClick 绑定一个箭头函数,然后在箭头函数中调用真实的事件处理函数来处理逻辑。由于箭头函数中的 this 继承自上一作用域 render() 方法,因此 this 指向实例对象,在箭头函数中通过 this 调用 handleBtnClick,因此 handleBtnClick 中的 this 也就指向实例对象 <button onClick={() => {this.handleBtnClick()}}>按钮</button> ) } }
事件处理函数中的 event 对象:
事件发生时,事件处理函数默认会接收到一个事件对象 event 作为参数。在 React 中,React 在拿到原生 event 对象之后会对其进行包装。
class App extends React.Component {
handleBtnClick = e => {
console.log(e)
}
render() {
return (
<button onClick={this.handleBtnClick}>按钮</button>
)
}
}
给事件处理函数传递参数:
- 通过 bind 的方式。
class App extends React.Component { // 默认会获取到事件对象 event,放置在最后 handleBtnClick = (id, e) => { console.log(id) console.log(e) } render() { return ( <button onClick={this.handleBtnClick.bind(this, 1)}>按钮</button> ) } }
- 通过箭头函数的方式。
class App extends React.Component { handleBtnClick = (e, id) => { console.log(id) console.log(e) } render() { return ( // 事件对象 event 必须显式地进行传递,否则将无法获取到 event 对象。因为在 onClick 绑定的箭头函数中是默认可以获取到 event 对象的,如果此时不显示地将 event 再传递给真正的事件处理函数,那么在真正的事件处理中就无法获取到 event 对象了。 <button onClick={(e) => {this.handleBtnClick(e, 1)}}>按钮</button> ) } }