react入门三

2021-03-28  本文已影响0人  liquan_醴泉

ReactDOM.render(<MyComponent/>, document.getElementById("root"))发生了什么

react解析MyComponent标签找到了MyComponent组件,发现组件是类定义的,随后new出该类的实例,并通过该实例调用到原型上的render方法
将render方法返回的虚拟dom转为真实dom,随后呈现在页面中,呈现后还会调用一个componentDidMount

ref

createRef

class Demo extends React.Compoent{
    myRef = React.createRef(); // createRef返回一个容器,该容器用于存放被ref所标识的节点,该容器专人专用只能绑定一个,因此后放进去的会覆盖之前的,如果想使用多个就需要createRef多次
    render() {
        return (
            <input ref={this.myRef}/> {/*这里相当于将input存放到了myRef中了,input节点可以通过this.myRef.current访问*/}
        ) 
    }
}

react中的事件

收集表单数据

高阶函数和柯里化

高阶函数:
若函数A接受的参数是一个函数,那么A就称之为高阶函数
若函数A调用的返回值依然是一个函数,那么A就称之为高阶函数
常见的高阶函数:如Promise, setTimeout, 数组的常用方式
函数柯里化
部分求值,函数的调用仍旧返回函数,实现多次接受参数最后统一处理的函数编码

生命周期

组件从创建到死亡会经历一些特定的阶段

React组件中包含一系列钩子函数,会在特定的时刻调用

在定义组件时,会在特定的声明周期回调函数中做特定的工作

组件挂载的执行顺序
constructor ---> componentWillMount ---> render ---> componentDidMount ---> componentWillUnmount

组件更新执行顺序(可以分为3条线)

父组件渲染子组件,当传入的数据更新时的执行路线

componentWillReceiveProps ---> shouldComponentUpdate ---> componenWillUpdate ---> render ---> componentDidUpdate ---> componentWillUnmount

当组件setState更新的时候

setState() ---> shouldComponentUpdate ---> componentWillUpdate ---> render ---> componentDidUpdate ---> componentWillUnmount

当强制更新组件的时候(不更改状态数据就想让组件更新就可以走这条线)

forceUpdate() ---> componentWillUpdate ---> render ---> componentDidUpdate ---> componentWillUnmount

ps: shouldComponentUpdate是一个阀门钩子,如果不写这个钩子默认返回true,接下来的流程正常执行,如果写了并且返回false则接下来的流程不会执行,只能返回true或者false,

componentWillReciveProps 这个钩子第一次不调用,只有接受的props变化了才会调用,也就是父组件重新渲染导致子组件渲染才执行该钩子,接受新的props。

总结:

初始化阶段
constructor ---> componentWillMount ---> render() ---> componentDidMount
更新阶段: 由组件内部this.setState() 或者父组件更新触发
shouldComponentUpdate ---> componentWillUpdate ---> render ---> componentDidUpdate

卸载阶段:由reactDOM.unmountComponentAtNode(node)触发
componentWillUnmount()

常用的钩子:
componentDidMount: 一般做初始化操作例如开启定时器,发送网络请求,订阅消息。。。

componentWillUnmount: 做一些收尾工作如取消定时器,取消订阅等

render: 必须用,需要掉1+n次

所有带will的钩子在新版版中都加了UNSAFE_前缀, 除了卸载的钩子,为什么这么做,因为react在设计异步渲染,这些组件可能会出现bug,以后可能会被废弃

新版本废弃了componentWillMount, componentWillUpdate, componentWillReciveProps,新增了getDerivedStateFromProps 和getSnapshotBeforeUpdate

需要注意的是 getDerivedStateFromProps不能在实例上调用,需要声明成静态的需要加static, 而且该方法必须要有返回值,要么你返回状态对象,要么返回null,不能返回其他的。需要注意的是只要返回一个对象,那么状态的更新就没有意义了,因为一旦返回状态,此状态就不能被改了,该方法接收一个参数props, 他会接收props属性并且会派生出一个取决于props的状态。可以接收第二个参数state,使用场景罕见,基本不用。

getSnapshotBeforeUpdate: 该钩子处于render和componentDidUpdate之间,用于在更新之前获取快照,因为更新后之前的状态就不见了,在更新之前再看一眼之前的状态。

componentDidUpdate() 该钩子会接收三个参数,第一个是更新前的props,第二个是更新前的state,第三个是一个快照值(即getSnapshotBeforeUpdate返回的值。)
挂载时:

constructor ---> getDerivedStateFromProps ---> render (react更新dom和refs) ---> componentDidMount

更新:

(new props | setState() | forceUpdate)时 ---> getDerivedStateFromProps(得到一个派生的状态) ---> shouldComponentUpdate

dom的diff算法

class Person extends React.Component{
   state = {
       persons: [
           {id: 2, name: '小张', age: 23},
           {id: 3, name: '小李', age: 19}
       ]
   }

   render() {
       return (
           <ul>
               {
                   this.state.persons.map((person,index) => {
                       return <li key={index}>{person.name}</li>
                   })
               }
           </ul>
       )
   }
}


