学习 React 笔记
react 17 的一些变化
- 支持渐进式升级,逐步升级
- 升级路由系统、在升级弹窗系统、一个模块一个模块的升级
- 事件委托机制改变
- 17 版本之前会给dom添加 addEventListener() 事件处理
- 17版本后,添加到虚拟 dom 的容器中,方便新旧版本交替
- 向浏览器原生事件靠拢
- onScroll、onFocus、onBlur(失焦)
- 删除事件池
- useEffect 清理操作改为异步操作
- 过去版本清除操作是同步的,会拖慢UI的渲染效率
- 清理中会释放资源,不会产生UI阻塞的问题了
- jsx render 函数不能返回 undefined了
- 删除了一些私有 API,大部分是报漏给 RN 使用的,对前端页面开发没有影响
State
初始化
- 生命周期第一阶段:初始化
- 构建函数 constructor 是唯一可以初始化 state 的地方
class ShoppingCart extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isOpen: false
}
}
}
- state 只能通过 setState 修改状态
State 的更新是异步的
- 调用 setState后,state不会立刻改变,是异步操作,
react 会优化操作,多个修改会合并成一次
- 不能依赖当前的 State,计算下一个 state
Props
props 就是传入函数的参数,是传入组件内部的数据。
由于 react 是单线数据流的,可以理解成从父组件传递向自组件的数据
Immutable 直读属性
- 所有的 props 都是 直读的(Immutable)
- props 对象一旦创建就不可改变,只能通过销毁、重建来改变数据
- 通过判断内存地址是否一致,来确定对象是否有修改过(提高效率)
- 同思想的库:Redux、RxJS
Event 事件处理机制
Css 样式使用方式
1、直接引入整个 css 文件:
import './index.css';
<div className="app">< /div>
容易造成全区污染
- css in js 模块化引入组件
import style from './index.css';
<div className={styles.app}></div>
标签的 class 名称是动态生产的,可能对调试造成麻烦
setState 更新
- 调用 setState后,state不会立刻改变,是异步操作,
react 会优化操作,多个修改会合并成一次
- 不能依赖当前的 State,计算下一个 state
1、setState 同步执行,异步更新视图
dom 视图上会显示 count = 1, 视图会异步更新
// state 伪代码
this.state.count = 0;
this.setState({ count: this.state.count + 1 });
console.log('count:', this.state.count); // this.state.count = 0
// dom 视图上会显示 count = 1, 视图会异步更新
2、setState 第二个参数,异步回调
使用第二个参数,可以获取更新后的数据
// state 伪代码
this.state.count = 0;
this.setState({ count: this.state.count + 1 }, () => {
console.log('count:', this.state.count) // this.state.count = 1
})
3、多个 setState
由于 setState是异步操作的,如果使用多个 this.state,也是获取的同步数据,也就是上一个生命周期state,并没有更新
下面 2 个 setState 执行,最后也是以增量为 1 添加的
视图页面的只会增量加 1
// state 伪代码
this.state.count = 0;
// 业务 伪代码
<button onClick={ () => {
this.setState({ count: this.state.count + 1 }, () => {
console.log('count:', this.state.count) // this.state.count = 1
})
this.setState({ count: this.state.count + 1 }, () => {
console.log('count:', this.state.count) // this.state.count = 1
})
} }></button>
解决方法:第一个参数接受一个函数,获取上一个生命周期的 state
第一个参数可以获取上一个生命周期的 state 和 Props:preState
, preProps
每次点击执行 2 个 setState,最后的结果 this.state.count 每次会 + 2
// state 伪代码
this.state.count = 0;
// 每点击一次 this.state.count + 2,拿到的结果会是 2 / 4 / 6
<button
onClick={ () => {
// 第一个参数可以获取上一个生命周期的 state 和 Props
this.setState((preState, preProps) => {
return { count: preState.count + 1 }
}, () => {
console.log('count:', this.state.count) // this.state.count = 2 / 4 / 6
})
this.setState((preState, preProps) => {
return { count: preState.count + 1 }
}, () => {
console.log('count:', this.state.count) // this.state.count = 2 / 4 / 6
})
} }></button>
组件生命周期
- Mounting:组件第一次绘制,创建虚拟 DOM,渲染UI,完成组件的初始化
- Updating:更新虚拟 DMO,重新渲染UI,处理用户的交互,收集监听事件
- Unmounting:删除虚拟 DOM,移除UI,
初始化 Mounting
- 创建构造函数,初始化 state
class App extends React {
constructor(props) {
super(props)
this.state = {
name: ''
}
}
}
- 调用
getDerivedStateFromProps
- 检查 state 和 props 是否有变化
- render(): 渲染 UI
componentDidMount
- 在组件创建好 dom 元素以后,挂载进页面的时候调用,只会调用一次
- 一般请求接口在这里面调用
更新阶段 Updating
- 调用
getDerivedStateFromProps
- 检查 state 和 props 是否有变化
shouldComponentUpdate
判断组件是否需要更新,函数返回一个 布尔值jsshouldComponentUpdate(nextProps, nextState, nextContext) { return nextState.name !== this.state.name }
- render: 渲染UI (如果需要更新)
componentDidUpdate
- 组件更新后调用,UI重新渲染整个函数会被调用,处理组件更新以后的逻辑
销毁阶段 Unmounting
componentWillUnmount
组件销毁后调用- 这个函数可以回收各种监听以及事件,用来防止可能存在内存泄漏
Hooks 钩子
- 消息处的一种理方法,用来监听指定程序
- 函数组件需要处理副作用,可以用钩子把外部代码'钩'进来
- 常用的钩子:useState、useEffect、useContext、useReducer
- hooks 一律使用 use 前缀命名:useXxx
useEffect 副作用
不管什么情况,初始化阶段都会调用
- 如果第二参数没有,每次任意 state 状态发生改变都会调用,初始化会调用
- 注意使用场景,不要这没有参数的情况下修改任意 state 有可能出现循环回调,系统卡死
import React, { useState, useEffect } from 'react'
const [count, setCount] = useState(0)
const [robotGallery, setRobotGallery] = useState([])
useEffect(() => {
console.log('useEffect11111')
}) // 模拟 componentDidUpdate
- 第二参数如果是空数组[], 只会执行一次,类似于
componentDidMount
- 一般用来请求接口
import React, { useState, useEffect } from 'react'
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then((data) => setRobotGallery(data))
console.log('useEffect2222')
}, []) // 空数组相当于 componentDidMount
- 第二个参数接收数组,值为 state,会监听任意 state 的变化,然后进行会回调执行(类似于 vue 的 watche)
import React, { useState, useEffect } from 'react'
const [count, setCount] = useState<number>(0)
const [robotGallery, setRobotGallery] = useState<any[]>([])
useEffect(() => {
document.title = '点击' + count
console.log('useEffect3333')
}, [count])
useEffect 处理 异步
useEffect 只支持返回一个函数,不能使用 async,async 返回一个 promise
解决方法: 在声明一个 异步函数 fetchData
useEffect(() => {
const fetchData = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const data = await res.json()
setRobotGallery(data)
}
fetchData()
}, [])
自定义 hooks
自定义 hooks 一般是需要依赖状态,或者修改状态,又或者使用其他的 hooks。
如果上面不符合,就直接用一个函数就行
useMount 默认挂载一次
之前:
import { useEffect } from 'react'
useEffect(() => {
// 初始化挂载逻辑...
}, []) // componentDidMount
封装后:
import { useEffect } from 'react'
// 默认挂载一次
export const useMount = cb => {
useEffect(() => {
cb()
}, [])
}
useMount(() => {
function fetchData() {
// ...
}
})
useDebounce (利用 useEffect 实现防抖)
import { useEffect, useState } from 'react'
export const useDebounce = (value, delay) => {
// 定义内部变量
const [debounceValue, setDebounceValue] = useState(value)
useEffect(() => {
// 每次在 value 或 delay 变化后,设置一个新的定时器
const timeout = setTimeout(() => setDebounceValue(value), delay)
// 每次在上一个 useEffect 处理完后以后在运行
// 第二 useEffect 清理 第一setTimeout,一次类推,最后一个没有清理
return () => clearTimeout(timeout)
}, [value, delay])
return debounceValue
}
useContext 全局数据传递
声明
- 通过
React.createContext
声明并导出,可以声明函数或对象 - 添加
<xxxxx.Provider>
关键标签value
属性传递
// AppState.jsx 文件
const defaultContextValue = { username: '德玛西亚' }
export const appContext = React.createContext(defaultContextValue)
export const appSetStateContext = React.createContext(undefined)
export const AppStateProvider = (props) => {
const [state, setState] = useState(defaultContextValue)
return (
<appContext.Provider value={state}>
<appSetStateContext.Provider value={setState}>
{props.children}
</appSetStateContext.Provider>
</appContext.Provider>
)
}
使用
- import 引入 数据
- 使用
useContext
import React, { useContext } from 'react'
import { appContext, appSetStateContext } from '../AppState'
export const withAddToCart = (ChildComponent) => {
return (props) => {
const defaultContext = useContext(appContext) // => { username: '德玛西亚' }
const setState = useContext(appSetStateContext) // => undefined
}
}
自定义 hooks
hook
是函数- 命名以
use
开头 - 内部可调用其他
hook
函数 - 并非
React
的特性
import React, { useContext } from 'react'
// 命名以 use 开头
export const useAddToCart = () => {
// 内部可以调用其他 hook 函数
const setState = useContext(appSetStateContext)
// 复用逻辑
const addToCart = () => {
console.log('加入购物车,我是复用逻辑')
}
return addToCart
}
高阶组件 HOC
高阶组件是参数为组件,返回值为新组件的函数。
一般用来封装复用的逻辑
业务场景: 1、普通商品卡片 2、打折商品卡片 3、拥有复用的页面逻辑:加入购物车
- 编写高阶组件
// AddToCart.jsx
import React from 'react'
export const withAddToCart = (ChildComponent) => {
// 复用逻辑
const addToCart = () => {
console.log('加入购物车,我是复用逻辑')
}
return (props) => { // props 是业务中默认传递
return <ChildComponent {...props} addToCart={addToCart} />
}
}
- 使用高阶组件:
普通商品组件:
// Robot 组件
import { withAddToCart } from './AddToCart'
const Robot = ({ name, id, addToCart }) => {
const value = useContext(appContext)
return (
<div>
<h2>{name}</h2>
<p>{id}</p>
</div>
)
}
export default withAddToCart(Robot)
打折商品组件:
// RobotDiscount 组件
import { withAddToCart } from './AddToCart'
const RobotDiscount = ({ name, id, addToCart }) => {
const value = useContext(appContext)
return (
<div>
<h2>打折商品</h2>
<h2>{name}</h2>
<p>{id}</p>
</div>
)
}
export default withAddToCart(RobotDiscount)
业务中使用:
// 正常商品
<Robot name={'name'} id={'id'} />
// 打折商品
<RobotDiscount name={'name'} id={'id'} />
命名规范
一般以 with 开头
withXxxx
==> withAddToCart
遇到的问题
在使用 Menu
组件嵌套时,要注意 组件的 Key
值的唯一
下面代码只有一级 Menu 组件的key 是唯一;二三级都会存在重复的
export const SideMenu: React.FC = () => {
return (
<Menu mode="vertical" className={styles['side-menu']}>
{sideMenuList.map((m, i) =>
<Menu.SubMenu
key={`side-menu-${i}`} title={m.title}>
{m.subMenu.map((sm, smindex) =>
<Menu.SubMenu
key={`sub-menu-${smindex}`} title={sm.title}>
{sm.subMenu.map((sms, smsIndex) =>
<Menu.Item key={`sub-sub-menu-${smsIndex}`} title={sms} />
)}
</Menu.SubMenu>
)}
</Menu.SubMenu>
)}
</Menu>
)
}
解决方法:
遇到 树形结构时,如果没有唯一 id,要注意key 的使用,可以借助父级的 key
1.一级 menu:key = mIndex
1.1 二级 subMenu:key = mIndex_smIndex
1.1.1 三级 subSubMenu: key = mIndex_smIndex_smsIndex
export const SideMenu: React.FC = () => {
return (
<Menu mode="vertical" className={styles['side-menu']}>
{sideMenuList.map((m, mIndex) =>
<Menu.SubMenu key={`side-menu-${mIndex}`} title={m.title} icon={<FireOutlined />}>
{m.subMenu.map((sm, smIndex) =>
<Menu.SubMenu key={`sub-menu-${mIndex}_${smIndex}`} title={sm.title} icon={<GifOutlined />}>
{sm.subMenu.map((sms, smsIndex) =>
<Menu.Item key={`sub-sub-menu-${mIndex}_${smIndex}_${smsIndex}`} title={sms} icon={<GiftFilled />} />
)}
</Menu.SubMenu>
)}
</Menu.SubMenu>
)}
</Menu>
)
}
路由 react-router
其实安装是 react-router-dom
npm install react-router-dom
提供了 BrowserRouter
、HashRouter
、Route
、Switch
、Link
等组件
<Link />
组件可以渲染出<a/>
标签<BrowserRouter />
组件利用 H5 API 实现路由切换Switch
切换页面,会对路径做短路处理,每次只渲染一个单独页面,消除页面堆叠影响Route
路径组件,会页面堆叠,从上到下匹配路径并渲染页面。没有匹配到就是空页面(可以用来做404页面)<HashRouter/ >
组件利用原生 js 中的window.location.hash
来实现路由切换
网站系统的要求
- 路由导航与原生浏览器操作一样:使用
<BrowserRouter />
- 路由的路径解析原来与原生浏览器一样,可以自动识别 url 路径:使用
<Route />
- 路径的切换以页面为单位,不要页面堆叠:使用
<Switch />
基础路由组成:<BrowserRouter />
+ <Switch />
+ <Route />
添加 types 文件
TIP
只参与开发过程中使用依赖安装到 devDependencies 下,这些打包后不会影响代码的体积变大
@types/react-router-dom
只会在开发环境使用,实际运行并不需要,要安装在 devDependencies 下
不会参与最后的发布,这样打包后的体积会缩小,如果体积变大,用户打开网站时间也会变长
Route 组件
匹配页面
下面这种场景 会吧2个 路径的组件,同时渲染到一个页面上
<BrowserRouter>
<Route path={'/'} component={HomePage} />
<Route path="/signIn" render={() => <h1>登录</h1>} />
</BrowserRouter>
添加 exact 属性,精准匹配
exact:告诉 route 组件,有且仅有以 路径 一模一样的时候才做页面的匹配,负责继续执行下面的代码
<BrowserRouter>
<Route exact path={'/'} component={HomePage} />
<Route path="/signIn" render={() => <h1>登录</h1>} />
</BrowserRouter>
Route 组件会给 component 的 props 属性 传递 路由信息:history、location、match 等
路由文件
<BrowserRouter>
<Route exact path={'/'} component={HomePage} />
</BrowserRouter>
HomePage 文件:
import React from 'react'
export const HomePage: React.FC = (props) => {
console.log(props) // history、location、match 等路由信息
return (
<h1>HomePage</h1>
)
}
Switch 组件
会优先渲染匹配到路径,如果配置了 根路径 '/'
,会优先渲染到 根路径 '/'
上
根路径:'/'
路径a:'/a'
路径b:'/b'
url:'xxx.com/b'
这种情况也会匹配到 根路径 '/'
配合 exact 使用:
<BrowserRouter>
<Switch>
{/* 匹配顺序 从上到下 一次匹配 */}
<Route exact path={'/'} component={HomePage} />
<Route path="/signIn" render={() => <h1>登录</h1>} />
<Route path="/test" render={() => <h1>test</h1>} />
{/* 404 页面 什么路径都没有匹配到 */}
<Route render={() => <h1> 404 页面没有找到</h1>} />
</Switch>
</BrowserRouter>
完整路由
import React from 'react'
import styles from './App.module.css'
import { BrowserRouter, Link, Route, Switch } from 'react-router-dom'
import { HomePage } from './pages'
function App() {
return (
<div className={styles.App}>
<BrowserRouter>
<Switch>
{/* 匹配顺序 从上到下 一次匹配 */}
<Route exact path={'/'} component={HomePage} />
<Route path="/signIn" render={() => <h1>登录</h1>} />
<Route path="/test" render={() => <h1>test</h1>} />
{/* 404 页面 什么路径都没有匹配到 */}
<Route render={() => <h1> 404 页面没有找到</h1>} />
</Switch>
</BrowserRouter>
</div>
)
}
export default App
路由之间的跳转方法
- 高阶组件
withRouter
- Hooks 的方法:
useHistory
,useParams
,useRouteMatch
,useLocation
- Link 组件
高阶组件方法
高阶组件方法:react-router-dom
自己提供的 高阶组件 withRouter
withRouter
会向组件的 props 提供 history
, match
, location
等属性
利用 history.push()
可以跳转
import { withRouter, RouteComponentProps } from 'react-router-dom'
interface PropsType extends RouteComponentProps {
imageSrc: string
title: string
}
export const ProductImageCompoent: React.FC<PropsType> = ({
imageSrc, title, // 业务本身的 props
history, match, location // withRouter 提供的 props
}) => {
return (
<div onClick={() => history.push(`/detail/${id}`)}></div>
)
}
export const ProductImage = withRouter(ProductImageCompoent)
Hooks 的方法
Hooks 的方法:react-router-dom
自己提供的hooks方法:useHistory
, useParams
, useRouteMatch
, useLocation
利用 history.push()
可以跳转
import { useHistory, useParams, useRouteMatch, useLocation } from 'react-router-dom'
export const Header: React.FC = () => {
const match = useRouteMatch()
const history = useHistory()
const params = useParams()
const location = useLocation()
return (
<div className={styles['app-header']}>
<Button onClick={() => history.push('/register')}>注册</Button>
<Button onClick={() => history.push('signIn')}>登陆</Button>
</div>
)
}
Link 组件
使用 Link 组件:
- 节省代码,避免手动对导航栈进行处理
- 会别 a 标签包裹,可以使用a标签的一些特性,比如右击打开新的页面
import { Link } from 'react-router-dom'
export const ProductImageCompoent: React.FC<PropsType> = ({ id, imageSrc, }) => {
return (
<Link to={`/detail/${id}`}>
<Image src={imageSrc} height={285} width={490} />
</Link>
)
}
Link 组件的原理:
- 组件 props 接受
children
,to
2个属性 - 使用 a 标签包裹
children
- 引入
useHistory
hooks - a 标签 使用
useHistory
的 push 方法
import React from 'react'
import { useHistory } from 'react-router-dom'
interface LinkProps {
to: string
}
const Link: React.FC<LinkProps> = ({ children, to }) => {
const history = useHistory()
return (
<a href={to} onClick={() => history.push(to)}>
{children}
</a>
)
}
Redux 通讯
Redux 原理
- 剥离组件数据(state)
- 数据统一放在 store 中
- 组件订阅 store 获得数据
- store 同步 推送数据更新
Redux 统一保存数据,在隔离了数据与UI的同时,负责处理数据的绑定
什么时候需要使用 Redux
- 组件需要共享数据(状态 state)的时候
- 某个状态需要在任何地方都可以随时访问的时候(用户登录的全局数据共享)
- 某个组件需要改变另外一个组件状态的时候
- 场景:语言切换、黑暗模式切换、用户登录数据全局共享
简单的 Redux 工作流
- 根据 redux 的定义,任何 store 中的 state 都是 immutable 状态(不可修改的)
- 通过新的对象来代替原有你的数据:
const ne wState = { ...state, language: action.payload }
action 分发
react-redux
I18n 国际化
下载依赖 底层框架:i18next, react的插件:react-i18next
npm install react-i18next i18next --save
import { initReactI18next } from 'react-i18next'
在引用 initReactI18next 这个实例的时候,会自动 context api 的注入
添加语言包文件
以 json 数据结构
英文包 en.json:
json 语言包
{
"header": {
"slogan": "Make travel happier",
"add_new_language": "add new language",
"title": "Listen to Baibai Travel",
"register": "Register",
"signin": "Sign In"
},
"footer": {
"detail": "All rights reserved @ 花果水帘洞齐天大圣"
},
"home_page": {
"hot_recommended": "Hot Recommended",
"new_arrival": "New arrival",
"domestic_travel": "Domestic travel",
"joint_venture": "Joint Venture",
"start_from": "(start from)"
}
}
中文 zh.json
json 语言包
{
"header": {
"slogan": "让旅行更幸福",
"add_new_language": "添加新语言",
"title": "Listen to Baibai 旅游网",
"register": "注册",
"signin": "登陆"
},
"footer": {
"detail": "版权所以 ©️ 花果水帘洞齐天大圣"
},
"home_page": {
"hot_recommended": "爆款推荐",
"new_arrival": "新品上市",
"domestic_travel": "国内游推荐",
"joint_venture": "合作企业",
"start_from": "(起)"
}
}
添加 config 配置文件
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import translation_en from './en.json' // json 语言包
import translation_zh from './zh.json' // json 语言包
const resources = {
en: { translation: translation_en },
zh: { translation: translation_zh }
}
i18n.use(initReactI18next) // 使用 插件
.init({
resources,
lng: 'zh', // 默认语言
// 关闭后可以使用链式调用:header.slogan 可以使用链式调用
// keySeparator: false, // we do not use keys in form messages.welcome
interpolation: {
// 不会为强行转换字符串
escapeValue: false, // react already safes from xss,react本身会处理xss攻击
},
})
export default i18n
项目中使用 高阶函数 和 hooks
react-i18next
插件会提供 t
函数,可以通过 t(xxx.xxx)
来访问到 json 语言包 里面的数据
高阶函数方式使用:
json 语言包:
json 语言包
{
"header": {
"slogan": "让旅行更幸福",
"register": "注册",
"signin": "登陆"
}
}
{
"header": {
"slogan": "Make travel happier",
"register": "Register",
"signin": "Sign In"
}
}
- 引如高阶函数:
withTranslation
,以及类型WithTranslation
,注意大小写 - 传入类型
React.Component<RouteComponentProps & WithTranslation, State>
- 直接从 props 引用 t 函数:
const { t } = this.props
- 倒出
export const Header = withTranslation()(HeaderComponent)
TIP
注意:withTranslation()()
有2个括号,可以理解成高阶函数的高阶函数
import React from 'react'
import { withTranslation, WithTranslation } from 'react-i18next'
class HeaderComponent extends React.Component<RouteComponentProps & WithTranslation, State> {
render() {
const { t } = this.props
return (
<div>
<div>{t('header.slogan')}</div> {/* 让旅行更幸福 */}
<Button.Group>
<Button>{t('header.register')}</Button> {/* 注册 */}
<Button>{t('header.signin')}</Button> {/* 登录 */}
</Button.Group>
</div>
)
}
}
export const Header = withTranslation()(HeaderComponent)
Hooks 方式使用:
json 语言包:
json 语言包
{
"footer": {
"detail": "版权所以 ©️ 花果水帘洞齐天大圣"
}
}
{
"footer": {
"detail": "All rights reserved @ 花果水帘洞齐天大圣"
}
}
- 引入 hooks 方法:
useTranslation
- 在函数组件里面使用:
const { t } = useTranslation()
- 使用:
t('footer.detail')
还是 hooks 方式简单
import React from 'react'
import { useTranslation } from 'react-i18next'
export const Footer: React.FC = () => {
const { t } = useTranslation()
return (
<div>
{/*版权所以 ©️ 花果水帘洞齐天大圣*/}
{t('footer.detail')}
</div>
)
}