docs: React Note update

This commit is contained in:
ZiuChen 2023-04-16 21:49:48 +08:00
parent f6a0906d06
commit 2f9587c845

View File

@ -341,4 +341,695 @@ root.render(<App />)
- All in JS
- 不同于Vue的模板语法 不需要专门学习模板语法中的指令(v-for/v-if/v-bind)
### JSX的使用
#### 书写JSX的规范与注意事项
- JSX的顶层只能有一个根元素 元素必须包裹在单独的闭合标签中
- 后续会接触到Fragment标签 Vue3也是将元素包裹在了Fragments标签中
- 为了方便阅读 通常在JSX外层包裹一个小括号`()`方便阅读
#### JSX的注释
在JSX中编写注释需要以`{/* ... */}`的形式,在`.jsx/.tsx`文件中,通过快捷键就可以快捷的生成注释内容
本质上是通过花括号语法`{}`嵌入了一段JavaScript表达式在表达式中书写注释
```tsx{4}
...
return (
<div>
{/* Some Comment... */}
<h1>Count: {count}</h1>
<button onClick={this.addCount}>Add</button>
<button onClick={this.subCount}>Sub</button>
</div>
)
...
```
#### JSX嵌入变量作为子元素
可以通过花括号语法将变量内容嵌入到JSX语法中
```tsx
const message = 'Hello, React!'
const arr = ['abc', 'cba', 'nba']
return (
<div>
<h1>{ message }</h1>
<div>{ arr }</div>
</div>
)
```
- 变量类型为number string array类型时可以直接展示
- 变量类型为null undefined boolean类型时内容为空
- 如果希望可以展示null/undefined/boolean类型需要通过`.toString()`方法将其转为字符串
- 空字符串拼接、String构造函数等方式
- Object对象类型不能作为子元素 (Objects are not valid as a React child)
下例中只有number类型会被正常展示而其余变量则不会展示在视图中
```tsx
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会抛出错误
```tsx {6}
...
render() {
const obj = { name: 'Ziu' }
return (
<div>
{ obj }
</div>
)
}
...
```
#### JSX的属性绑定
- 在Vue中我们通过`v-bind`绑定属性
- 在React中如何绑定元素属性
- `title` `src` `href` `class` 内联`style`
下例中我们通过花括号语法对元素的属性进行了动态绑定点击按钮可以切换className状态
同时动态绑定的内联样式也会发生改变通过花括号语法动态绑定style属性
```tsx
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`
- 提供了多种创建className的语法
### JSX事件绑定
先前的例子中,我们已经通过`onClick`给按钮绑定过事件处理函数了其中涉及了this绑定
回顾一下this的四种绑定规则
1. 默认绑定 独立执行 foo() this => undefined
2. 隐式绑定 被一个对象执行 obj.foo() this => obj
3. 显式绑定 call/bind/apply foo.call('aaa') this => String('aaa')
4. new绑定 new Foo() 创建一个新对象并且赋值给this
除了之前通过`function + bind`绑定事件处理函数的方式,还可以通过箭头函数来帮我们完成处理
箭头函数的内部使用this时会自动向上层作用域查找this 实际开发中这种方式并不常用
```tsx {2}
...
changeActive = () => {
this.setState({
isActive: !this.state.isActive
})
}
...
```
相比之下更推荐使用的,是下面这种方式:
```tsx {2}
...
<button onClick={() => this.changeActive()}>Change Active</button>
...
```
这样书写有几种好处:
- 给事件处理函数传递参数更方便
- 书写更方便 不必主动考虑this绑定问题
它的原理是,我们对外暴露的本质上是一个箭头函数,当调用箭头函数时,本质上是执行`this.changeActive`,这是 一种隐式绑定找到的this为当前组件实例
### 事件绑定参数传递
- Event参数传递
- 额外参数传递
事件回调函数的第一个默认参数就是Event对象这个Event对象是经过React包装后的但是原生的属性都包含在内React对其进行了一些扩展
```tsx {13}
...
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>
)
}
...
```
当我们需要传递额外的参数时,通过箭头函数传递也更容易:
```tsx {13}
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对象这会导致非预期行为
```sh
> Event 'Ziu'
> Name 18
> Age {Event}
```
### JSX事件绑定案例
创建一个Tab栏选中哪个选项哪个选项被激活切换为红色同一时间仅有一个激活项目
结合之前学习的内容,很容易就可以写出下述 代码:
```tsx
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 />)
```
### 条件渲染
控制元素按照某种条件渲染,以加载状态为例
列表未加载出来时,展示`加载中`,加载完毕则渲染完整内容:
```tsx
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`标签是否展示在视图中
```tsx
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 />)
```
#### 在React中简单写一个"v-show"
`v-show`是Vue提供的语法糖不同于`v-if`,它只切换元素的`display`属性。
下面我们在React中简单复现一个`v-show`的效果:
```tsx
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`函数 排序
- ...
```tsx
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的本质
#### JSX的转换过程
假设我们有下面的JSX代码
```tsx
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`
#### 虚拟DOM树
JSX仅仅是`React.createElement(component, props, ...children)`的语法糖
所有的JSX语法都会被Babel转化为这样的命令式语法
.createElement函数的参数
- type
- 当前ReactElement的类型
- 如果是标签元素,值为字符串如:`"div"`
- 如果是组件元素,那么值为组件的名称
- config
- 所有JSX中绑定的属性都在config中以键值对的形式存储
- 例如`className` => `class`
我们借助Babel官网的Playground来检查一下JSX语法的转化
```js
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树进行更新
### 声明式编程
- 虚拟DOM帮我们从命令式编程转到了声明式编程的模式
- 对虚拟DOM作何处理如何渲染是由React决定的由于做了一层抽象那么同样可以将虚拟DOM渲染成原生组件React Native
### 购物车案例
下面写一个经典的购物车案例
```tsx
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 />)
```
## React脚手架
- 认识脚手架工具
- create-react-app
- 创建React项目
- Webpack的配置
类似于Vue提供的 `pnpm create vite` 创建一个模板React也可以通过 `create-react-app` 来初始化一个空的React模板
```sh
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重新安装依赖
```
```tsx
// index.js
import ReactDOM from 'react-dom/client'
import App from './App'
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
// App.js
import React from 'react'
export default class App extends React.Component {
render() {
return <div>Hello, React!</div>
}
}
```