import{_ as s,o as n,c as a,a as l}from"./app.ab8d0b9f.js";const p="/assets/react-life-cycle.67e890c0.png",b=JSON.parse('{"title":"React","description":"","frontmatter":{},"headers":[{"level":2,"title":"邂逅React","slug":"邂逅react","link":"#邂逅react","children":[{"level":3,"title":"React开发依赖","slug":"react开发依赖","link":"#react开发依赖","children":[]},{"level":3,"title":"Babel与React的关系","slug":"babel与react的关系","link":"#babel与react的关系","children":[]},{"level":3,"title":"React初体验","slug":"react初体验","link":"#react初体验","children":[]},{"level":3,"title":"第一个React程序","slug":"第一个react程序","link":"#第一个react程序","children":[]},{"level":3,"title":"组件化开发","slug":"组件化开发","link":"#组件化开发","children":[]},{"level":3,"title":"提前绑定this","slug":"提前绑定this","link":"#提前绑定this","children":[]},{"level":3,"title":"列表渲染","slug":"列表渲染","link":"#列表渲染","children":[]},{"level":3,"title":"计数器案例","slug":"计数器案例","link":"#计数器案例","children":[]}]},{"level":2,"title":"认识JSX语法","slug":"认识jsx语法","link":"#认识jsx语法","children":[{"level":3,"title":"JSX的使用","slug":"jsx的使用","link":"#jsx的使用","children":[]},{"level":3,"title":"JSX事件绑定","slug":"jsx事件绑定","link":"#jsx事件绑定","children":[]},{"level":3,"title":"事件绑定参数传递","slug":"事件绑定参数传递","link":"#事件绑定参数传递","children":[]},{"level":3,"title":"JSX事件绑定案例","slug":"jsx事件绑定案例","link":"#jsx事件绑定案例","children":[]},{"level":3,"title":"条件渲染","slug":"条件渲染","link":"#条件渲染","children":[]},{"level":3,"title":"列表渲染中的高阶函数","slug":"列表渲染中的高阶函数","link":"#列表渲染中的高阶函数","children":[]},{"level":3,"title":"JSX的本质","slug":"jsx的本质","link":"#jsx的本质","children":[]},{"level":3,"title":"声明式编程","slug":"声明式编程","link":"#声明式编程","children":[]},{"level":3,"title":"购物车案例","slug":"购物车案例","link":"#购物车案例","children":[]}]},{"level":2,"title":"React项目开发","slug":"react项目开发","link":"#react项目开发","children":[{"level":3,"title":"React脚手架","slug":"react脚手架","link":"#react脚手架","children":[]},{"level":3,"title":"React组件化开发","slug":"react组件化开发","link":"#react组件化开发","children":[]},{"level":3,"title":"类组件","slug":"类组件-1","link":"#类组件-1","children":[]},{"level":3,"title":"函数组件","slug":"函数组件","link":"#函数组件","children":[]},{"level":3,"title":"组件的生命周期","slug":"组件的生命周期","link":"#组件的生命周期","children":[]}]}],"relativePath":"note/React.md","lastUpdated":1681706359000}'),o={name:"note/React.md"},e=l(`
react
包含React的核心代码react-dom
将React渲染到不同平台需要的核心代码babel
将JSX转换成React代码的工具为什么要拆分成这么多的包?
react
包中包含了 React Web 和 React Native 共同拥有的核心代码react-dom
针对Web和Native完成的事情不同 react-dom
会将JSX渲染成真实DOM,展示在浏览器中react-dom
会将JSX渲染成原生的控件(如Android中的Button,iOS中的UIButton)Babel是什么?
二者之间的联系
我们通过CDN方式引入react、react-dom、babel这三个依赖
并且创建#root
根节点,作为渲染React组件的容器,再新建一个script标签,键入以下内容
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js" crossorigin></script>
<script type="text/babel">
ReactDOM.render(<div>Hello, React!</div>, document.querySelector('#root'))
</script>
这时,一个内容为Hello, React!
的div标签就被渲染到页面上了
需要注意的是:ReactDOM.render
这种写法适用于React18之前,在React18之后建议用下面的代码渲染根节点:
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<h1>Hello, React!</h1>)
设想我们现在有这样一个需求:点击按钮使文本Hello, World!
变为Hello, React!
我们很容易就能写出如下代码:
const root = ReactDOM.createRoot(document.querySelector('#root'))
let msg = 'Hello, World!'
render() // initial render
function handleChangeClick() {
msg = 'Hello, React!'
}
root.render(
<div>
<h1>{msg}</h1>
<button onClick={handleChangeClick}>Change Text</button>
</div>
)
在Vue中,如果我们对数据进行了修改,Vue的数据响应式会自动帮我们完成视图的更新
然而在React中,当我们修改了数据需要通知React,让React重新渲染视图。在这里,我们可以把渲染的过程封装为一个函数,方便我们重复调用,触发重新渲染
const root = ReactDOM.createRoot(document.querySelector('#root'))
let msg = 'Hello, World!'
render() // initial render
function handleChangeClick() {
msg = 'Hello, React!'
render() // re-render
}
function render() {
root.render(
<div>
<h1>{msg}</h1>
<button onClick={handleChangeClick}>Change Text</button>
</div>
)
}
这个案例中,我们使用{}
语法,将动态的JS语法嵌入到JSX代码中
React有两种组件:类组件与函数组件,React18+推荐使用函数组件+Hooks
我们使用类组件来逐步重构上面的案例:
class App extends React.Component {
constructor() {
super()
this.state = {
msg: 'Hello, World!'
}
}
render() {
return <h2>{this.state.msg}</h2>
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
类组件必须实现render方法,render方法返回值为后续React渲染到页面的内容
组件内数据分为两类
需要触发视图重新渲染的数据,我们将其成为:参与数据流
state
属性中this.state = { name: 'Ziu' }
来定义状态this.setState
来更新数据,通知React执行视图更新:::success 需要注意的是,在constructor中我们调用了super
,因为App类是继承自React.Component类,调用super
即调用了其父类的构造函数,让我们的App组件可以继承一些内置属性/方法如state setState render
:::
至此我们完成了数据的迁移,下面我们来完成事件函数的迁移。
有了之前的介绍,我们很容易的可以写出下面的代码:
class App extends React.Component {
constructor() {
super()
this.state = {
msg: 'Hello, World!'
}
}
changeText() {
this.setState({
msg: 'Hello, React!'
})
}
render() {
return (
<div>
<h2>{this.state.msg}</h2>
<button onClick={this.changeText}>Change Text</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
我们可以写一个实例方法changeText来修改msg,然而,此时我们点击按钮后发现,案例不能正常工作。
如果在changeText中打log,会发现函数被正常触发了,但是状态没有更新
为什么this.setState失效了?这和this的绑定有关:绑定的changeText
在被调用时,向上找this
找到的是全局的this
即undefined
这种情况有点类似于下面的例子:
const app = new App()
app.changeText() // this => app
const func = app.changeText
func() // this => undefined
在非严格模式下,直接调用func时的this指向的是window,严格模式下则为undefined
为了解决this绑定的问题,我们需要显式把函数调用绑定给当前组件,这时组件就可以正常工作了。
class App extends React.Component {
constructor() {
super()
this.state = {
msg: 'Hello, World!'
}
}
changeText() {
this.setState({
msg: 'Hello, React!'
})
}
render() {
return (
<div>
<h2>{this.state.msg}</h2>
<button onClick={this.changeText.bind(this)}>Change Text</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
在render函数中频繁通过.bind
毕竟不太优雅,好在也有另一种方式:可以在constructor中提前对实例方法进行this绑定:
...
constructor() {
super()
this.state = {
msg: 'Hello, World!'
}
this.changeText = this.changeText.bind(this)
}
render() {
...
<button onClick={this.changeText}>Change Text</button>
...
}
...
可以通过循环,将数组渲染到视图中
class App extends React.Component {
constructor() {
super()
this.state = {
movieList: [
'The Shawshank Redemption',
'The Godfather',
'The Godfather: Part II',
'The Dark Knight'
]
}
}
render() {
return (
<ul>
{this.state.movieList.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
需要注意的是,这里绑定的key的功能类似于Vue中的特殊属性key,它用来帮助React对列表渲染进行更高效的更新。
结合之前的知识,可以实现一个简单的计数器
class App extends React.Component {
constructor() {
super()
this.state = {
count: 0
}
this.addCount = this.addCount.bind(this)
this.subCount = this.subCount.bind(this)
}
addCount() {
this.setState({
count: this.state.count + 1
})
}
subCount() {
this.setState({
count: this.state.count - 1
})
}
render() {
const { count } = this.state
return (
<div>
<h1>Count: {count}</h1>
<button onClick={this.addCount}>Add</button>
<button onClick={this.subCount}>Sub</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
是因为我们给script标签添加了type="text/babel"
属性,浏览器不会对这个script进行解析,当babel被加载完成后,babel会在页面中寻找type="text/babel"
的script标签进行转义,转义后的代码才会被浏览器执行
()
方便阅读在JSX中编写注释,需要以{/* ... */}
的形式,在.jsx/.tsx
文件中,通过快捷键就可以快捷的生成注释内容
本质上是通过花括号语法{}
嵌入了一段JavaScript表达式,在表达式中书写注释
...
return (
<div>
{/* Some Comment... */}
<h1>Count: {count}</h1>
<button onClick={this.addCount}>Add</button>
<button onClick={this.subCount}>Sub</button>
</div>
)
...
可以通过花括号语法将变量内容嵌入到JSX语法中:
const message = 'Hello, React!'
const arr = ['abc', 'cba', 'nba']
return (
<div>
<h1>{ message }</h1>
<div>{ arr }</div>
</div>
)
.toString()
方法将其转为字符串下例中,只有number类型会被正常展示,而其余变量则不会展示在视图中
render() {
const number = 123
const n = null
const u = undefined
const b = true
return (
<div>
<div>
Number: {number}
</div>
<div>
Null: {n}
</div>
<div>
Undefined: {u}
</div>
<div>
Boolean: {b}
</div>
</div>
)
}
将对象类型变量嵌入到JSX语法中,React会抛出错误:
...
render() {
const obj = { name: 'Ziu' }
return (
<div>
{ obj }
</div>
)
}
...
v-bind
绑定属性title
src
href
class
内联style
等下例中,我们通过花括号语法对元素的属性进行了动态绑定,点击按钮可以切换className状态
同时,动态绑定的内联样式也会发生改变,通过花括号语法动态绑定style属性
class App extends React.Component {
constructor() {
super()
this.state = {
isActive: false,
title: 'Description'
}
this.changeActive = this.changeActive.bind(this)
}
changeActive() {
this.setState({
isActive: !this.state.isActive
})
}
render() {
const { isActive, title } = this.state
const classList = ['title', isActive ? 'active' : '']
return (
<div>
<div
className={classList.join(' ')}
title={title}
style={{ color: isActive ? 'red' : 'blue' }}
>
Hello, React!
</div>
<button onClick={this.changeActive}>Change Active</button>
</div>
)
}
}
当我们通过脚手架创建项目时,可以使用第三方库来帮我们完成className的绑定
classnames
库 pnpm add classnames
先前的例子中,我们已经通过onClick
给按钮绑定过事件处理函数了,其中涉及了this绑定
回顾一下this的四种绑定规则:
除了之前通过function + bind
绑定事件处理函数的方式,还可以通过箭头函数来帮我们完成处理
箭头函数的内部使用this时会自动向上层作用域查找this 实际开发中这种方式并不常用
...
changeActive = () => {
this.setState({
isActive: !this.state.isActive
})
}
...
相比之下更推荐使用的,是下面这种方式:
...
<button onClick={() => this.changeActive()}>Change Active</button>
...
这样书写有几种好处:
它的原理是,我们对外暴露的本质上是一个箭头函数,当调用箭头函数时,本质上是执行this.changeActive
,这是 一种隐式绑定,找到的this为当前组件实例
事件回调函数的第一个默认参数就是Event对象,这个Event对象是经过React包装后的,但是原生的属性都包含在内,React对其进行了一些扩展
...
changeActive(ev) {
console.log('Event', ev)
}
render() {
return (
<div>
{/* event将作为默认入参传递给changeActive */}
<button onClick={this.changeActive}>Change Active</button>
{/* 通过箭头函数绑定事件监听回调函数时 需要手动透传一下event */}
<button onClick={(ev) => this.changeActive(ev)}>Change Active</button>
</div>
)
}
...
当我们需要传递额外的参数时,通过箭头函数传递也更容易:
changeActive(ev, name, age) {
console.log('Event', ev)
console.log('Name', name)
console.log('Age', age)
}
render() {
return (
<div>
{/* NOT Recommand */}
<button onClick={this.changeActive.bind(this, 'Ziu', 18)}>Change Active</button>
{/* Recommand */}
<button onClick={(ev) => this.changeActive(ev, 'Ziu', 18)}>Change Active</button>
</div>
)
}
需要注意,当通过.bind
传递额外参数时,最后一个参数才是默认传递的Event对象,这会导致非预期行为
> Event 'Ziu'
> Name 18
> Age {Event}
创建一个Tab栏,选中哪个选项,哪个选项被激活切换为红色,同一时间仅有一个激活项目
结合之前学习的内容,很容易就可以写出下述 代码:
class App extends React.Component {
constructor() {
super()
this.state = {
activeIndex: 0,
tabList: ['Home', 'Recommend', 'Hot', 'User']
}
}
changeActive(index) {
this.setState({
activeIndex: index
})
}
render() {
const { activeIndex, tabList } = this.state
return (
<div>
<div className="tabs">
{tabList.map((item, index) => (
<button
className={'tab ' + index === activeIndex ? 'active' : ''}
style={{
color: index === activeIndex ? 'red' : 'black'
}}
key={index}
onClick={() => this.changeActive(index)}
>
{item}
</button>
))}
</div>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
控制元素按照某种条件渲染,以加载状态为例
列表未加载出来时,展示加载中
,加载完毕则渲染完整内容:
class App extends React.Component {
constructor() {
super()
this.state = {
isLoading: true
}
}
changeLoading() {
this.setState({
isLoading: !this.state.isLoading
})
}
render() {
const { isLoading } = this.state
return (
<div>
{isLoading ? (
<div className="loading"> Loading ... </div>
) : (
<div className="list">Some Content</div>
)}
<button onClick={() => this.changeLoading()}>Toggle Loading</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
常用的条件渲染方式
if/else/else-if
?:
&&
user?.info?.name
下例中通过逻辑与运算符&&
决定VIP
标签是否展示在视图中
class App extends React.Component {
constructor() {
super()
this.state = {
isVip: false
}
}
changeVip() {
this.setState({
isVip: !this.state.isVip
})
}
render() {
const { isVip } = this.state
return (
<div>
<div class="user">
<span>username: Ziu</span>
{isVip && <span className="vip-banner"> VIP </span>}
</div>
<button onClick={() => this.changeVip()}>Toggle Vip</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
v-show
是Vue提供的语法糖,不同于v-if
,它只切换元素的display
属性。
下面我们在React中简单复现一个v-show
的效果:
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>
<div style={{ display: isShow ? '' : 'none' }}>Target Element</div>
<button onClick={() => this.changeShow()}>Toggle Show</button>
</div>
)
}
}
实际使用中,将其封装为hooks来调用更具通用性,也更方便管理
filter
函数 过滤器slice
函数 分页sorc
函数 排序class App extends React.Component {
constructor() {
super()
this.state = {
stuList: [
{ name: 'Ziu', age: 18, score: 88 },
{ name: 'Kobe', age: 19, score: 59 },
{ name: 'Why', age: 20, score: 61 },
{ name: 'James', age: 21, score: 99 }
]
}
}
render() {
const { stuList } = this.state
// 及格的学生
const passStuList = stuList.filter((item) => item.score >= 60)
// 分数最高的两个学生
const top2StuList = stuList.sort((a, b) => b.score - a.score).slice(0, 2)
return (
<div>
<div className="list">
{stuList.map(({ name, age, score }) => (
<div className="item" key={name}>
<span className="name">{name}</span>
<span className="age">{age}</span>
<span className="score">{score}</span>
</div>
))}
</div>
</div>
)
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
假设我们有下面的JSX代码:
class App extends React.Component {
constructor() {
super()
}
render() {
const page = (
<div className="page">
<div className="header">Header</div>
<div className="content">
Content
<div className="banner">Banner</div>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
</div>
<div className="footer">Footer</div>
</div>
)
console.log(page)
return <div>{page}</div>
}
}
通过JSX语法描述出来的template会经过Babel转化,转化为JavaScript树的数据结构
在控制台中我们可以看到,子节点都存放进了父节点的props.children
中
JSX仅仅是React.createElement(component, props, ...children)
的语法糖
所有的JSX语法都会被Babel转化为这样的命令式语法
.createElement函数的参数
"div"
className
=> class
我们借助Babel官网的Playground来检查一下JSX语法的转化
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
const page = /*#__PURE__*/_jsxs("div", {
className: "page",
children: [/*#__PURE__*/_jsx("div", {
className: "header",
children: "Header"
}), /*#__PURE__*/_jsxs("div", {
className: "content",
children: ["Content", /*#__PURE__*/_jsx("div", {
className: "banner",
children: "Banner"
}), /*#__PURE__*/_jsxs("ul", {
children: [/*#__PURE__*/_jsx("li", {
children: "Item 1"
}), /*#__PURE__*/_jsx("li", {
children: "Item 2"
}), /*#__PURE__*/_jsx("li", {
children: "Item 3"
}), /*#__PURE__*/_jsx("li", {
children: "Item 4"
}), /*#__PURE__*/_jsx("li", {
children: "Item 5"
})]
})]
}), /*#__PURE__*/_jsx("div", {
className: "footer",
children: "Footer"
})]
});
console.log(page);
这时经过Babel转义后的纯JS函数,这段函数可以在浏览器中直接运行
如果移除了相关JSX代码,并将他们都替换为React.createElement
函数调用,那么得到的代码也可以直接在浏览器中运行。Babel帮助我们完成了转化,提高了开发效率,相比于通过调用React.createElement
来描述视图,通过JSX编写的代码更加容易维护
这些代码最终形成的就是虚拟DOM树,React可以将虚拟DOM渲染到页面上,形成真实DOM
虚拟DOM允许React可以通过diff算法,高效地对真实DOM树进行更新
下面写一个经典的购物车案例
function formatPrice(price) {
return \`$ \${price.toFixed(2)}\`
}
class App extends React.Component {
constructor() {
super()
this.state = {
books: [
{ name: 'book1', author: 'author1', price: 100, count: 0 },
{ name: 'book2', author: 'author2', price: 200, count: 0 },
{ name: 'book3', author: 'author3', price: 300, count: 0 },
{ name: 'book4', author: 'author4', price: 400, count: 0 }
]
}
}
changeCount(index, count) {
this.setState((state) => {
const books = [...state.books]
books[index].count += count
return { books }
})
}
removeItem(index) {
this.setState((state) => {
const books = [...state.books]
books.splice(index, 1)
return { books }
})
}
getTotal() {
const { books } = this.state
return books.reduce((acc, { price, count }) => acc + price * count, 0)
}
renderBookCart() {
const { books } = this.state
const total = this.getTotal()
return (
<div className="shopping-cart">
<h1>Shopping Cart</h1>
<div className="books">
{books.map(({ name, author, price, count }, index) => (
<div className="book" key={name}>
<span className="idx">{index + 1}</span>
<span className="name">{name}</span>
<span className="author">{author}</span>
<span className="price">{formatPrice(price)}</span>
<span className="counter">
<button onClick={() => this.changeCount(index, -1)} disabled={count <= 0}>
-
</button>
<span className="counter-number">{count}</span>
<button onClick={() => this.changeCount(index, 1)}>+</button>
</span>
<button onClick={() => this.removeItem(index)}>Delete</button>
</div>
))}
</div>
<div className="total">
<span>Total: {formatPrice(total)}</span>
</div>
</div>
)
}
renderEmptyTip() {
return <div className="empty">Shopping Cart is Empty</div>
}
render() {
const isEmpty = this.state.books.length === 0
return !isEmpty ? this.renderBookCart() : this.renderEmptyTip()
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
类似于Vue提供的 pnpm create vite
创建一个模板,React也可以通过 create-react-app
来初始化一个空的React模板
pnpm add create-react-app -g # 全局安装create-react-app
create-react-app react-app # 创建一个名为react-app的React项目
# 删除node_modules package-lock.json
cd react-app
pnpm i # 使用pnpm重新安装依赖
// index.js
import ReactDOM from 'react-dom/client'
import App from './App'
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
// App.jsx
import { Component } from 'react'
export default class App extends Component {
render() {
return <div>Hello, React!</div>
}
}
组件化是React的核心思想之一,组件化是一个抽象的过程,将大的应用程序抽象为多个小的组件,最终形成组件树
分而治之,让代码更容易组织和管理
React组件相对于Vue更加灵活多样,按照不同的方式可以分为多种组件
除此之外,还有异步组件、高阶组件等...
this.props
或this.state
发生变化时被调用被调用时组件内会检查this.props
和this.state
是否发生变化,并且返回下面的返回值之一:
<div />
会被React渲染为DOM节点,而<SomeComponent />
会被React渲染为自定义组件<div />
还是<SomeComponent />
,他们都为React元素函数组件不需要继承自任何父类,函数的返回值相当于render函数的返回值,表示组件要渲染的内容
修改前文中编写的App.jsx
即可:
// App.jsx
export default function App() {
return <div>Hello, React!</div>
}
我们需要在组件的不同生命周期中执行不同的操作,比如添加解除监听器、发起网络请求等
componentDidMount
组件挂载后componentDidUpdate
组件更新后componentWillUnmount
组件卸载前