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 (
            
App comp
) } } ReactDOM.render(, 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 = '

标题

' const visiable = true const arr = [1, 2, 3] const obj = { a: 1, b: 2, c: 3 } return (
// 渲染 App comp 字符串
App comp
// 渲染变量 message
{ message }
jsx 支持在双大括号{}里书写 js 语法逻辑
{ message === '' ? '空' : message }
{ visiable && }
{ message.split('') }
// 渲染数组
    { arr.map((item, index) => { // 这里key本不该用 index // 需要使用业务的唯一值 // react 用来做复用,做性能优化 return
  • {item}
  • }) }
// 渲染对象
    { (() => { let tagArr = [] for (const key in obj) { if (Object.hasOwnProperty.call(obj, key)) { tagStr.push(
  • {obj[key]}
  • ) } } return tagArr })() }
// 设置类名与添加类名
App comp
// 设置样式,第一层{}表示要解析当前字符串 // 第二层表示对象
App comp
// 相当于 vue 的 v-html
// 子组件
) } } ReactDOM.render(, 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 (
            
App comp
// 每次 render,都会bind一次,可以提前到 constructor 中存储
App comp
App comp
App comp
App comp
{ // 不推荐这么写,因为 render 每次执行,这里都会重新创建函数以及其作用域,没必要 console.log('执行了 :>> ') // 该this为当前组件上下文 console.log('this :>> ', this) }}>App comp
) } 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 (
            
) } handlePlus = () => { console.log('plus :>> ', 1) } } ReactDOM.render(, document.querySelector('#root')) // **************************** // SonComp.js import React, { Component } from 'react' import types from 'prop-types' export default class SonComp extends Component { render() { return (
son comp
) } } // 设置可接受的数据类型,若不通过校验,则会报错 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 (
            
) } acceptSonData = (sonData) => { console.log('接收到子组件数据 :>> ', sonData) } } ReactDOM.render(, document.querySelector('#root')) // **************************** // SonComp.js import React, { Component } from 'react' export default class SonComp extends Component { render() { return (
son comp
) } 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 (
            
App
) } 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 (
            
{ this.state.count }
点击 + 1
多次修改
点击 + 1
) } 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 (
            
App comp
) } } // 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 (
        
fun comp
) } 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 (
    // 第一种、不推荐
    
// 第二种、函数形式,即可在当前组件使用 this.refName 获取当前元素,element 即是当前元素
{ this.refName = element}}>
// 第三种、将ref 设置为组件变量 this.refName // 而这个 this.refName = createRef() 其值为: { current: element } // 取值 this.refName.current === element
)

ref 转发

  • ref 设置在子组件上,但是该 ref 可以不是获取当前组件对象
  • 可以子组件内部自己设置 ref 所指定的元素或者对象
import React, { createRef } from 'react'
constructor() {
    // 第三种
    this.refName = createRef()
}
componentDidMount() {
    console.log('refName', this.refName) // 这里为 SonComp 中的某个元素
}
return (
    
)

// ********************************
// SonComp.js
// 在 SonComp 中,将 ref 转发到自身组件的某个元素上
// 这样,父组件即可获得自身组件的某个元素

// 函数式组件接收 ref
export default React.forwardRef((props, ref) => { 
    return (
        
// 或者 ) }) // 类组件接收 ref export default React.forwardRef((props, ref) => { class AcceptComp extends Component { render() { return (
// 或者 ) } } return })

页面内部的子组件脱离父组件渲染

  • 子组件脱离父组件在父组件外部渲染
  • 但是子组件依然可以使用父组件的数组,可以获取父组件的上下文
// 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 (
            
// 这里 Model 不会渲染在这个位置 // 而是渲染在外部指定的元素标签底下
) } } // *********************** // 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 (
            // 提供数据
            
                
) } }
  • 第一种方式
    • 使用子组件.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 (
            
son comp
) } } // 设置可获取的上下文 SonComp.contextType = StoreContext
  • 第二种方式
    • 子孙组件可以使用 Consumer 获取到设置在该上下文上的数据
// SonComp.js
// 第二种 Consumer (推荐函数式组件使用,可以不使用 this)
import StoreContext from './context/StoreContext'
export default class SonComp extends Component {
    render() {
        return (
            
{ (data) => { return (
accept data
) } }
) } }
  • 第三种方式
    • vue 种的 eventbus 方式
    • 或者 pubsub-js(因为 vue 相对来说太大了)
import PubSub from 'pubsub-js'
// SonComp.js
export default class SonComp extends Component {
    render() {
        return (
            
SonComp
) } 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 (
OtherComp
) } 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 (
            
// 子孙组件报错会被捕获
) } // 两个方法都是异常捕获的方法,不可共存 // 只需要实现其中一个即可 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 的简写
import { Fragment } from 'react'
render() {
    return (
        // 不可以
        
1
2
3
// 可以---包裹一个标签
1
2
3
// 可以---数组渲染 [
1
,
2
,
3
] // 可以---Fragment
1
2
3
// 可以---空标签<>,但是不推荐,因为浏览器可能不支持或者渲染异常 <>
1
2
3
) }

异步组件

  • 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 (
            
// 配合使用 Suspense 才能使用异步组件 // 在加载异步js同时,需要使用 fallback 作为临时提示 // 加载完成后关闭提示,展示异步组件逻辑 }> { show && }
) } }

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