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

posted @ 2021-04-04 17:28  青S衫%  阅读(482)  评论(0编辑  收藏  举报