React
原生js痛点:DOM-API操作UI 修改.style变更样式,每一次修改DOM都会引起浏览器重绘重排,缺少组件化编码方案,代码复用率低
组件化:构成局部功能的元素,比如图片、js、css...
模块化:按照功能点,单个模块有自己独立的业务逻辑
React特点
- 组件化模式,声明式编码,只需要维护自己的状态,提高组件复用率
- React Native中可使用React语法进行移动端开发
- 虚拟DOM和DIffing算法,减少与真实DOM交互,最小化页面重绘
- 推崇
all in js
, 认为UI和js逻辑存在很强的耦合度, 所以一个组件里包含UI和js
jsx
首先是js的一种语法扩展,js extention
使用jsx语法,jsx描述UI的样子,通过babel转成js
以state存储组件内状态 setState({})进行修改
render方法中需要return出html代码, 如果是单标签需要使用/
结尾
应用状态发生改变时,通过setState修改状态,状态发生改变UI会自动更新
jsx嵌入数据
1.在{}
中可以正常显示的数据类型: String/Number/
2.在{}
中不能显示(忽略): null/undefined/Boolean
如果想在页面中显示的话: Boolean.toString() / String(null) // undefined + "" 三种方法
3.对象不能作为jsx的子类
jsx添加属性和类名
1.添加属性:可以用函数对属性进行处理,很灵活
2.添加类名:注意不能与关键字重复:class-->className; label中的for-->htmlFor
3.绑定style样式: style={{fontSize:'16px',prop:{propName}}}
4.绑定点击事件: 使用bind
改变this; 使用箭头函数定义方法; 调用方法时使用箭头函数
推荐使用第三种方法
添加动态的class
:
vue
中相对简单,传入一个对象即可
<div class="static" v-bind:class="{active:isActive,'text-danger':hasError}">
<div v-bind:class="[activeClass,errorClass]">
React
可以按照js
的逻辑运算进行某些类名是否需要添加, 可以使用classNames
第三方库,使用和vue
类似
条件渲染
vue中使用的是v-if
,v-show
react中用的就是传统的js逻辑判断
; 三元运算符; 逻辑与&&
v-show
的实现即在标签中通过style标签
控制
render(){
let welcome = null
if(this.state.isLogin){
prop = <h2>登录了</h2>
btnText = '退出'
}else{
prop = <h2>没有登录</h2>
btnText = '登录'
}
return (
<div>
{welcome}
<button>{btnText}</button>
</div>
)
}
列表渲染
需要用js
代码实现v-for
的指令
1.使用map
这样的高阶函数
本质
实际上jsx是React.createElement的语法糖 通过babel进行的转换 最终创建出一个ReactElement对象树(类似于snappdom的转换)
再经过ReactDOM.render变成虚拟DOM, 最后转变成真实DOM
为什么使用虚拟DOM?
虚拟DOM帮助我们从
命令式编程
转到声明式编程
- UI可以以虚拟化方式保存在内存中
- 可以通过ReactDOM.render让虚拟DOM和真实DOM同步起来
- 只需要告诉React希望UI是什么状态(即进行最新的
render
操作), 不需要进行DOM操作(这里实际上就是vue的双向绑定的手动调用)难以跟踪状态发生的改变,不方便调试
操作真实DOM性能低
- document.createElement本身创建出的对象就很复杂
- DOM操作会引起浏览器回流和重绘
react脚手架
create-react-app
就是react的脚手架
生成一个通用的目录结构, 并且已经将我们需要的工程环境配置好
脚手架都是使用node
编写的, 都是基于webpack
的
react脚手架默认使用的是yarn
包管理工具
创建项目: create-reate-app my-project
react组件化开发
react组件相对于vue更加灵活
按照组件定义方式: 函数组件和类组件
根据组件内部是否有状态维护: 无状态组件和有状态组件
根据组件不同职责: 展示型组件和容器型组件
这些概念主要关注数据逻辑和UI展示的分离: 以上前者关注UI展示,后者主要关注数据逻辑
还有异步组件,高阶组件
类组件
组件名称是大写的
类组件需要继承React.Component
类组件必须实现render函数
函数式组件
需要return出react元素
, 或者说jsx
创建的元素
组件的嵌套
被嵌套的子组件可以函数组件
,也可以是类组件
传递的属性是通过参数props
进行传递
constructor(props){
super() //在contructor里面props是undefined,但是在后面的生命周期还是可以访问到的
}
constructor(props){
super(props)
}
//constructor(props){
// super(props)
//}
三种方式效果一致
受控组件
一般是处理HTML
表单: 表单元素的数据会保存在内部的state
, 通过监听事件调用setState
更新state
,这样state
的数据就会是最新数据且是唯一数据源
非受控组件
采用直接操作DOM的方法操作表单内的元素, 使用ref
去获取到对应的值,这种方式不推荐!
组件通信
父传子: 子组件上传递属性即可
子传父: 父组件传过去的方法在子组件里调用即可
插槽: 直接当做变量值传到子组件
跨组件通信
- 即非父子组件数据的共享. 可以通过
props
属性自上而下传递,可以使用Spread Attributes
运算符简化传递操作 - 最好使用
Context
, 不必显式的通过组件树逐层传递props
,如下方代码所示
React.createContext(): 创建一个需要共享的Context对象
Context.provider: 是一个React组件,允许消费组件订阅context的变化,一旦Provider的值发生改变,子代组件都会更新
Class.contextType: 挂载在class上的contextType属性会被重赋值为React.createContext()创建的context对象; 可以使用this.context消费最近Context上的那个值,可以在任何生命周期访问到它,包括render函数
//App爷爷组件先定义
import React, { Component } from 'react'
import TabControl from './TabControl'
//可以传递默认值,当SonContext.Provider的value没有传递值得时候可以使用设置的默认值
export const SonContext = React.createContext()
export default class App extends Component {
constructor() {
super()
this.state = {
nickname: 'wwww',
level: 99,
GrandMessage: '你好,跨了三层的孙子组件!',
}
}
render() {
return (
<div>
<SonContext.Provider
value={[
this.state.nickname,
this.state.level,
this.state.GrandMessage,
]}
>
<TabControl></TabControl>
</SonContext.Provider>
</div>
)
}
}
//1.孙子的类组件使用
import React, { Component } from 'react'
import { SonContext } from './newProduct'
export default class GrandSon extends Component {
render() {
GrandSon.contextType = SonContext
console.log(this.context)
return (
<div>
<h3>昵称: {this.context[0]}</h3>
<h3>等级: {this.context[1]}</h3>
<div>爷爷传来的信息: {this.context[2]}</div>
</div>
)
}
}
//2.孙子的函数式组件
function GrandSon(){
return(
<SonContext.Consumer>
{
value=>{
return(
<div>
{value.nickname}
</div>
)
}
}
</SonContext.Consumer>
)
}
-
开发时通常使用
events
库,yarn add events
//eventBus.js文件 import { EventEmitter } from 'events' export const eventBus = new EventEmitter() //兄弟1组件 import React, { Component } from 'react' import { eventBus } from '../utils/eventBus' export default class FluentProduct extends PureComponent { render() { return ( <div> <button onClick={(e) => this.emmitEvent()}> 向新品兄弟页面发射事件 </button> </div> ) } emmitEvent() { eventBus.emit('sayHello', 'hello react!', '123', 456) } } //兄弟2组件 import React, { Component } from 'react' import { eventBus } from '../utils/eventBus' export default class NewProduct extends PureComponent { componentDidMount() { eventBus.addListener('sayHello', this.handleSayHelloListener) } componentWillUnmount() { //指定移除单一函数的监听 eventBus.removeListener('sayHello', this.handleSayHelloListener) } handleSayHelloListener(message, str, num) { console.log(message, str, num) //接收传来的值 } }
高阶组件
高阶函数
接受一个或多个函数传入, 或者输出一个函数,例如 map
,filter
,reduce
函数
类高阶组件
一开始React
使用的是mixin
混入, 类组件无法使用混入, 所有使用高阶组件解决代码复用的问题
函数高阶组件
简称为HOC
, 以组件作为参数, 且返回值为新组件的函数
高阶组件并不是react API
的一部分, 是基于React的组合特性而形成的设计模式, 主要实现对组件进行劫持, 后续也会被hooks
代替
在一些React
第三方库中非常常见:
redux
的connect
react-router
的withRouter
渲染判断鉴权
利用高阶组件进行进入某个页面前的Auth
权限判断, 根据Auth
值返回不同的页面
生命周期劫持
高阶组件的生命周期函数中进行操作, 一般也是对组件进行相同的操作
setState
1.setState
是异步更新的, 为什么?
-
设计为异步,可以显著提升性能
-
如果每次调用
setState
都更新一次,那么render
函数会被频繁调用,页面重新渲染最好的方法就是获取到多个更新,之后实现批量数据在一次
render
更新
-
-
如果同步更新了
state
,但是还没有执行render
函数,则不能保持数据和视图的一致性
想要拿到最新的数据的话,需要传入第二个参数,在回调函数中获取的值就是最新的,类似于vue
中的nextTick
函数,在更改了一些数据等到DOM更新之后使用它
或者在componentDidUpdate
生命周期中拿到异步更新后的state
数据
2.什么情况下setState
是同步的?
- 在
setTimeout
中调用setState - 在
componentDidMount
生命周期中使用原生DOM
操作进行监听事件
3.setState
的合并?
数据的合并:
Object.assign({},this.state,{message:"你好,react"})
本身的合并:
//传入对象,会执行最后一次的代码
this.setState({
counter: this.state.counter + 1,
})
this.setState({
counter: this.state.counter + 2,
})
//传入函数,会进行两次的累加,第二次的prevState就是第一次调用setState之后的值
this.setState((prevState,props)=>{
return{
counter:prevState.counter+1
}
})
this.setState((prevState,props)=>{
return{
counter:prevState.counter+2
}
})
4.setState
传递的数据应该是不可变数据?
开发中, 不要直接修改state
的数据, 因为shouldComponentUpdate
生命周期中会对新老state
进行对比看是否相等, 如果直接修改state
后再调用setState
赋值,实际上指针指向的是同一个内存地址, 也就是新老state
并没有发生改变, 所以不会触发render
更新页面
react更新机制
渲染机制
简单来说: jsx
-->虚拟DOM-->真实DOM
更新机制
-
props/state
改变 -
render
函数重新执行-
当
App
组件或其他父组件重新render
时, 默认所有的子组件的render
都会被重新调用,非常浪费性能,一般在页面不显示某个属性而只是作为内部的隐藏依赖时 -
shouldComponentUpdate
生命周期函数默认是返回true
的, 即当组件的setState
调用之后就会触发render
函数,可以设置该生命周期返回值为false
来阻断render
-
类组件可以继承自
PureComponent
就会达到shouldComponentUpdate
生命周期一样的效果, 会对state
的数据进行shallowDiff
浅层比较 -
函数式组件需要使用
memo
包裹函数式组件, 会返回一个新的被处理的组件const newComp = memo( function(){ return( <div>被处理后的memo组件</div> ) } )
-
-
产生新的DOM树
-
新旧DOM树进行
diff
-
同层节点比较
-
不同类型节点, 产生不同树结构
-
开发中, 通过key指定哪些节点在不同渲染下保持稳定,即进行相同节点的复用
- key必须唯一
- 不能使用随机数
- 使用index作为key,对性能没有优化
-
-
计算出差异进行更新
-
更新到真实的DOM节点
ref操作DOM
首先明确一点: jsx
上的ref
不会被当做props
传递给下一个组件! react
内部进行管理
一般是不需要直接操作DOM原生,但是有一些情况需要操作DOM:
- 类组件:
import React, { Component } from 'react'
import GrandSon from './GrandSon'
export default class FluentProduct extends Component {
constructor() {
super()
this.titleRef = React.createRef()
this.compRef = React.createRef()
this.titleEl = null
}
render() {
return (
<div>
<button onClick={(e) => this.emmitEvent()}>
向新品兄弟页面发射事件
</button>
{/**1.传入对象 */}
<h2 ref={this.titleRef}>hello world!</h2>
{/**2.传入函数 */}
<h2 ref={(arg) => (this.titleEl = arg)}>hello world!</h2>
{/**3.操作组件内方法 */}
<GrandSon ref={this.compRef}/>
<button onClick={(e) => this.changeText()}>
使用ref操作dom改变h2文字
</button>
</div>
)
}
emmitEvent() {
eventBus.emit('sayHello', 'hello react!', '123', 456)
}
changeText() {
this.titleRef.current.innerHTML = 'hello react!'
this.titleEl.innerHTML = 'hello react+ts!'
this.compRef.current.xxx方法调用
}
}
-
函数式组件使用ref
函数式组件是没有
this
的, 没法直接使用ref
,使用另一个高阶组件forwardRef
需要使用
React.forwardRef
的API, 对组件中的某个元素标记ref
会在
hooks
中经常使用
Portals
某些情况下,希望渲染的内容独立于父组件,甚至独立于当前挂载的DOM元素,Portal
提供了一种将子节点渲染到存在于父节点以外的DOM节点的优秀方案
ReactDOM.createPortal(child,container)
//child是任意可渲染的react子元素
//container是一个DOM元素
类似于Vue
中的teleport
Fragment
代替组件div
的父元素, 减少层级嵌套
css in js
简单来说就是react
中的一种css
使用的解决方案, 通常使用styled-components
库,具有props
穿透特性,attrs
特性自定义属性,传入state
中变量作为props
属性
- 首先, 要知道
es6
中调用函数的一种方式fn``aa,bb可以作为参数, 类似于fn(aa,bb) - 可以使用
styled.div.attrs({type: 'password'})
定义一些标准或非标准的属性在``语法中使用 - 可以使用组件中
state
定义的变量值传递到styled.div.'color:(props)=>${props.bColor}'
中 - 还可以进行组件间样式的继承:
styled(XX组件样式)''
- 还可以设置主题
<themeProvider theme={{color:'red'}}/>
AntDesign
作为react
的组件库使用, icon
突变使用需要独立引用icons库
其他使用与element
类似
craco
对主题等相关的高级特性进行配置, 就是运行yarn run eject
暴露出的配置信息, 还能配置别名
修改脚本, 创建craco.config.js
文件
纯函数
-
函数相同的输入值, 必须产生相同输出. 和函数的输出和输入值以外的其他隐藏信息或状态无关, 也和I/O设备产生的外部函数无关
-
不能有语义上可观察的函数副作用, 例如'触发事件', 使输出设备输出或者更改输出值以外物件的内容等
即确定输入一定产生确定输出, 函数执行过程不能产生副作用
同样的, React
组件都必须像纯函数一样保护它们的props
不被修改
react动画
使用react-transition-group
插件
生命周期
-
装载阶段
Mount
, 组件第一次在DOM树中被渲染的过程 -
更新阶段
Update
, 组件状态发生变化, 重新更新渲染的过程 -
卸载阶段
Unmount
, 组件从DOM树被移除的过程 -
可以在类组件中进行
生命周期函数
的调用, 函数式组件需要借助hooks进行模拟
-
componentDidMount
类似于vue的onMount阶段,在此处通常进行依赖DOM操作的调用,发送网络请求,或者在此处添加一些订阅,然后在componentWillUnmount阶段取消订阅
-
componentDidUpdate
在更新后立即调用,首次渲染不会调用 当组件更新后,在此处对DOM进行操作 如果对更新前后的props进行比较,可以选择在此处进行网络请求(例如,当props一旦发生变化,就执行网络请求)
-
componentWillUnmount
执行必要的清理操作 例如清除timer,取消网络请求,清除之前componentDidMount中创建的订阅
-
核心库和周边库
核心库:react.development
周边库:react-dom.development,针对web和移动端完成的事情不同
- web端:react-dom将jsx最终渲染成真实DOM,显示在浏览器
- native端:将jsx渲染成原生控件,比如Android的button,ios的button
Redux
简单来说, 就是对react
中的state
状态数据进行管理
react
本身的的props
和context
是难以控制和追踪的, 可以将状态state
放在一个容器中进行管理, redux
就是这个容器, 提供了可预测的状态管理,类似于vuex
和pinia
想要修改数据必须通过派发(dispatch
)action
来更新,action
是一个普通的js对象, 用来描述这次更新的type
和content
, 通过reducer
纯函数处理action
请求最后返回新的state
三大原则:
-
单一数据源
- 整个应用程序的数据被存储在一棵
object tree
中, 并且这棵树只存在一个store
- 单一数据源可以让整个应用程序的
state
变得方便维护,追踪,修改
- 整个应用程序的数据被存储在一棵
-
State是只读的
- 唯一修改State方法一定是触发action !
- 保证所有修改都被集中化处理, 并按照严格顺序执行, 不用担心
race condition
竟态的问题
-
使用纯函数执行修改
- 使用reducer将旧的state和actions联系在一起, 返回一个新的State
- 可以将reducer拆分成多个小的reducers, 但是必须还要保证是纯函数
使用流程:
- 组件实例在
componentDidMount
对Store进行订阅 - 组件实例在派发Store的提前定义好的action
- Store会根据Reducer接收到的action的type更新State
- State更新会trigger组件实例对Store的订阅...
react-redux
作为将react和redux连接的第三方库进行使用
一般来说,网络请求的数据也属于状态管理的一部分,应该也交给redux进行管理,即在派发actions时发送异步网络请求,组件仅仅负责在生命周期中调用映射的方法
怎么在redux进行异步操作?
使用中间件(Middleware)!
可以帮助我们在请求和响应之间嵌入一些操作的代码
redux-thunk
react官方推荐的进行代码扩展的中间件
做的最重要的事情就是action可以作为函数传入, 在内部会对传入的函数进行调用, 一般就会把异步请求等操作写在这个函数里, 调用函数后拿到返回值, 再根据type类型dispatch对返回的值进行映射
redux-saga
其实是redux-thunk的替代中间件,相比起来saga更加灵活,使用了ES6的generator语法
React-Router
路由,和vue类似,重点关注动态路由和嵌套路由
Hooks
多个Hook组成Hooks
Hook是16版本的新特性,可以在不编写class的情况下使用state及其他的React特性(比如生命周期)
class的优势:
- state可以保存组件内部的状态
- class组件有自己的生命周期
- class在状态改变时只会重新执行render函数及我们希望重新调用的生命周期函数componentDidUpdate等
class的问题:
- 生命周期中可能会写大量代码,代码复杂的情况下是难以进行拆分到多个组件的, 因为他们的逻辑往往混在一起
- this指向的复杂性
- 组件复用状态很难, 必须使用高阶组件或者render props,会造成代码存在很多嵌套
Hook
Hook只能在函数组件中使用,不能在类组件或者函数组件之外的地方使用,不能在普通函数使用
Hook指的类似于useState,useEffect这样的函数, Hooks是对这类函数的统称
只能在函数最外层调用Hook,不能在循环中调用
只能在React函数组件中调用Hook,不能在其他js函数中调用
useState
和class组件的state类似,进行组件内部变量的管理
useReducer是对useState复杂逻辑情况的替代,并不是redux中的状态共享方案
useEffect
引出生命周期的钩子函数
useContext
一般用于主题共享
useCallback
主要是为了传递函数性能的优化
会返回一个函数的memoized(记忆的)值, 在依赖不变的情况下,多次定义的时候返回的值是相同的
一般用于将一个组件中的函数,传递给子元素进行回调使用时,使用useCallback对函数进行处理
useMemo
也是为了进行复杂计算重复调用时性能的优化
返回的也是一个memoized(记忆的)值, 在依赖不变的情况下,多次定义的时候返回的值是相同的
useRef
返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变
正常情况下ref引用是用在class组件上, 函数组件使用ref进行引用会报错
useImperativeHandle
通常和forwardRef对ref标记函数式组件进行转发时连用, react认为传统的forward转发ref的模式下子组件暴露的太多了, 使用此API进行点对点导出
const HYInput = forwardRef((props,ref)=>{
useImperativeHandle(ref,()=>({
focus:()=>{
//一系列操作
}
}))
return <input ref={ref} type='text' />
})
useLayoutEffect
useEffect会在渲染的内容更新到DOM之后执行,不会阻塞DOM的更新
useLayoutEffect会在渲染内容更新到DOM之前执行, 会阻塞DOM更新, 有可能会造成视觉阻塞
自定义Hook
本质上只是一种函数代码逻辑的抽取, 严格意义上并不算React的特性
需要使用useFn()的形式, use开头,可以在函数内使用hook的API
Fiber
电脑屏幕一个参数: 刷新率 requestAnimationFrame/ js代码执行 / 键盘事件响应 这些操作最好是在一帧中做完, Fiber就是处理这些事件的API