基本
1 | # 创建基于ts的react项目 |
详情见https://juejin.cn/post/7081281152069140494
必须依赖三个库
react 核心代码
react-dom 使react渲染在不同平台
babel 转换jsx
react-dom
默认导出ReactDom
对象
方法:
render(content, target)
content
渲染的内容(jsx)target
挂载的对象(dom对象)
组件
1 | // 创建组件 |
- 当数据更新后,不推荐直接修改state数据源,必须手动调用
this.setState({...})
修改,react才会重新render渲染到界面 - jsx中事件绑定组件方法执行时,受到react调用了
call
处理,this
指向为空。解决方法:- 绑定时用
bind
指定this
-bind
的优先级要高于call
- 构造器中用
bind
重新赋值方法 - 采用箭头函数定义方法
- 绑定时用箭头函数包裹调用(推荐)
- 绑定时用
- 函数式组件没有状态和生命周期,但是可以用hooks解决
- 组件父类
React.Component
已经实现将props
自动赋给this
,只需要在构造器中调用super(props)
这个操作react内部已经帮我们做了可不写 - 设置静态属性
displayName
可定义组件在开发工具的名称,否则会默认用类名 - 官方强烈建议不要创建自己的组件基类,因此高阶组件都是通过函数,而不是创建class继承 - 组合非继承
- 懒加载组件:
React.lazy(()=>import(path))
数据通信
父子组件通信通过
props
向传递数据,子传父可以由父组件向子组件传递方法,子组件调用并传递参数。注意this
指向跨组件通信使用
context
- react v16.3后的版本:React.createContext
创建context对象context内都有一个Provider组件(默认,创建context后直接使用),运行消费者组件订阅context变化。消费组件作为
Context.Provider
的后代组件放置。Context.Provider
接收一个value
属性传递给下面的所有消费组件Context.Provider
可以嵌套,里层会覆盖外层数据Context.Provider
上的value
变化时,所有其下的消费组件会重新渲染。传入的value应该是state或者props才可触发render消费组件使用:
类组件:
组件.contextType = context
或者static contextType = context
内部才可以通过
this.context
访问最近的context数据使用ts的话需要在组件内声明context类型:
context!: React.ContextType<typeof MyContext>
类组件嵌套多个
context
只有最近有个生效,除非像函数式组件原因使用consumer
函数式组件内部:
1
2
3
4
5
6
7
8
9//value相当于this.context,当Provider嵌套时,这种写法也要嵌套
<Context.consumer>
{
value => {
//通过value访问
return <div>value.xx</div>
}
}
</Context.consumer>
同样可以用事件总线进行跨组件通信,events社区用的多。通过
EventEmitter
构造函数实例eventBus
eventBus.emit
发送事件eventBus.addListener
监听事件eventBus.removeListener
移除监听
setState
setState
使用后不能马上拿到更新后的数据
- 定时器、及原生dom事件的回调内,
setState
与更新后的状态获取是同步的(宏任务) - 组件生命周期和react合成事件内是异步的, 不能马上拿到
主要原因:
- 性能考虑,避免短期频繁调用render函数
- 同步更新state的话,若没有执行完render,state和props不能同步.
解决方法
setState
第二个参数回调(箭头函数)中获取- 生命周期钩子
componentDidUpdate
中获取 - 在
setState
后的setTimeout
内可获取,时间可以为0 - 原生dom事件的回调内可获取
选项合并
调用
setState
时,第一层未传入的属性不会消失,在源码中通过Object.assign
合并了未更改的属性同时调用多次
setState
,同一个属性会取最后一次设置的值,因为react通过上下文的不同对setState进行同步或批处理。如果希望短期每次设置都以上一次结果继续,传入的第一个参数应该为函数:1
2
3
4
5this.setState((preState, props) => {
...
//preState保证拿到上次设置的状态
return newState
})
数组修改
在数组内的数据发生变动时,我们习惯用
push
、splice
等方法去修改,但直接对state
数据使用这些方法会影响数据的不可变性。
通常使用[...arr]
等浅拷贝创建新变量,对新变量操作后进行setState
hooks
新版v16.8后 react自身提供一些hooks方便函数式组件的编写
hooks只能在函数组件最外层,不能在条件判断、循环、嵌套函数中调用,因其本质是列表
react16推出fiber优化界面渲染,在reconciliation中将影响界面渲染的耗时操作分成fiber执行单元(碎片)
因requestIdleCallback有兼容问题,自行实现了类似的Chanel函数,保证在界面刷新的1贞中处理渲染后的剩余时间分优先级运行update.dispatch更新
useState
自定义组件state,返回第二个参数函数修改stateuseEffect(cb, arr)
实现类似生命周期的作用,第二个参数有如下作用:- 不传 组件渲染完成、更新触发回调+回调返回的函数
- 空数组 组件渲染完成执行回调,组件卸载前执行回调返回的函数
- 非空数组 组件在渲染、卸载之外;数组内的依赖更新时也会触发回调+回调返回的函数
可以使用多个进行逻辑拆分
useContext(context)
直接获取context数据,不用consumer去嵌套useReducer(reducer,initVal)
useState
的替代,与redux无关,以reducer的方式对复杂逻辑/依赖状态的state进行拆分。useState
内部本质上也是useReducer
useCallback(fn, deps)
优化函数型变量性能 :当deps中的依赖不变时不会重新定义,配合
memo
创建子组件,子组件再使用传入的函数可减少不必要的渲染useMemo(fn, deps)
与useCallback
类似,处理函数外的变量:重新渲染时避免处理不必要的逻辑(复杂计算、传给子组件引用类型)。涉及传到子组件的变量也要搭配memo
函数包装子组件useRef
代替createRef
创建ref 还可以传入state,返回的ref.current在整个生命周期不会随原state改变(记录历史状态)useImperativeHandle(ref, cb, deps)
cb内一般返回对象,自定义ref暴露的方法属性,外部ref.current会被替换成cb的返回值useLayoutEffect(cb, deps)
dom加载前执行,会阻塞dom更新,通过判断重新设置state可以减少dom渲染次数;useEffect
是在dom加载后执行
通过useXX命名的函数会被认为是自定义hooks
除了函数组件,自定义hooks内部才能使用hooks
JSX
本质上是通过babel转换为React.createElement(type, props, c1, c2...)
的函数调用。
因此每项都为jsx的数组直接放到jsx{}
中会被解析为同级的兄弟元素
特性:
只能有一个根标签,或者使用
Fragments
包裹多个标签,不会添加额外dom:<React.Fragment> ... </React.Fragment>
1
2
3
4
- ```jsx
//Fragments只支持添加key属性,但此语法不能加任何属性,包括key
<>...</>
设置class要用
className
,通常使用classnames便于动态添加类由于
for
是js中的关键字,label
标签的for
属性在jsx中要用htmlFor
设置内联样式可以传入对象,属性
key
必须为驼峰添加事件要用
onEvent
{}
内的undefined
、null
、boolean
类型数据不会显示在界面,而object
会报错数组、fragment可使jsx有多个根节点
属性展开: 在jsx组件上使用
{...obj}
可将obj
对象内部的属性全部传递到子组件
表单
受控组件
表单元素由state作为value,通过修改state作为唯一数据源,这种单向数据流是受控组件: state => 界面
- 例如
input
通过change
事件监听,修改对应value绑定的state
非受控组件
表单内的数据不交给state管理,只监听提交时通过
ref
等方式获取
实现插槽
- 可使用
props.children
来实现插槽,容器组件通过索引获取并放到对应位置。有顺序要求并且不够直观,建议只有一个children时使用 - 通过
props
传递jsx可以实现精准的具名插槽
portals
渲染子节点到不同的dom子树,不管渲染到何处,该节点仍保持与组件上下文的联系
ref
用于获取组件对象或dom
向目标jsx添加
ref
属性,值可以是:- 字符串 (新版已弃用,不推荐) 通过
this.refs.xx
获取 - 对象 传入通过
createRef
创建的对象,通过对象的current
属性获取 - 函数 传入回调函数,通过函数参数拿到元素
- 字符串 (新版已弃用,不推荐) 通过
由于函数式组件不会创建实例,
ref
拿不到组件对象,可通过React.forwardRef
传递内部的ref(还是拿不到函数式组件本身对象,因为没有):forwardRef
接收render函数创建函数式组件,有props
、ref
两个参数- 可向函数组件内的任意内容传入
ref
属性,值为render函数第二个参数。 - 之后在父组件就可以正常用
ref
获取函数式组件中的任意dom或组件对象(非函数式)
校验props
项目中没有使用flow、ts时,可以用prop-types
库来校验props。prop-types
在React15.5之前是内置的,现在需要手动引入
1 | // 所有组件可用 |
生命周期
Mount
装载 组件第一次被dom树渲染Update
更新 组件状态变化 程序渲染Unmount
卸载 组件从dom树中被移除
生命周期函数
- componentDidMount 组件挂载到dom树后
- componentDidUpdate 更新后
- componentWillUnmount 组件卸载及销毁之前
不常用
- getDerivedStateFromProps 调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。应返回一个对象来更新 state,如果返回
null
则不更新任何内容 - shouldComponentUpdate 返回布尔值决定是否渲染render
- getSnapShotBeforeUpdate 最近一次渲染到dom前,可以获取之前的一些dom信息,返回值会作为componentDidUpdate的参数
严格模式
React.StrictMode
在开发阶段进行额外的检查和警告:
- 不安全的生命周期
- 过时的
ref
(字符串)、context
写法 - 废弃的
findDomNode
API
严格模式下,开发环境class组件会调用两次以检查副作用
样式
css modules
react脚手架内置,样式文件名后缀前加上
.mdoule
如index.module.scss
默认会暴露一个styleModule
对象,样式文件内的所有选择器作为对象属性的key
组件导入后,将styleModule.key
复制到对应元素的属性上:
1 | // index.module.scss |
1 | import styleModule from './index.module.scss' |
缺点:
1.类名不能用
-
连接,只能用驼峰,因为js不支持2.使用较繁琐
3.不方便动态修改,只能内联
css in js
有很多的三方库,主要推荐:
styled components
、emotion
glamorous
已经很久不维护了
利用了es6中使用模板字符串方式调用函数:
1 | // 函数参数可获取拆分后的字符串以及插值内容进行处理 |
以styled-components
为例:
1 | import styled from 'styled-components' |
1 | //提供组件`ThemeProvider`设置主题样式 |
缺点 :
1.较大型项目性能较低
2.增加了额外的层
3.自动生成的选择器影响可读性
过渡动画
安装react-transition-group实现过渡动画,内部包含四个组件:
Transition
支持平台无关CSSTransition
最常用,基于Transition
;有apprear、enter、exit三个状态;对应以下class(例classNames
为xx):- 开始状态:
xx-appear
、xx-enter
、xx-exit
- 执行状态:
xx-appear-active
、xx-enter-active
、xx-exit-active
- 执行结束:
xx-appear-done
、xx-enter-done
、xx-exit-done
常用属性:
in
切换显示/离场timeout
动画class切换时间classNames
class类名前缀unmountOnExit
退出后卸载组件appear
首次加载后立即执行一次转换,再通过样式设置默认显示时(in=true
)进入的默认动画
- 开始状态:
SwitchTransition
两组件显示切换,内部包裹CSSTransition
并添加key
,只能有一个子节点1
2
3
4
5
6
7
8
9
10render(): ReactNode {
const {show} = this.state;
const A = <CSSTransition key="about" classNames="my" timeout={100}><About/></CSSTransition>
const P = <CSSTransition key="profile" classNames="my" timeout={100}><Profile/></CSSTransition>
return
<SwitchTransition mode="out-in">
{show ? A : P}
</SwitchTransition>
}常用属性:
mode
切换插入模式:out-in
、in-out
TransitionGroup
: 将多个动画组件包裹,分组,也需要key
。可以有多个子节点
TransitionGroup
与SwitchTransition
内部的CSSTransition
不用再添加in
属性,控制渲染的切换即可
Redux
纯函数
- 确定的输入产生确定的输出。有依赖外部的数据,需要其不可变以保证确定的输出
- 无副作用
三大原则
- 数据源单一,整个应用只创建一个store,其中只需要一个
state
对象树管理所有状态 - state只读,只能通过
action
修改 - 使用纯函数reducer联系action与state,可以将reducer拆分成多个小的reducers分别操作state tree的一部分
使用
createStore(reducer(state, action)=>{...})
创建store,其中reducer:必须是纯函数
将state与action结合返回新的state
建议使用
combineReducers
创建reducer函数,将不同逻辑的state和reducer拆分到对象不同属性,避免每次更新拷贝全局state,在action不更新时使用旧的state1
2
3
4export const reducer = combineReducers({
counter: countReducer,
age: ageReducer
})
通过
store.subscribe(callback)
订阅store的变更(subscribe
返回函数,调用即可取消订阅)- 一般在componentDidMount订阅,componentWillUnmount取消
- 在
subscribe
回调中通过store.getState()
拿到最新的状态并setState
更新组件
创建action对象(一般通过工厂函数返回,有不同是type属性标识不同的变更操作),通过
store.dispatch(action)
派发
4.2版本后createStore已弃用,官方建议迁移到Redux Toolkit
react-redux
将react和redux进一步关联,不需要在每个组件内都进行多余的订阅等操作
实现原理:
本质上通过函数封装高阶组件处理使用store的一系列操作以简化代码使用。通过Context value共享store,与业务代码分离。由于组件构造器内的context未初始化,需要从contstructor第二个参数(
context
)传入super第二个参数
暴露的工具
Provider
组件共享store,不用每个组件引入1
2
3
4
5
6
7
8
9
10
11import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
import store from './store'
import { Provider } from 'react-redux'
ReactDom.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)connect
连接redux与react组件,将state
和dispatch
映射到组件的props
,返回新组件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import { connect } from 'react-redux'
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
react-redux 也提供一些hooks
useDispatch
直接拿到dispatch派发actionuseSelector
设置并获取数据,不必再用connect、mapStateToProps注册connect
内会对mapStateToProps做浅比较,不依赖的数据变更不会重新渲染组件;而useSelector
默认是全等比较,函数组件每次创建新对象。需要使用
useSelector
第二个参数自定义比较函数,通常传入react-redux提供的shallowEqual
异步action
使用中间件在dispatch action
和reducer
之间插入异步请求等扩展操作
通过monkeying patch修改原因代码逻辑,封装patch函数,在应用中间件时重写store.dipatch
等原有api插入操作
redux-thunk
官方推荐,轻量、使用方便
原理是支持将action作为函数,在dispatch action
和reducer
之间执行,函数内可以进行异步操作后dispatch
真正的action
创建store时传入该中间件
1
2
3
4
5import { createStore, applyMiddleware } from 'redux';
import { countReducer } from './reducer';
import thunk from 'redux-thunk'
export default createStore(countReducer, applyMiddleware(thunk))使用函数作为action(原本是对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//actions.ts
export const asyncAction = (num: number) => ((dispath: Dispatch) => {
// 模拟异步操作
setTimeout(() => {
dispath(actionXX(num))
}, 2000)
})
//component.tsx
const mapStateToProps = (state: IState) => {
return {
xx: state.xx
}
}
const mapDispatchToProps = (dispatch: Dispatch, getState: any) => {
return {
// 为支持函数作为action:
// 不使用any需要用thunk提供的泛型:ThunkAction、ThunkMiddleware
// 参考https://juejin.cn/post/6844903965658710029
clickHandle: (num: number) => dispatch<any>(asyncAction(num))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Component)
缺点:不利于维护
异步操作过于分散在各个action
action形式不统一,有的返回函数有的返回对象
redux-saga
也支持异步action;特性更多,可以更清晰的拆分代码
使用了Generator特性:生成的迭代器每第二次调用next
传入的参数会作为上一次yiled
的返回值
建立连接,调用
run
方法,需要传入Generator函数1
2
3
4
5
6
7
8
9
10
11
12
13import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import mySaga from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(mySaga)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// mySaga.ts
import { call, put } from 'redux-saga/effects'
import { takeEvery } from 'redux-saga'
export function* fetchData(action) {
try {
const data = yield call(Api.fetchUser, action.payload.url);
yield put({type: "FETCH_SUCCEEDED", data});
} catch (error) {
yield put({type: "FETCH_FAILED", error});
}
}
export function* mySaga() {
yield takeEvery('FETCH_REQUESTED', fetchData)
}
redux-sage/effects有一系列供Generator函数内供yiled
调用的方法:
takeEvery(type, generatorCallback)
拦截对应type的action并执行生成器函数回调takeLatest(type, generatorCallback)
与takeEvery
相同,但一次只会执行最后一次action,类似节流put(action)
dispatch真正的action到reducerall([yield put(a1), ...])
执行数组中所有effects操作
注意: mySaga内takLatest、takeEvery等type不能与回调内put的type相等,否则回死循环
React-Router
react-router-dom提供如下组件
BrowserRouter/HashRouter
- 包含对路径改变的监听,并传递给子组件
- BrowserRouter - history模式
- HashRouter - hash模式
Link/NavLink
- 通常使用Link进行跳转,会被渲染为a标签
- NavLink在Link基础上增加了样式属性,便于修改激活样式
- to属性:设置跳转目标路径 可传路径string或object(其中
state
方便传递复杂参数,通过props.location.state获取
)
Route 用于路径的匹配,属性如下:
- path :设置匹配到的路径, 支持
:xx
设置动态路由参数,不设置path相当于*
全匹配 - component: 设置匹配到渲染的组件
- exact: 精准匹配,只有完全一致的路径才会渲染
Route渲染的组件内可以再使用Route进行嵌套添加子路由
通过Route渲染的组件会传入3个
prop
:- history(v6中
useNavigate
替代了useHistory
协助js跳转) - location 包含query、state等参数(v6通过
useSearchParams
、useLocation
) - match 包含param参数(v6通过
useParams
)
通过withRouter(cmp)创建的高阶组件,其中也会有这三个prop
需要先用
BrowserRouter/HashRouter
包裹才能用withRouter- path :设置匹配到的路径, 支持
Switch 将多个Route放到Switch种只会从上往下渲染匹配的第一个(排他)
Redirect 该组件渲染时,会跳转到
to
属性指定的路由
react-router-config
实现类似vue-router的集中路由管理
- 提供
renderRoutes(routes)
函数,返回高阶组件代替Switch
、Route
- routes可嵌套属性注册子路由,子路由组件通过
props.route
拿到当前父路由再通过renderRoutes(childRoutes)
渲染 - 提供辅助函数
matchRoutes
匹配获取指定的route信息
v6版本变动、hooks
react-router-dom v6版本有如下更新
- Switch替换成了Routes;
- Route去除exact;
- Route中统一使用element属性,去掉原来的component和render
- useNavigate取代useHistory;
- 可以省略path前的/
因此不兼容vue-router-config了,但通过useRoutes
本身已支持集中管理
V6舍弃了class组件的一些支持,需要使用hooks
Imutable
imutableJs解决数据可变性问题:
编码 - 对象引用类型赋值修改存在隐患,可读性差
性能 - 为解决可变性问题,需要频繁拷贝对象
设置state对象的一个属性,也需要拷贝整个对象
特点
只要修改对象,就会返回新对象,为节省性能产生了持久化数据结构算法 (新对象尽肯利用旧数据结构 - 结构共享)
API
- Map 浅层转换对象
- List 浅层转换数组
- fromJS 深层转换且支持嵌套及多种类型
- is 比较两个对象是否相等,通过
hashCode
与valueOf
对比,避免深度遍历 - im.get(key/index) 获取immutable数据的属性值
- im.getIn([key/index, …]) 获取嵌套immutable数据的属性值
- im.toJS 将immutable数据转为JS类型
reudx-imutable
提供combineReducers函数,将原始combineReducers以imutable的方式优化
diff
react diff对于虚拟dom的比较优化(相较于全量遍历):
- 只同级比较
- 不同节点类型产生不同树结构
key
指定节点在不同渲染下保持稳定- 默认情况下回同时遍历新旧树,发现差异时生成一个mutation,后续所有节点都会生成mutation,会造成一定性能问题
- 有key时,通过key比对新建节点,不会在有差异后全部都生成mutation,只会移动节点位置
- 因此 :
- 在数组尾部添加数据,key的意义不大
- key不能直接放
Math.random
随机数,每次render都会生成新的,没有意义 - 数组index会根据内容数量发生变化,对性能优化意义不大
- 节点元素不同时,直接拆卸原有的树创建新树。元素相同时保留节点并比对更新有变化的属性
性能优化
render渲染
默认情况下,组件的
state
发生改变重新render时,不论是否使用组件的state
作为prop
,下面的所有子组件都会重新render
,浪费性能
优化方法如下:
sholdComponentUpdate(newProps, newState)
在state
更新前触发,返回布尔值决定是否调用render
。判断时需要对比函数参数中的新数据与当前的state
,因此需要保持数据的的不可变性,否则无法判断变化- 类组件使用
react.PureComponent
代替react.Component
,内部会自动比对state
和props
更新决定是否render
- 函数式组件传入
memo
函数,返回的组件也可达到PureComponent
的效果注意
sholdComponentUpdate
中使用深层比较或JSON.stringify
会比较耗费性能
其他
snippet generator (snippet-generator.app)
生成各编辑器的代码片段配置
CRA配置
eject
一是将create-react-app隐藏的配置都暴露出来,包括别名配置
命令行执行:react-scripts eject
不建议直接eject方式修改配置,容易给项目带来负担; eject暴露后也没办法隐藏
craco
社区中对cra自定义配置的解决方案
npm i @craco/craco -D
/* package.json */ "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", + "start": "craco start", + "build": "craco build", + "test": "craco test", }
根目录创建craco.config.js进行配置
进行上述基本操作后即可开始配置: 如继续引入craco-less修改antd主题
单项数据流
redux、vux、react和vue组件内都推荐单向数据流利于维护,不直接在
子级修改上级的状态
- 本文作者: MR-QXJ
- 本文链接: https://mr-qxj.github.io/2023/03/23/框架/react/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!