虚拟dom中的key有什么作用?

简单说key是虚拟dom对象的标识,在更新显示是key起着极其重要的作用,,

详细说,当状态中的数据发生变化的时候,react会根据新的数据生成新的虚拟dom,随后react进行新虚拟dom和就虚拟dom的diff对比,比较规则如下:
- 就虚拟dom中找到了与新虚拟dom中相同的key:
    1) 若虚拟dom中内容没有变化,直接使用之前的真实dom
    2) 若虚拟dom中内容变了,则生成新的真实dom,随后替换页面中之前的真实dom


- 旧虚拟dom中没有找到与新虚拟dom相同的key

    根据数据创建新的真实dom,随后渲染到页面。

用index作为key可能会引发的问题:

- 若对数据进行逆序添加,逆序删除等破坏顺序操作,会产生没有必要的真实DOM更新 ==》 界面效果没问题,但是效率低

- 如果页面中还包含输入类的dom:
    会产生错误dom更新==》 界面有问题

注意,如果不存在对数据逆序添加删除等破坏顺序的操作,仅用于渲染列表,展示列表,使用index是没有问题的

<input type="checkbox"/> 这样的输入内容在react中有一个defaultChecked属性,可以代替checked属性,因为使用checked,则必须使用onchange来修改其值

react组件通讯

消息的发布订阅实现兄弟组件之间的通信常用的三方库有pubsubjs

react路由

- 向路由组件传递search参数

```
<Link to={`home/message/detail/?id=${id}&title=${title}`}> // 相当于传递了一个id,一个title

接受的方式(无需声明正常注册路由即可)
<Route path="/home/message/detail" component={Detail}>

Detail组件通过this.props.location.search获取,获取到的是一个字符串,我们需要的是对象的格式,因此需要特殊处理,安装脚手架的时候其实下载了一个叫做querystring的库,通过这个库可以帮我们完成
import qs from 'querystring';

// 通过qs.stringify(obj) 可以将一个对象转为key=value&key=value的格式
// 通过qs.parse(str) 可以将一个key-value格式的字符串变成对象

```
```
<Link to={{pathname:`/home/message/detail`, state: {id: id, title: title}}}>

// 接受state
通过this.props.location.state可以获取

```

- push 与 replace

历史记录是一个压栈模式,默认是push,如果要开启replace需要这么写, replace是替换模式
<Link replace to={{pathname:`/home/message/detail`, state: {id: id, title: title}}}>

- 编程时路由导航

通过脚本的方式进行跳转。路由组件通过this.props.history可以拿到history对象,里面包含操作历史的方法,需要注意的是编程时路由跳转时注册路由需要对应,也就是说通过search跳转的不能以params的方式注册路由,其他同理,params和query的参数在地址栏好理解,state的不再地址栏,但是同理对应的方式也有第二个参数可以接收state, 如this.props.history.push(path, state)

编程时路由导航的几个api:
* this.props.history.push
* this.props.history.replace
* this.props.history.goBack
* this.props.history.goForward
* this.props.history.goBack
* this.props.history.go

- withRouter

路由组件里面可以获取到操作历史的方法,但是再非路由组件里面是没有这些的。可以借助withRouter使一般组件也能获取到history
import {withRouter} from 'react-router-dom'

比如
export default withRouter(Header) ; 通过withRouter将一般组件Header加工后暴露,此时使用header组件就会有history相关的内容


- BrowserRouter与HashRouter的区别

 * 底层原理不一样,BrowserRouter使用的是H5的history API,不兼容Ie9及以下版本HashRouter使用的是url的hash值
 * url表现形式不一样,HashRouter带#
 * 刷新后对路由state的影响
    BrowserRouter没有影响,因为state保存在history对象中
    HashRouter会丢失state参数

* HashRouter可以用于解决一些路径错误相关的问题

redux

react-redux

react-redux需要注意的几个点:

容器组件就是一个桥梁,用于连接ui组件和redux,因此在容器组件中需要引入要包装的ui组件,要引入store(redux的核心就是store),需要注意的是容器组件不能亲自引入store,而是通过props的方式传入store,比如有一个容器组件Count,此时可以通过props传递
<Count store={store}/>
需要注意的是页面中不止这么一个容器组件,他都需要store,那么react-redux提供了一个批量提供store的方法
import {Provider} from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
)

需要引入连接的方式
import {connect} from 'react-redux'
// 得到一个容器组件
const ContainerComponent = connect()(ui组件);

容器组件和父子组件通信通过props的方式传递状态和操作状态的方法,正常的组件是通过标签属性的方式给子组件传递参数的,但是容器组件是由connect方法形成的,并没有和ui组件有这明确的父子关系,因此需要注意的是connect的第一个()需要两个参数。第一个参数是一个函数,用于生成传给ui组件的状态,需要返回一个对象,既然是操作状态,那么该函数一定能接受store中的状态,该函数接受一个参数state,第二个参数同样是一个函数,(当然也可以是一个对象,里面是一系列action,是精简版的写法,这这种写法,react-redux会自动分发对应的action),接受一个分发action的dispatch方法,返回一个对象,对象封装的是操作状态的方法。从语义上来讲,第一个参数就是一个mapStateToProps,第二个参数就是mapDispatchToProps

