theme: channing-cyan
1.react的介绍与使用
(1) react是什么
React 是一个用于构建用户界面的 JavaScript 库。
(2)react的特点
声明式 组件化 随学随用
(3)脚手架建立项目
- 命令:
npx create-react-app +项目名称
。 - 启动项目:
yarn start
ornpm start
。 - npx 是
npm@v5.2
版本新添加的命令,用来简化 npm 工具包的使用流程。
(4)JSX
1.因为React.createElement()
创建 React 元素的问题:繁琐/不简洁;不直观,无法一眼看出所描述的结构;代码不容易维护!所以有JSX
2.JSX 是 JavaScript XML 的简写,表示可以在 JavaScript 代码中写 XML(HTML) 格式的代码。
优势:声明式语法更加直观,与 HTML 结构相同,降低了学习成本,提高了开发效率,JSX 是 React 的核心之一。
3.注意点
3.1. 必须有 1 个根节点,或者虚拟根节点 <></>
、<React.Fragment></React.Fragment>
。
3.2. 属性名一般是驼峰的写法且不能是 JS 中的关键字,例如 class 改成 className,label 的 for 属性改为 htmlFor
,colspan 改为 colSpan
。
3.3. 元素若没有子节点,可以使用单标签,但一定要闭合,例如 <span/>
。
3.4. React@16.14
之前需要先引入 React 才能使用 JSX(这个也好理解,因为 JSX 最后还是要被转成 React.createElement()
的形式)。
3.5. 换行建议使用 ()
进行包裹,防止换行的时候自动插入分号的 Bug。
(5)18.0版本入口文件
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.querySelector("#root")).render(<App />)
2.组件介绍与通信
(1)函数组件
本质上来说就是一个 JS 函数必须以大写字母开头,必须有返回值函数组件中this指向undefined
(2)类组件
使用ES6语法中的class创建的组件类组件应该继承 React.Component
父类
类组件this指向:
1.柯里化:通过函数调用继续返回函数的形式,实现多次接收参数最后统一处理的函数编码形式
state = {
count: 0,
}
handleClick() {
// 这里的 this 指向是什么?那就看是谁调用的!
return () => {
console.log(this.state.count)
}
}
render() {
return (
<div>
<h2>计数器:{this.state.count}</h2>
<button onClick={this.handleClick()}>+1</button>
</div>
)
}
}
2.箭头函数中的 this 指向“外部”,即 render 函数,而 render 函数中的 this 正是组件实例
class App extends Component {
state = {
count: 0,
}
handleClick() {
console.log(this.state.count)
}
render() {
return (
<div>
<h2>计数器:{this.state.count}</h2>
<button onClick={() => this.handleClick()}>+1</button>
</div>
)
}
}
3.使用 bind
class App extends Component {
state = {
count: 0,
}
handleClick() {
console.log(this.state.count)
}
render() {
return (
<div>
<h2>计数器:{this.state.count}</h2>
<button onClick={this.handleClick.bind(this)}>+1</button>
</div>
)
}
}
4.通过赋值语句往实例上面添加一个箭头函数
class App extends Component {
state = {
count: 0,
}
handleClick = () => {
console.log(this.state.count)
}
render() {
return (
<div>
<h2>计数器:{this.state.count}</h2>
<button onClick={this.handleClick}>+1</button>
</div>
)
}
}
5.在构造函数中再创建一个实例方法,和原型方法公用一个函数体
constructor() {
super()
this.state = {
count: 0,
}
// 1. 往实例自身上又挂载了一个 handleClick 函数
// 2. 此函数的函数体是通过原型上 handleClick 函数生成的新函数
// 3. 并把原型上 handleClick 函数中的 this 通过 bind 绑定为了 this,而这里构造函数中的 this 正是实例对象
// 4. 其实点击的时候调用的是这个构造函数 handleClick(就近原则),而这个构造函数中的 handleClick 执行的是原型上的 handleClick 的函数体
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
console.log(this.state.count)
}
render() {
return (
<div>
<h2>计数器:{this.state.count}</h2>
<button onClick={this.handleClick}>+1</button>
</div>
)
}
}
父传子
例如:salary={this.state.salary}
函数组件 接收{props.salary}
类组件 接收{this.props.salary}
子传夫
通过自定义事件
子onClick={this.props.getMsg }
夫getMsg={this.getChildMsg}
兄弟通信(状态提升)
- 准备 A、B 兄弟组件。
- 把需要操作的 B 组件中的数据 count 提升到公共的父组件里面。
- 父组件提供数据和操作数据的方法。
- 把数据传递给 B 组件,把操作数据的方法传递给 A 组件。
Context 实现跨级组件通讯
1.一般先封装Context
import { createContext } from "react";
export const Context = createContext();
指定默认值
createContext({ age: 88, })
2.祖先组件通过 <Context.Provider>
配合 value 属性提供数据
3.后代组件通过 <Context.Consumer>
配合函数获取数据
import React, { Component } from 'react'
import { context } from './App'
export default class B extends Component {
render() {
return (
<context.Consumer>
{(value) => {
return (
<div>
<h1>{value.money}</h1>
<button onClick={() => value.changeMoney(8)}>changeMoney</button>
</div>
)
}}
</context.Consumer>
)
}
}
4.另一种获取数据的方式
import React, { Component } from 'react'
import { Context } from '../../context'
export default class B extends Component {
// 声明一个静态属性 contextType 等于 Context 对象
static contextType = Context
// 然后就可以通过 this.context 拿到传递过来的数据啦
render() {
return <div>B {this.context.age}</div>
}
}
children 属性
<Hello children='我是子节点' />
// children 是一个特殊的 prop,上面的写法和下面等价,当内容比较多的时候,下面的写法更加直观
<Hello>我是子节点</Hello>
//也可以当作vue中作用域插槽来使用
function Hello(props) {
return <div>该组件的子节点:{props.children}</div>
}
props校验
对于组件来说,props 是外来的,无法保证组件使用者传入数据的格式正确,如果传入的数据格式不对,可能会导致组件内部报错,而组件的使用者不能很明确的知道错误的原因。
如何对 props 进行校验
- 安装并导入
prop-types
包。 - 使用
组件名.propTypes = {}
来给组件的 props 添加校验规则。 - 校验规则通过
PropTypes
对象来指定。
4.使用
static propTypes={
colors:PropTypes.array
}
常见校验规则
- 常见类型:number、string、bool、array、func、object。
- React 元素类型(JSX):element。
- 必填项:isRequired。
- 特定结构的对象:shape({})。
{
// 常见类型
fn1: PropTypes.func,
// 必选
fn2: PropTypes.func.isRequired,
// 特定结构的对象
obj: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
}
默认值
import React, { Component } from 'react'
class Test extends Component {
render() {
const { age = 18 } = this.props
return <div>{age}</div>
}
}
export default Test
生命周期
只有类组件才有生命周期
setState 更新数据的表现
- 一般情况下(常见的在生命周期或合成事件处理函数中),通过
setState()
方法来更新数据,表现是异步的。 - 当执行到 setState 这一行的时候,React 出于性能考虑,并不会马上进行调用来修改 state,而是先把这个以及后续的更新对象放到一个更新队列里面进行合并的操作,期间不影响后续代码的执行。
- 多次调用 setState(),只会触发一次重新渲染,所以无需担心多次进行
setState
会带来性能问题。
如果是在 setTimeout/setInterval 或者原生事件的回调中,表现出来是同步的。
setState 推荐语法
1.通过 setState 第二个参数可以立即拿到更新后的数据。
this.setState({}, () => {
console.log('这个回调函数会在状态更新后立即执行')
})
2.推荐使用 setState((preState) => {})
语法
这种语法依旧是异步的,不同的是通过 preState 可以获取到最新的状态。
Hooks
React@16.8
以后,class 组件(提供状态和生命周期) + 函数组件(展示内容),Hooks(提供状态和生命周期) + 函数组件(展示内容),也可以混用这两种方式,即部分功能用 class 组件,部分功能用 Hooks + 函数组件。- 注意:Hooks 只能在函数组件中使用,虽然有了 Hooks,但 React 官方并没有计划从 React 库中移除 class。
useState
作用:为函数组件提供状态和修改状态的方法。
-
组件第 1 次渲染
- 调用函数式组件,从头开始执行组件中的代码逻辑。
- 调用
useState(0)
将传入的参数作为初始状态值,即:0。 - 开始渲染组件,此时得到的状态 count 值为:0。
-
组件第 2 次渲染
- 点击按钮,调用
setCount(count + 1)
来修改状态,因为状态发生改变,所以,该组件会重新渲染。 - 组件重新渲染时,会再次执行该组件中的代码逻辑。
- 再次调用
useState(0)
,此时 React 内部会拿到最新的状态值而非初始值,比如该案例中的最新状态值为 1。 - 再次渲染组件,此时,获取到的状态 count 值为:1。
- 点击按钮,调用
-
强调:useState 的初始值(参数)只会在组件第一次渲染时生效,也就是说,以后的每次渲染,useState 获取到都是最新的状态值,React 组件内部会记住每次更新后的最新状态值!
useState(回调函数)
,回调函数的返回值就是状态的初始值,该回调函数只会触发一次。
useEffect副作用
那组件或一般函数的副作用是什么呢?
a,组件的副作用:对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用,比如手动修改 DOM、数据(AJAX)请求、localStorage 操作等。
b,函数的副作用:如果一个函数修改了其局部环境之外的数据,那么它就被称为有副作用。
// 触发时机:第一次渲染会执行,任何数据变化导致组件更新时执行,相当于 componentDidMount + ComponentDidUpdate
useEffect(() => {})
// 触发时机:只在组件第一次渲染时执行,相当于 componentDidMount
useEffect(() => {}, [])
// 触发时机:第一次渲染会执行,当 count 变化时会再次执行,相当于 componentDidMount + componentDidUpdate(判断)
useEffect(() => {}, [count])
useEffect清理副作用
-
useEffect 可以返回一个函数,这个函数称为清理函数,在此函数内用来执行清理相关的操作(例如事件解绑、清除定时器等)。
-
清理函数的执行时机
a,useEffect 的第 2 个参数不写或写了一个有依赖项的数组,清理函数会在下一次副作用回调函数调用时以及组件卸载时执行,用于清除上一次或卸载前的副作用。
b,useEffect 的第 2 个参数为空数组,那么只会在组件卸载时会执行,相当于组件的
componetWillUnmount
。 -
建议:一个 useEffect 只用来处理一个功能,有多个功能时,可以使用多个 useEffect。
useEffect 发送请求
- useEffect 是专门用来处理副作用的,所以发送请求这个副作用可以在 useEffect 回调内进行处理。
- 注意:useEffect 的回调只能是一个同步函数,即不能使用 async 进行修饰。
- 原因:如果 useEffect 的回调是异步的,此时返回值会被 Promise 化,这样的话就无法保证清理函数被立即调用。
- 若需要使用 async/await 语法,可以在 useEffect 回调内部再次创建 async 函数并调用。
useEffect(() => {
async function fetchMyAPI() {
let url = 'http://something/' + productId
const response = await myFetch(url)
}
fetchMyAPI()
}, [productId])
useRef 操作 DOM
- 使用 useRef 创建一个有 current 属性的 ref 对象,
{ current: null }
。
const xxxRef = useRef(null)
- 通过 DOM 的 ref 属性和上面创建的对象进行关联。
<div ref={xxxRef}></div>
- 通过 xxxRef.current 就可以访问到对应的 DOM 啦。
useContext 使用
- 作用:在函数组件中,获取
<Context.Provider/>
提供的数据。 - 参数:Context 对象,即通过
React.createContext
函数创建的对象。 - 返回:
<Context.Provider/>
提供的 value 数据。
import { useContext } from 'react'
import { Context } from './countContext'
export default function Child() {
const value = useContext(Context)
return (
<div>
Child
<h3>{value.count}</h3>
</div>
)
}
Redux
Redux 是一个全局状态管理的 JS 库
三个核心概念
a,action(动作):描述要做的事情(要干啥)。
export const increment = (payload) => ({
type: 'INCREMENT',
payload,
})
export const decrement = (payload) => ({
type: 'DECREMENT',
payload,
})
b,reducer(函数):更新状态(怎么干)。
export default function counter(state = 10, action) {
// 处理各种各样的 action
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
default:
// 记得要有默认返回的处理
return state
}
}
c,store(仓库):整合 action 和 reducer(谁来指挥)。
-
一个应用只有一个 Store。
b,创建:
const store = createStore(reducer)
。c,获取数据:
store.getState()
。d,更新数据:
store.dispatch(action)
。 -
其他 API。
a,订阅(监听)状态变化:
const unSubscribe = store.subscribe(() => {})
,注意要先订阅,后续的更新才能被观测到。b,取消订阅状态变化:
unSubscribe()
。// store: 整个数据的仓库,负责关联 reducer 和 action //通过 store 对象可以给 reducer 分配 action import { createStore } from 'redux' import reducer from './reducers' const store = createStore(reducer) export default store
纯函数
-
纯函数是函数式编程中的概念,对于纯函数来说有一个很重要的特点:相同的输入总是得到相同的输出。
-
纯函数常具有以下特点。
a,不得改写参数,不能使用全局变量。
b,不能调用 Date.now() 或者 Math.random() 等不纯的方法,因为每次会得到不一样的结果。
c,不包含副作用的处理,副 作用:AJAX 请求、操作本地数据、或者操作函数外部的变量等。
-
好处:代码简洁、方便测试、方便性能优化。
-
为什么说纯函数呢?因为 reducer 要求自身就必须是一个纯函数。
react-redux
- React 和 Redux 是两个独立的库,两者之间职责独立,因此,为了更好的实现在 React 中使用 Redux 进行状态管理,就需要一种机制,将这两个独立的库关联在一起,这就是 react-redux 出现的原因。
- react-redux 是 Redux 官方提供的 React 绑定库。
配置
import ReactDOM from 'react-dom'
import App from './App.js'
import store from './store/store.js'
import { Provider } from 'react-redux'
// 通过 Provider 提供 store 供其他组件内部使用
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
)
// 用了 react-redux 下面手动触发更新的方式就没用了
/* store.subscribe(() => {
ReactDOM.render(<App />, document.querySelector('#root'))
}) */
✍ 一旦使用了 react-redux,获取和更新数据的方式就变化了,要按照这个库的要求来。
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './store/actions'
const App = () => {
const count = useSelector((state) => state)
const dispatch = useDispatch()
return (
<div>
<h3>{count}</h3>
<div>
<button onClick={() => dispatch(increment(1))}>+1</button>
<button onClick={() => dispatch(increment(5))}>+5</button>
<button onClick={() => dispatch(decrement(1))}>-1</button>
<button onClick={() => dispatch(decrement(5))}>-5</button>
</div>
</div>
)
}
export default App
Reducer 的分离与合并
解决:使用 Redux 中的 combineReducers({ counter: counterReducer, user: userReducer })
函数。
import { combineReducers } from 'redux'
function counter(state = 10, action) {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
default:
return state
}
}
function user(state = { name: 'ifer', age: 18 }, action) {
switch (action.type) {
case 'UPDATENAME':
return {
...state,
name: action.payload,
}
default:
return state
}
}
export default combineReducers({
counter,
user,
})
ActionTypes
- 是什么:action 对象中的 type 属性。
- Redux 项目中,同一个 type 会在不同文件中多次被用到,比如 actions.js、reducers.js 等。
- 目标:集中处理 action type,保持一致性,容易维护!
-
在 store 目录中创建
actionTypes.js
或constants.js
文件。 -
使用常量创建 ActionType 并导出。
-
命名推荐:
模块_动作
,比如:- 计数器:
COUNTER_INCREMENT
表示计数器模块中的 INCREMENT 动作。 - TodoList:
TODOLIST_ADD
表示 TodoList 案例中 ADD 动作。 - 登录:
LOGIN_GETCODE
表示登录模块中获取验证码的动作,LOGIN_SUBMIT
表示登录模块中的提交功能。 - 个人信息:
PROFILE_GETINFO
表示个人资料模块中的获取信息动作;PROFILE_UPDATEINFO
等。
- 计数器:
-
哪里需要用到就按需导入。
actionTypes.js
export const COUNTER_INCREMENT = 'COUNTER_INCREMENT'
export const COUNTER_DECREMENT = 'COUNTER_DECREMENT'
export const USER_UPDATENAME = 'USER_UPDATENAME'
actions.js
import { COUNTER_INCREMENT, COUNTER_DECREMENT, USER_UPDATENAME } from './actionTypes'
export const increment = (payload) => ({
type: COUNTER_INCREMENT,
payload,
})
export const decrement = (payload) => ({
type: COUNTER_DECREMENT,
payload,
})
export const updateName = (payload) => ({
type: USER_UPDATENAME,
payload,
})
中间件概述
-
Redux 中间件执行时机:在 dispatching action 和 到达 reducer 之间。
a,没有中间件:
dispatch(action) => reducer
。b,使用中间件:
dispatch(action) => 执行中间件代码 => reducer
。 -
原理:封装了 Redux 的 dispatch 方法。
a,没有中间件:
store.dispatch()
使用的是 Redux 库自己提供的 dispatch 方法,用来发起状态更新。b,使用中间件:
store.dispatch()
使用的是中间件封装处理后的 dispatch,但是最终还是会调用 Redux 库自己提供的 dispatch 方法
redux-thunk 使用
`redux-thunk` 中间件可以处理函数形式的 action,而在函数形式的 action 中就可以执行异步操作代码,完成异步操作。
- 安装:
yarn add redux-thunk
。 - 导入 redux-thunk。
- 将 thunk 添加到中间件列表中。
- 修改 action creator,返回一个函数。
- 在函数形式的 action 中执行异步操作,在异步操作成功后,分发 action 更新状态。
redux-devtools-extension调试工具
- 保证浏览器安装了 Redux 的开发者工具。
- 通过包管理器在项目中安装
yarn add redux-devtools-extension
。 - 在 store/index.js 中进行配置。
- 启动 react 项目,打开 chrome 开发者工具,测试
import { createStore, applyMiddleware } from 'redux'
import reducer from './reducers'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))
React Router
了解 SPA
-
SPA:
Single Page Application
单页面应用程序,整个应用中只有一个页面(index.html)。 -
MPA :
Multiple Page Application
多页面应用程序,整个应用中有很多个页面(*.html)。 -
优势:页面响应速度快,体验好(无刷新),降低了对服务器的压力。
a,传统的多页面应用程序,每次请求服务器返回的都是一整个完整的页面。
b,单页面应用程序只有第一次会加载完整的页面,以后每次请求仅仅获取必要的数据。
-
缺点:不利于 SEO 搜索引擎优化。
a,因为爬虫只爬取 HTML 页面中的文本内容,不会执行 JS 代码。
b,可以通过 SSR(服务端渲染 Server Side Rendering)来解决 SEO 问题,即先在服务器端把内容渲染出来,返回给浏览器的就是纯 HTML 内容了。
前端路由
现代的前端应用大多都是 SPA,也就是只有一个 HTML 页面的应用程序,因为它的用户体验更好、对服务器的压力更小,所以更受欢迎。
为了有效的使用单个页面来管理原来多页面的功能,前端路由应运而生,功能:让用户从一个视图(页面)导航到另一个视图(页面)。
- 安装。
yarn add react-router-dom@5.3.0
react-router-dom
这个包提供了三个核心的组件。
import { HashRouter, Route, Link } from 'react-router-dom'
- 使用
HashRouter
包裹整个应用,一个项目中只会有一个 Router。
<HashRouter>
<div className="App">App</div>
</HashRouter>
- 使用 Link 指定导航链接。
<Link className='nav-link active' aria-current='page' to='/findmusic'>
发现音乐
</Link>
- 使用
Route
指定路由规则。
// 在哪里写的 Route,最终匹配到的组件就会渲染到哪里
<Route path='/findmusic' component={FindMusic} />
Router 详细说明
- 常用有两种 Router:
HashRouter
和BrowserRouter
,用来包裹整个应用,一个 React 应用只需要使用一次。 - HashRouter:使用 URL 的哈希值实现(
http://localhost:3000/#/first
),是通过监听 window 的hashchange
事件来实现的。 - BrowserRouter:使用 H5 的 history API 实现(
http://localhost:3000/first
),是通过监听 window 的popstate
事件来实现的。
// 使用时建议通过 as 起一个别名,方便修改
import { HashRouter as Router, Route, Link } from 'react-router-dom'
路由的执行过程
- 点击 Link 组件(a 标签),浏览器地址栏中的 url 发生变化。
- ReactRouter 通过
hashchange
或popState
监听到了地址栏 url 的变化。 - ReactRouter 内部遍历所有 Route 组件,使用路由规则(path)与 pathname(hash)进行匹配。
- 当路由规则(path)能够匹配地址栏中的 pathname(hash)时,就展示该 Route 对应的组件。
Link 与 NavLink
-
Link
组件最终会渲染成 a 标签,用于指定路由导航。a,to 属性,将来会渲染成 a 标签的 href 属性。
b,
Link
组件无法实现导航的高亮效果。 -
NavLink
组件,一个更特殊的Link
组件,可以用于指定当前导航高亮。a,to:用于指定地址,会渲染成 a 标签的 href 属性。
b,activeClass:用于指定高亮的类名,默认
active
。c,exact:精确匹配,表示必须精确匹配类名才会应用 class,默认是模糊模糊匹配。
Route 匹配规则
- Route 组件 path 属性对应的值表示:默认是以此值开头的路径就会被匹配,添加 exact 属性可以开启精确匹配。
- 所以默认情况下,path 为
/
能够匹配所有路由组件,因为所有路由组件都是以/
开头的,一般来说,如果路径配置了/
,往往都需要配置 exact 属性。 - 如果 path 的路径匹配上了,那么对应的组件就会被 render,否则就会 render null。
- 如果没有指定 path,那么一定会被渲染,例如
<Route component={NotFound}></Route>
。
Switch 与 404
- 通常,会把一个个的
Route
包裹在一个Switch
组件中,这样只会渲染第一个匹配到的组件,往往是我们期望的。 - 通过
Switch
组件配合不带 path 属性的 Route 组件能实现 404 效果,即便不需要实现 404,也可以用 Switch 包裹来提升性能。
编程式导航
props.history.push('/comment')
动态路由传参
- 入口。
<NavLink className='nav-link' to='/findmusic/ranking/bsb'>
飙升榜
</NavLink>
- 出口。
<Route path='/findmusic/ranking/:id' component={RankingList} />
- 获取。
// props.match.params.id 或者通过 useParams()
Query 传参
- 入口。
<NavLink className='nav-link' to='/findmusic/ranking/?id=bsb'>
飙升榜
</NavLink>
- 出口。
<Route path='/findmusic/ranking' component={RankingList} />
- 获取。
import qs from 'qs'
export default function RankingList(props) {
const { id } = qs.parse(props.location.search.slice(1))
return (
<>
<span className='me-3'>props 获取参数: {id}</span>
</>
)
}