React-学习总结
1、学习一个框架的目的,在于使用它产出价值
如何熟练的使用,是产出多少价值的关键
2、框架的使用,有其自己的一套规范,那么我们如果熟知规范和使用方法
那么学习框架的目的你就达到了
备注:至于使用过程中,遇到框架本身的问题,那么就需要研究其源码了,这里我们暂且不讨论
react 优化与注意事项
框架有其规则,对于经验丰富的老手来说,上手并不难,难得是各种坑
这里我记录下react的使用方法,以及各种注意点
react 渲染
- 使用 ReactDOM渲染
- 可以使用 js 作为文件后缀,不必要求 jsx 后缀
- 因为 create-react-app 脚手架做了配置
- 其内部使用的 webpack 打包,配置了 babel-preset-jsx 用来转换 jsx 语法
// App.js
import ReactDOM from 'react-dom'
import React, { Component } from 'react'
export default class App extends Component {
render() {
return (
<div>App comp</div>
)
}
}
ReactDOM.render(<App/>, document.querySelector('#root'), () => {
console.log('渲染完成')
})
jsx 语法
// App.js
import ReactDOM from 'react-dom'
import React, { Component } from 'react'
// 约定子组件首字母大写,方便区分原生标签,查找组件
import SonComp from './component/SonComp'
export default class App extends Component {
render() {
const message = 'message'
const dangerHtml = '<h1>标题</h1>'
const visiable = true
const arr = [1, 2, 3]
const obj = {
a: 1,
b: 2,
c: 3
}
return (
<div>
// 渲染 App comp 字符串
<div>App comp</div>
// 渲染变量 message
<div>{ message }</div>
jsx 支持在双大括号{}里书写 js 语法逻辑
<div>{ message === '' ? '空' : message }</div>
{ visiable && <SonComp /> }
<div>{ message.split('') }</div>
// 渲染数组
<ul>
{
arr.map((item, index) => {
// 这里key本不该用 index
// 需要使用业务的唯一值
// react 用来做复用,做性能优化
return <li key={index}>{item}</li>
})
}
</ul>
// 渲染对象
<ul>
{
(() => {
let tagArr = []
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
tagStr.push(<li key={key}>{obj[key]}</li>)
}
}
return tagArr
})()
}
</ul>
// 设置类名与添加类名
<div className={`wrapper${ visiable ? 'visiable' : ''}`}>App comp</div>
// 设置样式,第一层{}表示要解析当前字符串
// 第二层表示对象
<div style={{ width: '100px', height: '100px', border: '1px solid black' }}>
App comp
</div>
// 相当于 vue 的 v-html
<div __html={dangerHtml}></div>
<div dangerousSetInnerHTML={{__html: dangerHtml}}></div>
// 子组件
<SonComp />
</div>
)
}
}
ReactDOM.render(<App/>, document.querySelector('#root'))
事件绑定
- 在 react 中,event 事件对象是 react 自身封装的合成对象
- 而不是 DOM 的原生 js 事件对象
- 可以通过 event.nativeEvent 拿到原生事件对象
- react 中,将事件挂在 document 上,通过事件委托触发
- 可对比 event.currentTarget(这里为:当前触发元素)、event.nativeEvent.currentTarget(这里为:document)
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
this.btnActionBind = this.btnAction4.bind(this)
}
render() {
return (
<div onClick={this.btnAction1}>App comp</div>
// 每次 render,都会bind一次,可以提前到 constructor 中存储
<div onClick={this.btnAction2.bind(this)}>App comp</div>
<div onClick={this.btnAction3(1, 2)}>App comp</div>
<div onClick={this.btnAction5(1, 2)}>App comp</div>
<div onClick={this.btnActionBind}>App comp</div>
<div onClick={(ev) => {
// 不推荐这么写,因为 render 每次执行,这里都会重新创建函数以及其作用域,没必要
console.log('执行了 :>> ')
// 该this为当前组件上下文
console.log('this :>> ', this)
}}>App comp</div>
)
}
btnAction1(ev) {
// 该this为undefined
console.log('btnAction1>this :>> ', this) // undefined
}
btnAction2(ev) {
// 该this为当前组件上下文
console.log('btnAction1>this :>> ', this) // 当前组件
}
btnAction3 = (param1, param2, ev) => {
// 传参数时,事件对象会被放置在最后一个
console.log('param1 :>> ', param1) // 1
console.log('param2 :>> ', param2) // 2
console.log('ev :>> ', ev)
// 箭头函数绑定this为当前组件上下文
console.log('btnAction1>this :>> ', this) // 当前组件
// react 合成事件
console.log('ev :>> ', ev)
console.log('ev :>> ', ev.target) // 绑定事件元素
console.log('ev :>> ', ev.currentTarget) // 触发元素
// 原生事件
// 绑定事件元素,点击完成后 nativeEvent 就会被释放掉,设置为 null
console.log('ev :>> ', ev.nativeEvent.target)
// 点击触发元素,通过事件委托在 document 上,冒泡触发事件
console.log('ev :>> ', ev.nativeEvent.currentTarget)
}
btnAction5 = (param1, param2) => (ev)=> {
console.log('param1 :>> ', param1) // 1
console.log('param2 :>> ', param2) // 2
console.log('ev :>> ', ev)
}
btnAction4(ev) {
// 使用 bind 绑定this为当前组件上下文
console.log('btnAction1>this :>> ', this) // 当前组件
}
}
react 属性类型校验
- PropTypes 设置组件的数据属性
- 外部传入或者内部设置的时候进行校验
// App.js
import ReactDOM from 'react-dom'
import React, { Component } from 'react'
import SonComp from './component/SonComp'
export default class App extends Component {
render() {
return (
<div>
<SonComp plus={this.handlePlus}/>
</div>
)
}
handlePlus = () => {
console.log('plus :>> ', 1)
}
}
ReactDOM.render(<App/>, document.querySelector('#root'))
// ****************************
// SonComp.js
import React, { Component } from 'react'
import types from 'prop-types'
export default class SonComp extends Component {
render() {
return (
<div>son comp</div>
)
}
}
// 设置可接受的数据类型,若不通过校验,则会报错
SonComp.propTypes = {
plus: types.func.isRequired
}
父子组件通讯
- 父组件在子组件上直接设置值,子组件 props 接收
- 父组件传递一个函数给子组件,子组件调用函数并传递数据给父组件
// App.js
import ReactDOM from 'react-dom'
import React, { Component } from 'react'
import SonComp from './component/SonComp'
export default class App extends Component {
render() {
return (
<div>
<SonComp handleAccpet={this.acceptSonData}/>
</div>
)
}
acceptSonData = (sonData) => {
console.log('接收到子组件数据 :>> ', sonData)
}
}
ReactDOM.render(<App/>, document.querySelector('#root'))
// ****************************
// SonComp.js
import React, { Component } from 'react'
export default class SonComp extends Component {
render() {
return (
<div onClick={this.handleSend}>son comp</div>
)
}
handleSend = () => {
this.props.handleAccpet({ message: '这是子组件传递的数据' })
}
}
保持 react 数据不可变原则
- 不可变数据只需要一次浅比较就可以确定整棵子树是否需要更新
- merge 视情况可能需要遍历组件树到比较深的层级,在 state 复杂的时候不可变数据会有性能提升
- 其他好处还包括方便做快照,方便 debug 等等
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
super()
this.state = {
msg: 'msg',
count: 1,
obj: {
x: 1,
y: 2
},
arr: [1,2,3]
}
}
render() {
return (
<div onClick={this.handleClick}>App</div>
)
}
handleClick = () => {
// 修改 state 数据,但是要依据数据不可变原则
this.setState({
msg: 'other msg',
count: this.state.count + 1,
// count: ++this.state.count // 这个违反了不可变原则, ++ 操作直接修改了 count
obj: {...this.state.obj, y: 3},
obj: Object.assign({}, this.state.obj, { y: 3}),
// 增
arr: [...this.state.arr, 4]
arr: this.state.arr.concat(4)
// 删
arr: this.state.arr.filter((item, index) => index !== 1)
// 改
arr: this.state.arr.map((item, index) => index === 1 ? '1' : '0')
})
}
}
react 数据修改的异步与同步
- react 自身事件中修改数据为异步更新
- setTimeout、自定义dom事件为同步更新数据
- setState 第一参数为对象,更新前会进行合并
- setState 第一参数为返回,更新前不会合并
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
super()
this.state = {
count: 0
}
}
componentDidMount() {
document.querySelector('#btn').addEventListener('click', () =>{
console.log('修改数据 :>> ')
// 同步更新
this.setState({
count: this.state.count + 1
})
// 上面为同步更新,所以这里得到 + 1 之后的值为 1
console.log('this.state.count :>> ', this.state.count)
})
}
render() {
return (
<div>{ this.state.count }</div>
<div onClick={this.handleClick}>点击 + 1</div>
<div onClick={this.handleMultiClick}>多次修改</div>
<div id="btn">点击 + 1</div>
)
}
handleClick = () => {
// 异步更新
// 点击一次
this.setState({
// 这里为异步更新
count: this.state.count + 1
})
// 上面为异步更新,所以这里先执行,得到 + 1 之前的值为 0
console.log('this.state.count :>> ', this.state.count)
// *********************************
// 同步更新
// 点击一次
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
// 上面为同步更新,所以这里得到 + 1 之后的值为 1
console.log('this.state.count :>> ', this.state.count)
}, 0);
}
// 多次异步 setState ,更新前这些操作将会被合并
handleMultiClick = () => {
this.setState({
// 这里为异步更新,多个setState操作将被合并,等价于
// count: 0 + 1
count: this.state.count + 1
}, () => {
// 修改后的值,相当于 vue 中的 this.$nextTick
// 全部更新后的回调执行,得到结果 1
console.log('this.state.count1 :>> ', this.state.count)
})
this.setState({
// 这里为异步更新,多个setState操作将被合并,此时 this.state.count 并未变化,等价于
// count: 0 + 1
count: this.state.count + 1
}, () => {
// 修改后的值,相当于 vue 中的 this.$nextTick
// 全部更新后的回调执行,得到结果 1
console.log('this.state.count2 :>> ', this.state.count)
})
this.setState({
// 这里为异步更新,多个setState操作将被合并,此时 this.state.count 并未变化,等价于
// count: 0 + 1
count: this.state.count + 1
}, () => {
// 修改后的值,相当于 vue 中的 this.$nextTick
// 全部更新后的回调执行,得到结果 1
console.log('this.state.count3 :>> ', this.state.count)
})
// 这里同步输出为 0
console.log('this.state.count4 :>> ', this.state.count)
}
// 多次异步 setState ,阻止更新前合并
handleMultiClick = () => {
this.setState((preState, props) => {
// count: 0 + 1
count: preState + 1
}, () => {
// 全部更新后的回调执行,得到结果 3
console.log('this.state.count1 :>> ', this.state.count)
})
this.setState((preState, props) => {
// count: 1 + 1
count: preState + 1
}, () => {
// 全部更新后的回调执行,得到结果 3
console.log('this.state.count2 :>> ', this.state.count)
})
this.setState((preState, props) => {
// count: 2 + 1
count: preState + 1
}, () => {
// 全部更新后的回调执行,得到结果 3
console.log('this.state.count3 :>> ', this.state.count)
})
// 这里同步输出为 0
console.log('this.state.count4 :>> ', this.state.count)
}
}
react 生命周期
挂载
- constructor()
- 初始化组件、设置 state
- static getDerivedStateFromProps(state, props)
- 相当于 vue computed
- 获取计算后的 state
- 返回的数据会被合并到 state
- render()
- 提供组件结构
- componentDidMount()
- 组件挂载之后调用
- 可挂载后请求数据,操作 dom
- 添加事件监听
更新
- static getDerivedStateFromProps()
- 更新阶段重新计算 state
- shouldComponentUpdate()
- 根据返回值确实是否需要更新组件
- render()
- 计算最小变更,提供新的组件结构
- getSnapshotBeforeUpdate()
- 获得更新前的快照
- 操作的为更新前的dom
- componentDidUpdate()
- 更新完毕调用
销毁
- componentWillUnmount()
- 移除事件监听等
使用 JSX 语法时候需要引入 React
因为 JSX 编译之后其实会变成 React.createElement()
import React, { Component } from 'react'
export default class App extends Component {
render() {
return (
<div>App comp</div>
)
}
}
// babel-preset-jsx 转换后
export default class App extends Component {
render() {
return React.createElement('div', 'App comp')
}
}
函数式组件
- 2.1. 会接收两个参数,一个是父级传递的 props,一个是 context 上下文,该上下文可能是 Provider 提供的
- 2.2. 只能操作 props,没有自己的 state,所以称之为无状态组件,也称之为UI组件
const Comp = (props, context) => {
return (
<div>fun comp</div>
)
}
export default Comp
ref 设置
- react 中不推荐设置字符串形式的 ref,因为每次 render 之后,都会重新设置 ref
- 而 ref 没有释放会导致内存泄露
- 16.0之后,使用 createRef 来解决
- ref 给普通标签设置,获取当前普通标签
- ref 给组件设置,获取当前组件对象
设置 ref 的三种方式
import React, { createRef } from 'react'
constructor() {
// 第三种
this.refName = createRef()
}
return (
// 第一种、不推荐
<div ref="refName"></div>
// 第二种、函数形式,即可在当前组件使用 this.refName 获取当前元素,element 即是当前元素
<div ref={(element) => { this.refName = element}}></div>
// 第三种、将ref 设置为组件变量 this.refName
// 而这个 this.refName = createRef() 其值为: { current: element }
// 取值 this.refName.current === element
<div ref={this.refName}"></div>
)
ref 转发
- ref 设置在子组件上,但是该 ref 可以不是获取当前组件对象
- 可以子组件内部自己设置 ref 所指定的元素或者对象
import React, { createRef } from 'react'
constructor() {
// 第三种
this.refName = createRef()
}
componentDidMount() {
console.log('refName', this.refName) // 这里为 SonComp 中的某个元素
}
return (
<SonComp ref={this.refName} />
)
// ********************************
// SonComp.js
// 在 SonComp 中,将 ref 转发到自身组件的某个元素上
// 这样,父组件即可获得自身组件的某个元素
// 函数式组件接收 ref
export default React.forwardRef((props, ref) => {
return (
<div ref={ref}></div>
// 或者
<OtherComp ref={ref}/>
)
})
// 类组件接收 ref
export default React.forwardRef((props, ref) => {
class AcceptComp extends Component {
render() {
return (
<div ref={ref}></div>
// 或者
<OtherComp ref={ref}/>
)
}
}
return <AcceptComp />
})
页面内部的子组件脱离父组件渲染
- 子组件脱离父组件在父组件外部渲染
- 但是子组件依然可以使用父组件的数组,可以获取父组件的上下文
// 1、html 上有 id 为 model-root 的 div 元素
// 2、Model 渲染时候可以在 model-root 内部渲染
// 3、Model 可以使用当前组件上下文和数据
export default class App extends Component {
constructor() {
this.state = {
x: 1
}
}
render() {
return (
<div>
// 这里 Model 不会渲染在这个位置
// 而是渲染在外部指定的元素标签底下
<Model data={this.state} />
</div>
)
}
}
// ***********************
// Model.js
const modelRoot = document.querySelector('#model-root')
export default class Model extends Component {
constructor(props) {
super(props)
this.el = document.createElement('div')
}
componentDidMount() {
modelRoot.appendChild(this.el)
}
componentWillUnmount() {
modelRoot.removeChild(this.el)
}
render() {
// 传送门,将 Model 组件的内容 this.props.children 放在新创建的 div 标签中, 即 this.el
// 而 this.el 在组件挂载的时候,已经被 appendChild 到指定的外部元素中了
// 所以当前整个组件都在外部了,父组件不会渲染当前组件进行占位
return ReactDOM.cratePortal(
this.props.children,
this.el
)
}
}
组件间通讯
- 定义共享上下文 StoreContext
- 使用 react 提供的 createContext 创建一个共享上下文 StoreContext
- 使用该上下文 StoreContext.Provider 包裹一个父组件
- createContext 上的数据变化会导致其包裹的组件都重新渲染,所以建议保存不经常变化的数据,如背景色、主题风格、国际化等
- 经常变化的数据建议使用 redux 来管理共享数据
// StoreContext.js
import { createContext } from 'react'
const storeContext = createContext(这里可以设置初始值)
export default storeContext
// **********************************
// App.js
import StoreContext from './context/StoreContext'
export default class App extends Component {
render() {
return (
// 提供数据
<StoreContext.Provider data={x: 1}>
<div>
<SonComp/>
</div>
</StoreContext.Provider>
)
}
}
- 第一种方式
- 使用子组件.contextType = StoreContext ,就可以从 this.context 获取数据
// SonComp.js
// 第一种 contextType(推荐类组件使用,不用使用 Consumer 冗长)
import StoreContext from './context/StoreContext'
export default class SonComp extends Component {
constructor(props, context) {
super()
// 这个 context 即是 StoreContext
// 当前组件可以拿到父组件上使用 StoreContext 共享的数据
}
render() {
// 这里取 context 的时候,会去寻找当前组件的 contextType
// 发现为 contextType 为 StoreContext ,此时会从当前组件树往上去查找是否有
// StoreContext 类型的 Provider
console.log('this.context :>> ', this.context)
return (
<div>son comp</div>
)
}
}
// 设置可获取的上下文
SonComp.contextType = StoreContext
- 第二种方式
- 子孙组件可以使用 Consumer 获取到设置在该上下文上的数据
// SonComp.js
// 第二种 Consumer (推荐函数式组件使用,可以不使用 this)
import StoreContext from './context/StoreContext'
export default class SonComp extends Component {
render() {
return (
<div>
<StoreContext.Consumer>
{
(data) => {
return (
<div>accept data</div>
)
}
}
</StoreContext.Consumer>
</div>
)
}
}
- 第三种方式
- vue 种的 eventbus 方式
- 或者 pubsub-js(因为 vue 相对来说太大了)
import PubSub from 'pubsub-js'
// SonComp.js
export default class SonComp extends Component {
render() {
return (
<div onClick={this.handleClick}>SonComp</div>
)
}
handleClick = () => {
// 触发 add 事件,并传入一个参数值为 1
PubSub.publish('add', 1)
}
}
// ******************************
// OtherComp.js
import PubSub from 'pubsub-js'
export default class OtherComp extends Component {
constructor() {
super()
this.state = {
a: 1,
b: 2
}
}
render() {
return (
<div onClick={this.handleClick}>OtherComp</div>
)
}
componentDidMount() {
this.reference = PubSub.subscribe('add', (eventName, ...rest) => {
console.log('触发了 add :>> 接收到数据', rest)
// 这是引用了 this,组件销毁是,需要移除 PubSub 事件监听
this.setState({
c: 3
})
})
}
componentWillUnmount() {
// 记得移除,否则可能造成内存泄露
PubSub.unsubscribe(this.reference)
}
}
异常捕获
- getDerivedStateFromError、 componentDidCatch 只可以捕获子孙组件异常,自身组件异常无法捕获
- 可以在根组件设置捕获函数,捕获其所有子孙组件的错误,统一设置错误发生时候的逻辑
- 同时将错误信息上传到日志服务中心,以便分析异常情况
export default class App extends Component {
constructor() {
super()
this.state = {
x: 0
}
}
render() {
return (
<div>
// 子孙组件报错会被捕获
<SonComp/>
</div>
)
}
// 两个方法都是异常捕获的方法,不可共存
// 只需要实现其中一个即可
static getDerivedStateFromError(error) {
// 不能访问 this,this 为 undefined
console.log('获取子孙组件异常', error)
// 可通过返回值的方式,合并 this.state 对象
return {
x: 1
}
}
componentDidCatch(error, stack) {
// 可调用 this
console.log('获取子孙组件异常', error, stack)
// 可直接修改 this.state
this.setState({
x: 1
})
}
}
render 的 return 格式
- 不能 return 多个对象
- 可以 return 数组
- 必须包裹一个最外层
- 可以使用
<Fragment></Fragment>
包裹元素,但是最终并不会渲染 Fragment - 可以使用 <></> 包裹元素,<></> 是 Fragment 的简写
import { Fragment } from 'react'
render() {
return (
// 不可以
<div>1</div>
<div>2</div>
<div>3</div>
// 可以---包裹一个标签
<div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>
// 可以---数组渲染
[
<div>1</div>,
<div>2</div>,
<div>3</div>
]
// 可以---Fragment
<React.Fragment>
<div>1</div>
<div>2</div>
<div>3</div>
</React.Fragment>
// 可以---空标签<></>,但是不推荐,因为浏览器可能不支持或者渲染异常
<>
<div>1</div>
<div>2</div>
<div>3</div>
</>
)
}
异步组件
- Suspense、lazy 实现异步组件,不用不加载,使用到才加载
import React, { Component, Suspense, lazy } from 'react'
// 使用 lazy 异步加载
const AsyncComp = lazy(() => import('./component/AsyncComp'))
export default class App extends Component {
constructor() {
super()
this.state = {
show: false
}
}
render() {
const { show } = this.state
return (
<div>
// 配合使用 Suspense 才能使用异步组件
// 在加载异步js同时,需要使用 fallback 作为临时提示
// 加载完成后关闭提示,展示异步组件逻辑
<Suspense fallback={<Loading/>}>
{ show && <AsyncComp />}
</Suspense>
</div>
)
}
}
immutable.js 设置不可变对象
- react 中为了性能优化,需要保持 state 的值不变,可以使用 immutable 来实现
- immutable 内部提供的所有数据类型,对其数据进行任意操作
- 操作得到的结果是修改后的值,且修改后的值是一个新对象,原来对象并没有发生变化
备注:我觉得为了实现这个不可变,引入一个新库,并且这个新库还有自己的操作api,实在是令人恶心
import { Map } from 'immutable'
const originObj = {a:1, b: 2, c: 3}
const immutableObj = Map(originObj)
const map1 = immutableObj.set({ a: 2 }) // map1 => { a:2, b: 2, c: 3 }
immutableObj.get('a') // 1
map1.get('a') // 2
得到修改后的全新的对象 map1
但是 immutableObj 不会改变
- 数据类型对应
- 不同数据类型都有自己的增删改查Api,需要去官网查询
Map => {}
List => []
IDE 报错解决
1、
提示找不到 react 声明或者
JSX 元素隐式具有类型 "any",因为不存在接口 "JSX.IntrinsicElements"
等等
解决:
安装 yarn add @types/react
都读到最后了、留下个建议如何