示例代码

funciton mapStateToProps(state) {
    return {
        count: state
    }
}

function mapDispatchToProps(dispatch) {
    return {
        increament: (number) =>  dispatch(createInreamentAction(number))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(ui组件)

mapStateToProps映射状态 mapDispatchToProps映射操作状态的对象, 这两个函数都返回一个对象
mapDispatchToProps 也可以是一个对象,对象是一系列action,react-redux会自动分发这个action

注意事项:当一个ui组件对应一个容器组件的时候,如果这样编码会导致组件的数量成倍增加,可以同过一个文件将其整合,定义ui组件,最终以容器组件暴露

redux的开发工具

一个项目一般都由多个人写,可能都会用到redux存数据,但是彼此之间不知道状态是如何存的,都存了哪些状态,这时可以通过redux的开发工具来快速追踪状态的变化。需要引入一个库redux-devtools-extension
在store.js中引入

import {composeWithDevTools} from redux-devtools-extension

export default createStore(allReducers,composeWithDevTools(applyMiddleware(thunk)))


关于setState

setState更新状态的两种写法

lazyLoad

路由组件的懒加载,不适用懒加载,所有的路由组件都会一次性被加载,如果有成百上千个路由组件,这是非常恐怖的。路由组件应该在跳那个路由就加载那个路由组件,这就是懒加载,在需要的时候才加载组件

hooks

组件优化

- Component的两个问题
    1) 只要执行setState(), 即使不改变状态数据,组件也会重新render() ,也就是setState({}) 也会render.
    2) 只要当前组件重新render,就会自动重新render子组件 ,效率低。

  

    高效的做法应该是
        只有当前组件的state或者props数据发生改变时才重新render

    之所以有这样的问题是因为Component中的shouldComponentUpdate()默认总是返回true. 通过重写这个钩子可以解决这个问题

    shouldComponentUpdate(nextProps, nextState){
        
        console.log(this.props, this.state) // 当前的props和state
        console.log(nextProps, nextState); // 目标props和state
        // 根据当前数据和目标数据对比来决定返回ture|false ,至于怎么比有人做了,react中提供了PureComponet这个组件,他会重写 shouldComponentUpdate, 里面的对比逻辑是写好了的
        reuturn true;
    }

- PureComponent 

    该组件其实也没干啥,就是重写了shouldComponentUpdate, PureComponent也会有一点小瑕疵。因为他的底层进行了浅对比,因此如果是下面的这种写法同样会有问题

    const obj = this.state;
    obj.xxx = 'XXX'; // 改了某一个属性
    this.setState(obj); // 因为obj和state指向了同一个引用,因此在使用PureComponent比较时认为state是没有变化的,所以不会render.因此一定注意使用PureComponent,在更新状态时不要和原来的状态发生任何关系。使用时额外注意数组的那些方法。

renderProps

场景,比如有两个组件A,B,不应该关系不大,但是因为某些逻辑的原因可能需要这么写,

<A>
    <B/>
</A>
对于这种格式要个可以通过this.props.children来获取,但是有个问题如果B组件需要A组件内的数据是做不到的,因此就有了下面的这种格式

<A render={(parmas) => <B {...params}/>}/> // 就相当于传递了一个函数,该函数返回一个组件,函数名不一定叫render.

在A组件中预留一个位置this.props.render(可以传递A中的状态到B),类似于vue中插槽,

组件通信的总结

* 组件间的关系

    1) 父子组件
    2) 兄弟组件 (非嵌套组件)
    3) 祖孙组件(跨级组件)

* 几种通信方式

    1) props
        1.1) children props
        1.2) render props

    2) 消息的发布订阅
            pubsub ,event等
    3) 集中式管理如redux, dva等

    4) context:
            生产者和消费者模式

快速预览打包后的程序

全局安装serve,会快速开启一个服务。

错误边界

一个页面有多个组件构成,但是其中的某个组件因为某些不可控因素导致出错,此时整个页面都会出问题,边界错误就是处理这类问题的,就是当某个组件发生了错误,不要导致整个页面崩溃,而是将错误控制在最小的范围了,出错了给一个友好的提示。需要注意的是错误边界只有在生产环境有效,就是打包后才生效,开发环境是不生效的。总的来说错误边界就是用来捕获后代组件错误,渲染出备用页面,只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件,定时器中产生的错误。

当Parent的子组件出错时会走这个钩子,定义在父组件,并且携带错误信息err
state = {
    error: '' // 默认空,当发生了错误就会有值,可以根据这个值给一个友好的提示
}
static getDerivedFromError(err) {
    return {error: err}
}

当然还有一个钩子componentDidCatch() {
    console.log('渲染组件出错');
} 当组件发生错误的时候就会触发该钩子,这里可以统计发生错误的次数
上一篇下一篇

猜你喜欢

热点阅读