React(上)

985 阅读37分钟

1,关于pureComponent

pureComponent默认实现了shouldComponentUpdate,对state与props进行浅比较,判断组件是否更新。浅比较使用Object.is(v1,v2),类似于===,区别于===如下:

console.log(NaN === NaN)            // false
console.log(Object.is(NaN, NaN))    // true

console.log(+0 === -0)              // true
console.log(Object.is(+0, -0))      // false

2,关于react中的元素与组件

react元素本质是JS对象或者说是虚拟dom
react组件本质是类或者函数,函数返回值或者类实例render方法返回值是虚拟dom

3,关于函数组件与类组件区别

1,函数组件是一个函数,函数返回值既要渲染的虚拟dom,类组件是一个类,先将类组件实例化,实例render方法返回值既要渲染的虚拟dom

2,函数组件无生命周期方法与状态管理,类组件有状态管理与生命周期方法。(hook出现之前)

3:
★  函数组件每次执行都会使用本次render所接受的props,所以函数组件永远展示的都是当前渲染时所接受的props

★  而对于类组件,props存在与组件实例上(this.props),因此无论类组件哪一次渲染,拿到的都是this对象上最新的props

关于3的实际场景配合理解:比如,一个函数组件渲染时,会添加一个定时器任务,5s后输出props,但是如果在这5s内更新props,函数组件重新渲染,那么最终一共会输出两次props,一次时上一次的preProps,一次是更新后的nextProps,但是对于同场景下的类组件,两次都会输出更新后的nextProps

  • 类组件代码及效果图:

    
    class Child extends React.Component {
        render() {
            setTimeout(() => console.log(this.props), 5000)
            return <div>  Child </div>
        }
    }
    
    class Parent extends React.Component {
        state = { counter: 1 }
        add = () => this.setState({ counter: this.state.counter + 1 })
        render() {
            return <div>
                <button onClick={this.add}>{this.state.counter}</button>
                <Child counter={this.state.counter} />
            </div>
        }
    }
    
    ReactDOM.render(<Parent />, document.getElementById('root'))
    

    Sep-18-2021 14-55-55.gif

  • 函数组件代码及效果图

    function Child(props) {
        setTimeout(() => console.log(props), 5000)
        return <div>  Child </div>
    }
    
    class Parent extends React.Component {
        state = { counter: 1 }
        add = () => this.setState({ counter: this.state.counter + 1 })
        render() {
            return <div>
                <button onClick={this.add}>{this.state.counter}</button>
                <Child counter={this.state.counter} />
            </div>
        }
    }
    
    ReactDOM.render(<Parent />, document.getElementById('root'))
    

    Sep-18-2021 14-57-21.gif

4,关于受控组件与非受控组件

非受控组件:非受控组件的状态由组件自己维护,如果需要获取它的状态可以使用ref命中该组件的真实dom获取其内部状态,如下,一个非受控input

class UnControlInput extends React.Component {
    constructor(props) {
        super(props)
        this.inputRef = React.createRef()
    }
    componentDidMount() {
        console.log(this.inputRef.current.value);
    }
    render() {
        return <input defaultValue="非受控组件" ref={this.inputRef} />
    }
}

受控组件:受控组件的状态由react维护,比如将组件的状态与react的state关联起来,使得react的state成为组件的数据源,每次修改组件状态,都更新关联的state状态。如下,一个受控的input:

class ControlInput extends React.Component {
    constructor(props) {
        super(props)
        this.state = { inputValue: '受控组件' }
    }
    change = e => {
        this.setState({ inputValue: e.target.value })
    }
    render() {
        return <input value={this.state.inputValue} onChange={this.change} />
    }
}

5,关于React.Children.map(children,mapCallback)与this.props.children.map(mapCallback)

对于子节点只有一个或者子节点为null,或者子节点为undefined,this.props.children的类型分别为对象,null和undefined

因此this.props.children.map对于这些情况时无法正确处理

而React.Children.map可以正确处理这些子节点特殊情况,从而不必我们手动判断子节点类型去使用this.props.children.map

6,关于React中的事件委托

React采用事件委托的方式处理(大部分)合成事件,原理既我们的事件绑定并没有绑定到对应dom上,而是将事件触发节点,事件类型,事件回调注册起来,然后利用事件冒泡特性,在docuemnt统一拦截各类型的事件,且event.target对应事件触发节点,最后根据事件类型,事件触发节点找到注册的对应事件回调执行。

事件委托的优点:

  • 仅根节点绑定一个事件可处理大量子节点元素的事件,从而减少dom元素上的事件绑定,节约内存

  • 事件统一注册在内存中,减少了对dom元素的操作

对于onPlay,onSubmit则并没有采用事件委托,而是直接绑定在dom上

  • onSubmit事件本身不具有通用信,且较为复杂,可能这是React不采用事件委托原因
  • onPlay不具有冒泡特性,但是具有捕获特性,可能这是React没有对其采取事件委托原因

7,关于createElement和cloneElement

React.createElement(type,props,...children):JSX本质既该方法,该方法接受多个参数,虚拟dom类型(文件类型,原生标签类型,组件类型等),虚拟dom的属性(props)及虚拟dom的的所有子节点创建一个虚拟dom元素返回

React.cloneElement(element,[newProps],[newChildren]):该方法以element元素为样板,返回一个新的react元素,其中newProps将与element的props浅合并,newChildren将替换element的children

// React.cloneElement(element,props,children) 几乎等同于下面这段代码
<element.type {...element.props} {...props}>{children}</element.type>

8,关于React生命周期

image.png

image.png

挂载阶段 更新阶段 卸载阶段 错误捕获阶段

挂载阶段:

  • constructor:

  • static getDerivedStateFromProps(nextProps,preState):

    • 该方法会在render前调用,不管是挂载阶段还是更新阶段都会被调用,它应该返回一个对象更新state,如果返回null则不更新state

    • 该方法仅适用于当前state无论什么时候都取决于props

    • 该方法是静态方法,不能访问组件实例(this)

    • 该方法每次渲染前都会触发,而UNSAFE_componetWillReceiveProps只在props更新(非初始渲染)或者父组件重新渲染时触发

  • UNSAFE_componentWillMount():

    • 该方法在组件挂载前被调用,且此方法内使用同步setState(不在异步函数内)不会触发额外渲染,但不建议在此更新state,完全可以在构造器中初始state

    • 避免在该方法中引入副作用与订阅,这些操作应该放在componentDidMount中

    • 此方法是服务端渲染会调用的唯一生命周期函数

  • render():

  • componentDidMount():

    • 该方法会在组件挂载到真实dom后立即调用(注意,是组件挂载到真实dom后立即调用,而不是dom渲染到屏幕上才调用,所以如果在componentDidMount中有死循环,那么页面是不会绘制到屏幕上的,因为此时dom虽然挂载完成,但是因为componentDidMount中有死循环,因此在这里阻塞住,因此屏幕没办法进行更新(既没办法处理浏览器的绘制),所以看不到任何页面,即使dom已经挂载完成)

    • 因此这里是获取组件挂载后的dom初始状态,或者发起网络请求或订阅的好地方

    • 这里也可以调用setState,不过区别于UNSAFE_componentWillMount,这里会触发组件的额外渲染(render执行两次,且这里也会开启批量更新!!!),不过这两次渲染会发生在屏幕更新(既浏览器绘制)之前,从而保证即使发生两次更新,用户也不会看见中间状态

更新阶段

  • UNSAFE_componetWillReceiveProps(nextprops):

    • 该方法会在已挂载组件接收新props时调用,如果需要比较nextProps与this.props以更新状态可以在此进行

    • 如果是父组件重新渲染,那么即使当前组件没有接受新props,该方法也会执行

    • 接受初始props该方法不会执行,一般只在组件props更新时调用该方法

  • static getDerivedStateFromProps(nextProps,preState):

  • shouldComponentUpdate(nextProps,nextState):

    • props 或者 state发生变化时,该函数会在渲染(render)之前调用,默认返回值为true,既允许组件更新渲染,如果返回false,则不允许组件更新渲染。

    • 首次渲染或forceUpdate调用不会执行该方法

    • 我们一般使用该函数进行性能优化,通过参数的nextProps与this.props进行比较或者通过参数的nextState与this.state进行比较判断是否需要跳过此次更新

    • 当shouldComponentUpdate返回false时:

      • componentWillUpdate render componentDidUpdate都不会被调用。
      • 但是state和props会被更新为nextState 和 nextProps,但是由于页面没有更新,因此页面显示仍然是preState与preProps
  • UNSAFE_componentWillUpdate(nextProps,nextState):

    • 当组件接受到新的props或者新的state时,会在更新前调用该方法,一般该方法用来做最后的更新前准备操作的时机

    • 注意:首次渲染不会执行该方法

    • 注意:不应该在此方法内触发组件更新操作(setState等),很大概率导致死循环

  • render():

  • getSnapshotBeforeUpdate(preProps,preState):

    • 该方法在render方法后,dom更新之前调用,因此我们可以在此捕获到更新之前的dom相关信息,此方法返回值将作为参数交给componentDidUpdate第三个参数

    • 该方法一般应用场景可能是:dom更新之前获取更新前的dom信息然后交给componentDidUpdate做一些操作(componentDidUpdate执行时dom已经更新完毕)

  • componentDidUpdate(preProps,preState,snapshot):

    • 该方法会在页面更新渲染后立即调用,首次渲染不会执行该方法(注意,这里是组件更新dom后立即调用,而不是更新的dom渲染到屏幕上才调用,所以如果在componentDidUpdate中有死循环,那么页面是不会绘制的的,因此此时虽然dom更新了,但是因为componentDidUpdate中有死循环,因此在这里阻塞住,屏幕没办法进行更新(既没办法处理浏览器的绘制),所以看不到页面的更新,即使dom已经更新)

    • 当组件更新后一般可以在此 处理一些新的dom操作,或者校验新旧props发起网络请求

    • 当然也可以在此setState,但是需要注意,setState一般需要放在条件语句中,否则大概率会导致死循环

卸载阶段

  • componentWillUnmount():

    • 该方法会在组件卸载销毁前调用,一般在此执行一些清除操作,比如:取消订阅,清除定时器,取消网络请求等

    • 该方法内不应该使用setState,因为组件永远不会重新渲染,组件实例卸载后,将永远不会再挂载它

错误捕获阶段

  • static getDerivedStateFromError(error):

    • 该方法将在后代组件抛出错误后被调用,抛出的错误既参数,并返回一个值更新state

    • 该方法会在渲染过程中触发,因此不允许出现副作用,如需处理副作用,放在componentDidCatch中处理

    • 一般我们在此发现发生错误,将更新state从而渲染错误边界组件的备用UI

  • componentDidCatch(error,info):

    • 该方法在提交阶段被调用,允许出现副作用,可以在此记录错误等行为

tips

  • 为什么废弃三个will生命周期: fiber渲染分为三个阶段,render,pre-commit,commit,其中render阶段是可以打断,暂停,重启的,因此这个阶段的任务很可能会被执行多次,废弃的三个will~都属于render阶段,因此可能多次执行。

  • 为什么componentWillMount为什么不适合处理异步:

    • 服务端渲染中componentWillMount会在服务端与客户端各执行一次,而didMount只在客户端执行一次

    • fiber中,任务可中断,willMount可能会被执行多次

    • willMount能做的事,在constructor与didMount中也能做

  • getDerivedStateFromProps 为什么被设计为静态: 该方法如其名,即通过props获得由其衍生的state,所以当我们state依赖props应该使用该方法,该方法返回值将作为新的state更新到类组件实例的state中,至于这个方法为什么是静态的,首先,当我们无条件的将props更新到state中或者根据props与state对比更新state,我们会用到这个方法,而这些操作本身就很纯粹,而且不需要我们操作类组件实例,所以设计成静态完全可以满足这个方法的需求。当然,设计成组件声明周期也是可行的,但是一旦设置成组件生命周期,该函数就能访问组件实例,我们除了上面的方法,还可以在该函数中进行副作用操作,比如调用接口获取数据更新到state中,很显然这不是react官方期望看到的,你完全可以将这种操作放到componentDidMount中,虽然我们不这么做,但getDerivedStateFromProps设计成生命周期形式提供了这种操作可能性,而设计成静态方法,getDerivedStateFromProps将访问不到组件实例,如果任有开发者期望在此异步操作,并更新state无疑很难做到,这将使得开发者做这种操作时远离该方法。为什么getDerivedStateFromProps是静态方法?

    • getDerivedStateFromProps可能的应用场景:如果前后props数据变化,则异步请求新的数据,之前的方式使用componentWillReceiveProps可以很好的实现,但是该方法即将废弃,所以可以使用getDerivedStateFromProps作为替代方案之一,具体实现既每次更新都在getDerivedStateFromProps对比前后props数据,如果props数据发生变化,则将props变化的数据更新到state中(return {preId : props.id}),之后在componentDidUpdate中对比前后两次props数据是否发生变化(this.props.id是否等state.preId),变化则重新加载数据

    • 当然上述场景直接在componentDidUpdate中进行前后props数据对比也可以实现componentDidUpdate(preProps){ preProps.id!==this.props.id && AsyncGetData() }

生命周期函数具体使用:

import React from 'react'

class Wrapper extends React.Component {
    constructor(props) {
        super(props)
        this.state = { count: 0 }
    }
    // componentWillMount() {
    //     console.log('componentWillMount------unsafe');
    // }
    static getDerivedStateFromProps(props, state) {
        console.log('getDerivedStateFromProps');
        return null
    }
    // componentWillReceiveProps(nextProps) {
    //     console.log('componentWillReceiveProps------unsafe');
    // }
    componentDidMount() {
        console.log('componentDidMount');
    }
    shouldComponentUpdate(nextProps, nextState) {
        console.log('shouldComponentUpdate');
        return true
    }
    // componentWillUpdate(nextProps, nextState) {
    //     console.log('componentWillUpdate------unsafe');
    // }
    getSnapshotBeforeUpdate(props, state) {

        console.log('getSnapshotBeforeUpdate',state);
        return null
    }
    componentDidUpdate(preProps, preState, snapshot) {
        console.log('componentDidUpdate');
    }
    static getDerivedStateFromError(error) {
        console.log('getDerivedStateFromError');
    }
    componentDidCatch(error, errorinfo) {
        console.log('componentDidCatch');
    }
    componentWillUnmount() {
        console.log('componentWillUnmount');
    }
    render() {
        console.log('render');
        return <button onClick={_ => { this.setState(state => ({ count: state.count + 1 })) }}>
            {this.state.count}
        </button>
    }
}

export default Wrapper

9,react使用state需要注意的事情

  • 1,不要直接修改state(this.state.name = 'aka韩红'),修改状态使用setState: 因为setState修改状态的同时也会触发组件的重新渲染,使组件保持最新的状态,但直接修改(this.state.count = 2这种)不会触发组件重新渲染。

  • 2,出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用,并采取浅合并的方式 : 因为如果每setState一次就需要重新渲染页面一次,如果某一函数连续触发setState那对于性能的降低不言而喻,所以react采取将多个setState合并成一个调用,这样即使大量的setState我们也只触发一次重新渲染。看下面一个例子。

    this.state = { count : 0 }
    onClick = () => {
            this.setState({ count: this.state.count + 1 })
            this.setState({ count: this.state.count + 1 })
            this.setState({ count: this.state.count + 1 })
        }
    

    当我们触发onClick合成事件,count只会在其基础+1,并不会+3,因为这三个setState浅合并,像下面这样

    Object.assign(
            {...this.state},
        { count: this.state.count + 1 },
        { count: this.state.count + 1 },
        { count: this.state.count + 1 },
    )
    

    最后一个 count: this.state.count + 1覆盖了前面的两个 count: this.state.count + 1 ,,造成count最后只是进行了一次count: this.state.count + 1,所以每次只+1。

    如果希望每次都+3也有办法,像下面这样修改。

    this.state = { count : 0 }
    onClick = () => {
            this.setState((preState,preProps)=>({ count : preState+1 }))
            this.setState((preState,preProps)=>({ count : preState+1 }))
            this.setState((preState,preProps)=>({ count : preState+1 }))
        }
    // setState传入第一个参数变为函数,函数两个参数分别是上一次的state与上一次的props,我们只需要使用上一次的state进行操作就可以了。
    
  • 3,setState大部分情况下是异步的操作(也有同步的时候)

    • 异步的setState(合成事件和生命周期函数使用的时候): setState其实其本身代码执行是同步的,但是合成事件与生命周期函数之中,react处于更新机制之中,当react处于更新机制时,所有setState要改变的状态会存入_pendingStateQueue队列中,不会立即执行,只有更新机制执行完毕,才会执行之前存入_pendingStateQueue的setState,导致setState看起来似乎时异步的。

    • 同步的setState(原生事件和异步事件之中 ): 根据js的异步机制,异步处理函数将会放到任务队列暂存,等待同步任务执行完毕再取出执行,当我们同步代码执行完,上一次更新机制已经执行完毕,此时处于react处于更新机制之外,setState要改变的状态并不会存入_pendingStateQueue队列中,所以立即执行setState,获取最新state。

    • 如果期待设置state之后做某事,可以使用回调函数处理: 如下代码

      // 在生命周期or合成事件中 如果希望在setState之后做些什么,以回调函数的形式处理即可
      this.setState({ name : 'aka韩红' }, callback)
      
    • ps:更多关于setState执行机制可以参阅下文
      setState的执行机制
      你真的理解setState吗?

  • 总结:

    • 1,不要直接修改state,修改状态使用setState。
    • 2,出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用,并采取浅合并的方式
    • 3,setState在合成事件以及钩子函数(生命周期函数)中执行是异步的,原生事件以及异步任务中执行是同步的

10,react中组件通讯方式有哪些

  • 1,父组件与子组件通讯:利用props向子组件传递数据
  • 2,子组件与父组件通讯:利用父组件传递props回调函数进行通讯
  • 3,父组件向子组件的子组件通讯:props嵌套或者使用context通讯
  • 4,非嵌套关系组件通讯:使用redux等状态管理或者全局自定义事件发布订阅

11,props与state的区别

  • props时传递给组件的数据,state是组件内部自己维护的状态数据,在构造函数中创建
  • props是不可修改的,所有react组件必须保护props不被修改,state可以修改且多变的

12,什么是高阶组件,什么是Render Props。

  • 关于对通用逻辑的抽象:如果我们期望在A页面中实时获取鼠标坐标展示在页面左下角,而B页面期望实时获取鼠标坐标展示在页面右上角,那么关于获取鼠标坐标的逻辑,我们其实期望将其抽象出来,关于通用逻辑获取鼠标坐标的抽象,我们就可以使用高阶组件,render props进行处理。

  • 高阶组件(HOC:Higher Order Component):HOC本质是一个函数,即接收组件为参数,返回一个新组件,新组件render结果是传入的参数组件,同时在新组件内部处理通用逻辑,将通用逻辑结果以props形式交给参数组件,完成通用逻辑的抽象。 下面是对于上述鼠标坐标逻辑抽象代码的HOC实现。

    import React from 'react'
    // HOC函数,用来处理获取鼠标坐标
    function HOC_mouseCatch(Component) {
        return class Wrapper extends React.Component {
            constructor(props) {
                super(props)
                this.state = { x: 0, y: 0 }
            }
            componentDidMount() {
                // 处理获取鼠标坐标逻辑
                window.addEventListener('mousemove', e => {
                    this.setState({ x: e.clientX, y: e.clientY })
                })
            }
            render() {
                return (
                    <Component
                        // 鼠标坐标结果以props形式交给 需要使用当前功能的组件
                        mouse={{ x: this.state.x, y: this.state.y }}
                        {...this.props}
                    />
                )
            }
        }
    }
    // 需要使用鼠标坐标的组件
    class ShowMouse extends React.Component {
        render() {
            return <div>x:{this.props.mouse.x},y:{this.props.mouse.y}</div>
        }
    }
    
    export default HOC_mouseCatch(ShowMouse)
    
  • 高阶组件使用注意点:高阶组件本身作为一种复用组件逻辑的技巧,我们期望HOC返回的新组件是在原组件的功能上做增强,而不是去修改原组件,所以应该尽量保持其返回新组件不会修改原组件。除了增强功能,其他功能应与原组件保持一致。 一般我们会做以下几种约定。

    • 1,不要改变原组件:我们不应该在HOC中对原组件修改,比如改写原组件原型上的componentWillUpdate方法。
    • 2,props透传:除了将通用逻辑以props形式传入原组件,我们也应该将新组件接受到的props传给原组件。
       render() {
              return (
                  <Component
                      mouse={{ x: this.state.x, y: this.state.y }}
                      // 新组件接收到的props也应该交给原组件
                      {...this.props}
                  />
              )
          }
      
      • 2.1,props透传不能传递ref,因为ref像key一样,并不是props属性,而是React单独处理,如果添加了ref到HOC返回组件,只是添加到传入组件的包裹组件上,不会传给原组件,如果期望传递ref,我们可以使用React.forwardRef进行处理。
      • 2.2,props透传还需要注意不要出现props覆盖:上图代码中,假如 {...this.props}中又mouse属性,那么将导致mouse={{ x: this.state.x, y: this.state.y }}被覆盖。
    • 3,原组件上的静态方法也需要部署到新组件上:仔细想一想,如果原组件上的静态方法但是在新组件中不具备,那么新组件其实不满足上面所说除了增强功能,其他功能应与原组件保持一致,所以我们一般手动拷贝(你得知道哪些方法需要被拷贝),或者使用hoist-non-react-statics自动拷贝所有 非React 静态方法,如图:
          import React from 'react'
          import hoistNonReactStatic from 'hoist-non-react-statics';
          function HOC (Component) {
              class Wrapper extends React.Component {}
              // hoist-non-react-statics自动拷贝所有 非React 静态方法
              hoistNonReactStatic(Wrapper,Component)
              return Wrapper
          }
      
    • 4,不要在render中使用HOC:如下图,如果在render使用HOC,那么每次重新渲染执行render方法,都将获取一个新的Content重新渲染(新的Content !== 旧Content),因此React每次都会卸载之前的Content,重新渲染新的Content,这不仅使得diff算法失效,导致性能问题,同时由于之前Content卸载,所以其状态以及其中子组件的状态都会丢失。
          import React from 'react'
          import HOC from './HOC'
          import ShowDetail from './ShowDetail'
          
          class Component_ extends React.Component {
                 render(){
                     // 不要下面这么做
                     const Conetent = HOC(ShowDetail)
                     return <Conetent />
                 }
          }
      
  • Render Props:简单来说即通过props属性告知当前组件渲染什么内容的技术。一般是定义抽象逻辑组件,其props属性传入一个函数,该函数接收props作为参数,并返回一个需要用到抽象逻辑的组件,同时将函数参数中的props传入返回组件中,同时抽象逻辑组件内部执行该函数,输出该组件。具体表现如下,依然以上面获取鼠标坐标举例。

    import React from 'react'
    class Wrapper extends React.Component {
        render() {
            // MouseCatch负责抽离通用逻辑, 
            // 其props中render属性接收通用逻辑结果,并返回使用当前逻辑结果的组件
            // 通用逻辑结果以props形式传给使用通用逻辑组件
            return <MouseCatch render={mouse => <ShowMouse mouse={mouse} />} />
        }
    }
    //  抽离鼠标坐标通用逻辑组件
    class MouseCatch extends React.Component {
        constructor(props) {
            super(props)
            this.state = { x: 0, y: 0 }
        }
        componentDidMount() {
            // 处理获取鼠标坐标逻辑
            window.addEventListener('mousemove', e => {
                this.setState({ x: e.clientX, y: e.clientY })
            })
        }
        render() {
            return (
                <div>
                    {/* render props */}
                    {this.props.render({ x: this.state.x, y: this.state.y })}
                </div>
            )
        }
    }
    
    // 需要使用鼠标坐标的组件
    class ShowMouse extends React.Component {
        render() {
            return <div>x:{this.props.mouse.x},y:{this.props.mouse.y}</div>
        }
    }
    
    export default Wrapper
    
  • Render Props注意点:render props有可能会抵消pureComponent带来的优势。 image.png

    • 上图中,每次Wrapper重新渲染,MouseCatch 中的render 都会接收到一个新的函数,因此即使MouseCatch使用pureComponent实现,但是每次父组件重新渲染,MouseCatch中props的render都会接收一个新函数,即MouseCatch中props每次都会发生变化,使得其内部重新渲染。 image.png
    • 通过将render值替换成Wrapper原型上的函数,即可解决上面问题,即使Wrapper重新渲染,而render属性值一直都是Wrapper原型对象上的方法,props也不会发生变化。
  • 关于renderProps与HOC区别:使用HOC处理返回的组件,外面套上父组件,父组件时机面向的是HOC内部的Wrapper组件,而不是原组件(父组件与原组件中间多了一层Wrapper组件),而render props中父组件处理的一直是原组件,所以HOC中的props覆盖问题,静态方法拷贝问题在render props中都不存在(render props中原组件接收的props就是其父组件传的props,而HOC是父组件将props交给Wrapper组件,再由Wrapper组件将值交给原组件)。

13,react中Context的理解

  • Context:Context提供了一种组件之间共享数据的方式,而不必通过组件显示的传递props。 组件之间的数据大都使用props层层传递,很多时候中间层可能根本用不到该数据,他们传递数据只是为了最后使用该数据组件服务,这样方式对于组件层级嵌套过深显而易见是极其繁琐的,
  • Context缺点:由于使用Context可以在很多不同层级组件之间共享数据,显而易见,如果我们希望复用这些组件,肯定需要为其提供Context,这样使得组件的复用性变差。
  • Context基本使用:如下代码,子组件消费父组件关联的context的数据
    import React from 'react'
    // 1,创建context,默认值{color:'red'}
    const ContextColor = React.createContext({ color: 'red' })
    
    class Wrapper extends React.Component {
        constructor(props) {
            super(props)
            this.state = { color: 'blue' }
        }
        render() {
            return (
                // 2,父组件使用context提供的Provider包裹,同时传入context值,替换创建传入的默认值,从而成为context数据源
                <ContextColor.Provider value={this.state} >
                    <Child />
                </ContextColor.Provider>
            )
    
        }
    }
    class Child extends React.Component {
        // 3,挂载Child类上的contextType赋值context对象
        // contextType属性可以使得子组件内部可以使用this.context访问之前创建的context对象中数据
        static contextType = ContextColor
        render() {
            // 4,使用context中的数据
            return <div>颜色:{this.context.color}</div>
        }
    }
    export default Wrapper
    
    • 如果子组件期望控制父组件value,因为一般我们如上图value会与父组件state关联,所以我们只需要想办法更改父组件state即可,所以我们可以采用下面两种办法。一般使用方法2。
      • 1,父组件利用props向子组件传递修改父组件state的回调
      • 2,父组件直接将修改父组件state的回调放到context中,子组件直接从context取出回调
  • Context使用注意事项:
    • 1,将value的状态提升到state中:,如下这种方式,每次触发render,都将value重新赋值{something: 'something'},导致子组件重新渲染。

       <MyContext.Provider value={{something: 'something'}}>
          <Toolbar />
        </MyContext.Provider>
      
      // 合适的做法,将value的状态提升到state中
      // some code
      this.state = { value: { something: 'something' } }
      // some code 
      <MyContext.Provider value={this.state.value}>
         <Toolbar />
       </MyContext.Provider>
      
    • 2,Provider的value的更新,其内部所有消费value的组件都会更新,且不受shouldComponentUpdate函数限制:即即使shouldComponentUpdate比较了props&state,只要value变化,那么还是会重新渲染。value的更新比较采用的是Object.is(NaN与NaN相等,+0与-0不等,其他都与===处理相同)。

    • 3,const MyContext = React.createContext(默认值)中默认值只有其父组件中没有使用MyContext.provider 才会使用默认值:否则即使父组件value是undefined也不会取默认值。这是因为默认值主要是为了在没有提供Provider时用来测试消费Context的组件,如下图:

          import React from 'react'
          // 传入context默认值{ color: 'red' }
          const ContextColor = React.createContext({ color: 'red' })
      
          class Wrapper extends React.Component {
              render() {
                  // 1,父组件只要使用.Provider包裹,不管设置value与否,子组件都不会使用context创建时传入的默认值
                  // 所以下面子组件中 this.context 是undefined,因为没有设置value
                  // 2,如果希望子组件使用默认值,则父组件不应使用context的Provider
                  return <ContextColor.Provider ><Child /> </ContextColor.Provider>
      
              }
          }
          class Child extends React.Component {
              static contextType = ContextColor
              render() {
                  return <div>颜色:{this.context?.color}</div>
              }
          }
          export default Wrapper
      
    • 4,如果当前组件需要消费多个父组件的context值,子组件需要使用context.Consumer形式获取context值,而不是static contextType = context,如下图,子组件ProfilePage获取ThemeContext与UserContext的值。当然,对于函数组件,需要使用Context,我们可以使用context.Consumer处理 image.png

14,React组件懒加载

  • React组件懒加载:类似于webpack中的import(src).then,React中也具有组件懒加载的方法,即React.lazy引入组件,Suspense组件包裹。注意该技术还不支持服务端渲染。 具体使用如下:
        import React, { Suspense } from 'react'
        // 1, 引入需要使用懒加载的组件,懒加载组件只支持默认导出,所以我们LazyComponent文件的导出还是使用export default LazyComponent;
        const LazyComponent = React.lazy(() => import('./LazyCom'))
    
        class SuspenseComponent extends React.Component {
            render() {
            // 2,Suspense包裹懒加载组件,同时可以给Suspense组件传入fallback,即在组件未完全加载时,暂时显示fallback中的内容
                return <Suspense fallback={<div>loading...</div>}>
                    <LazyComponent />
                </Suspense>
            }
        }
    
        export default SuspenseComponent
    

15,React错误边界

  • 错误边界:错误边界是一种React组件,他可以捕获子组件树中JavaScript错误,并在出错同时渲染错误UI替换出错的子组件树。 注意:错误边界只会在render中,生命周期函数中,constructor中捕获错误,而事件中(onclick等)错误不会捕获,因为错误边界期望捕获是渲染过程的错误,而不是事件处理出现的错误,事件处理出现的错误可以使用trycatch捕获,除了事件中的错误,异步函数中错误,错误边界组件自身错误以及服务端渲染产生的错误也不会被错误边界捕获
  • 如何生成错误边界:错误边界组件只能是类组件,我们只需要在组件内部添加错误捕获的钩子函数ComponentDidCatch(error,errorinfo){}或者静态方法static getDerivedStateFromErro(error){}即可,注意:当捕获错误时,一般错误边界需要渲染备用UI,所以上面两个方法一般都会去告知state发生错误,然后渲染备用UI。
  • 错误边界具体使用过程:react16之后,只要有组件抛出错误未被捕获,整个React组件树都会被卸载,如果我们期望某个组件抛出错误不会影响到其他组件,应用错误边界将其包裹。
    import React from 'react'
    // 1,一个将在state.count 计数到5抛出错误的组件
    class ErrorCom extends React.Component {
        constructor(props) {
            super(props)
            this.state = { count: 0 }
        }
        render() {
            if (this.state.count === 5) throw new Error('getErr!')
            return <div onClick={() => this.setState({ count: this.state.count + 1 })}>
                当count为5时将抛出错误,当前count:{this.state.count}
            </div>
        }
    }
    // 2,错误边界组件
    class ErrorBoundary extends React.Component {
        constructor(props) {
            super(props)
            this.state = { hasError: false }
        }
        // static getDerivedStateFromError(err) {
        //     return { hasError: true }
        // }
    
        // 2.1,getDerivedStateFromError 与 componentDidCatch 任取其一都可以形成错误边界,捕获到错误需要渲染备用UI,这里使用componentDidCatch捕获错误
        componentDidCatch(err, errInfo) {
            this.setState({ hasError: true })
        }
        render() {
            // 2.2,捕获错误渲染备用UI,未捕获正常渲染
            return this.state.hasError ? <div>发现错误</div> : this.props.children
        }
    }
    
    class Wrapper extends React.Component {
        render() {
            // 3,错误边界内部出现的错误捕获后渲染不会影响到其他组件功能
            // 而如果未使用错误边界的组件抛出错误,那么整个React组件树都会被卸载
            // 所以下面第一个被错误边界包裹的<ErrorCom />抛出错误被错误边界捕获,关闭错误页面,会发现下面未被错误边界包裹的 <ErrorCom />依然可用
            // 而反之,未被错误边界包裹的 <ErrorCom />抛出错误,因没有错误边界捕获该错误,将导致整个组件树卸载,关闭错误页面,会发现被错误边界包裹的<ErrorCom />已经无法渲染了
            return <div>
                <ErrorBoundary ><ErrorCom /></ErrorBoundary>
                <ErrorCom />
            </div>
        }
    }
    
    export default Wrapper
    

16,react中的Refs

  • Refs的作用:Refs提供了一种方式,允许我们访问实际Dom节点或者类组件创建的react元素。
  • 如何创建Refs:使用React.createRef方法 或者回调函数方式创建Refs。react会在组件挂载时给ref.current挂载对应dom或者react元素,并在组件卸载时传入null,ref总会在ComponentDidmount和ComponentDidUpdate之前更新,所以我们可以在这两个生命周期函数中使用它。
    • 1,React.createRef:如下代码即使用React.createRef形式获取实际dom input元素,并在组件完成挂载时自动获取焦点。
      import React from 'react'
      
      class TextInput extends React.Component {
          constructor(props) {
              super(props)
              // 1,使用React.createRef()创建ref
              this.inputRef = React.createRef()
          }
          componentDidMount() {
              // 3,使用ref.current 获取当前使用ref的元素,这里就是dom节点input
              this.inputRef.current.focus()
          }
          render() {
              // 2,ref 分配给当前input元素
              return <input ref={this.inputRef} />
          }
      }
      
      export default TextInput
      
    • 2,回调函数形式:如下代码即使用回调函数形式获取实际dom input元素,并在组件完成挂载时自动获取焦点。
      import React from 'react'
      
      class TextInput extends React.Component {
          constructor(props) {
              super(props)
              // 1,this上挂载即将使用ref获取的input节点,初始设置为null
              this.inputRef = null
          }
          componentDidMount() {
              // 3,访问this.inputRef获取当前dom节点,注意如果使用React.createRef()创建ref,我们使用的是this.inputRef.current获取对应dom节点
              this.inputRef.focus()
          }
          render() {
              // 2,使用ele => this.inputRef = ele回调函数形式将input节点交给this.inputRef,这样我们即可在访问this.inputRef获取当前dom节点。
              return <input ref={ele => this.inputRef = ele} />
          }
      }
      
      export default TextInput
      
  • ref使用注意点:我们不可以在函数组件上使用ref,即现在有函数组件内部的类组件或者dom上使用ref,这是因为函数组件没有实例,上面两个例子都是使用ref访问真实dom,当然我们也可以在类组件上使用,获取类组件实例,像这样<MyClassComponent ref={this.refMyClass} />

17,React中的ref转发

  • 为什么需要ref转发:正常我们在当前组件内创建ref,并在当前组件中使用它,自然不需要ref转发,但是如果我们期望在该组件的父组件访问到内部ref,我们可以使用ref转发技术实现,即在父组件使用React.createRef()或者回调函数创建ref,并使用ref转发技术将ref传递给内部组件,使得ref能够访问内部组件或者内部组件中的元素,这样我们就可以在父组件使用该ref。 如果期望直接使用props传递ref的话,这是行不通的,因为React中对ref与key处理都是单独进行的,并不会与props一起处理,所以我们不能使用props传递ref,因为ref根本不在props中。
  • 如何实现ref转发:使用React.forwardRef对父组件下一层的组件进行包裹,React.forwardRef接收父组件传递过来的ref,交给下一层组件实现ref转发,如下代码:
        import React from 'react'
    
        // 1,TextInput 内部的input元素期望在最外层父组件获取到,所以我们需要将ref传到TextInput组件内部
        class TextInput extends React.Component {
            render() {
                return <input ref={this.props.forwardRef} />
            }
        }
        // 2,使用React.forwardRef包裹TextInput组件,获取父组件传入的ref,交给TextInput的一个props属性forwardRef中,这样TextInput内部就可以通过该props属性forwardRef获取到最外层父组件创建的ref
        const WrapperRef = React.forwardRef((props, ref) => {
            return <TextInput forwardRef={ref} />
        })
        // 3,Wrapper作为最外层父组件,使用回调函数形式创建ref,通过React.forwardRef创建的WrapperRef组件将ref交给内部元素
        class Wrapper extends React.Component {
            componentDidMount() {
                this.inputRef.focus()
            }
            render() {
                return <WrapperRef ref={ele => this.inputRef = ele} />
            }
        }
    
        export default Wrapper
    

18,React.Fragment,React.memo,React.isValidElement的使用

  • React.Fragment使用场景:react组件可能期望返回多个元素,但是由于return只能返回一个元素,所以我们可能会使用div将多个元素包裹,但我们可能不期望多出这个div,那么我们可以将div替换成React.Fragment即可。

  • React.Fragment注意点:<React.Fragments></React.Fragments>可以简写成<></>,同时React.Fragment只支持传入处理key,其他属性目前还不支持。

  • React.Fragment具体使用:

    import React from 'react'
    
    class Context extends React.Component {
        constructor(props) {
            super(props)
            this.state = { list: [1, 2, 3, 4, 5, 6] }
        }
        render() {
            // 1,<> 对  {this.state.list.map((e, i) => <li key={i}>{e}</li>)} 进行包裹,最终渲染结果中只会出现 {this.state.list.map((e, i) => <li key={i}>{e}</li>)}
            return <>
                {this.state.list.map((e, i) => <li key={i}>{e}</li>)}
            </>
        }
    }
    
    class Wrapper extends React.Component {
        render() {
            return <ul>
                <Context />
            </ul>
        }
    }
    
    export default Wrapper
    
  • React.memo:React.memo与React.pureComponent相似,都是判断props是否发生变化决定组件是否重新渲染,不过React.memo适用于函数组件。

  • React.memo使用:React.memo接收两个参数,第一个参数需要性能优化的函数组件,第二个参数为自定义props比较函数,该函数会被注入当前props与新的props,我们可以根据其进行比较,如果不传该比较函数,则props不发生变化,函数组件不重新渲染,否则重新渲染。最终React.memo返回性能优化后的函数组件我们直接使用即可。注意:比较函数返回true则表示当前函数组件不需要重新渲染,false则渲染,这与shouldComponentUpdate刚好相反

  • React,memo具体使用:

    function MyComponent(props) {
        return <div>{this.props?.name}</div>
    }
    function isEqualFn(preProps, nextProps) {
        if (preProps.name === nextProps.name) return true
        return false
    }
    const MyComponent_ = React.memo(MyComponent, isEqualFn)
    
  • React.isValidElement:React.isValidElement(element)用于验证传入是否是React元素,返回true or false

19,this.forceUpdate 使用

this.forceUpdate 强制当前组件调用render方法,注意:该方法会跳过shouldComponentUpdate,即即使当前组件shouldComponentUpdate方法返回fasle,也会重新render,但是其子组件会正常调用生命周期函数,包括shouldComponentUpdate。一般我们应该尽量避免使用该方法。

20,react-router

  • react-router实现:react-router分为HashRouter与BorwserRouter,其对应路由实现的hash模式与history模式,两种模式实现可以参考《前端路由中的hash模式与history模式》

  • Route中三个属性component,render,children区别:

    • component:component接收是一个组件地址而不是具体组件(比如component={App}而不是component={}),之后会使用React.createElement去创建具体的组件去应用

      • 1,直接传入component当前组件名:如下即调用React.cretateElement(Home)创建对应组件。

         <Route path='/home' component={Home} />
        
      • 2,如果component传入内联函数(内联函数返回组件)会出现一些问题,即每次更新都会调用内联函数创建新的组件,导致每次diff都不是同一个组件,从而导致每次更新都会卸载原组件,挂载新组件,如果期望使用内联函数,可以使用render属性解决这个问题

            <Route path='/home' component={()=><Home idx={1}/>} />
        
    • render:render接受函数,所以我们可以在render中使用内联函数形式处理路由组件,且不会出现component接受路由内联函数的问题。

          <Route to='/home' render={props => <Home {...props}/>} />
          <Route to='/list' render={props => <List><Detail /></List>} />
      
    • children:children接受函数,与render区别是,不管当前路由是否匹配该路径,children中函数返回内容都将render,且如果匹配路径与当前组件路径不一致,那么组件props(如果主动添加props)中的match属性将为null

          {/* 由于children 不管路径是否匹配总是渲染,因此我们可以根据props.match 做一些逻辑处理,因为路径不匹配时match为null,匹配时match才有值 */}
          {/* 如下,路径不匹配展示Child1组件,匹配展示Child2组件 */}
          <Route path='/Child1' children={props => props.match ? <Child1 /> : <Child2 />} />
      
    • 官网:三者优先级关系是component>render>children

  • react-router中NavLink使用:如果期望实现Link命中高亮,我们可以使用NavLink替代Link,同时使用activeClassName挂在当前NavLink中,同时全局css设置该activeClassName对应命中当前NavLink时的样式即可。注意:activeClassName为路由组件库自己实现,不要写自定义的类名。

     <NavLink to='/profile' activeClassName='selected'>to profile</NavLink>
     // 记得在全局css样式定义对应命中NavLink样式
     //.selected {
     //       backgroundColor:'blue'
     //}
    
  • Route中的Switch属性作用:如下代码,如果当前url路径为/,那么两个路由页面都将展示,因为默认情况react-router是贪婪匹配,即即使匹配到对应路由,但还是会继续执行后面的路由代码,匹配成功则继续展示,直到匹配完最后一个,而在Route集合外面包裹Switch组件则可以避免这种情况,只要匹配到一个路由,即停止匹配。

        <Route path='/' component={Default}/> 
        <Route path='/home' component={Home}/> 
    
  • Route中的exact作用:带有exact只有loacation.pathname与path精准匹配时才展示。如下代码如果当前url路径为/home,则不会匹配到path='/'的Route。

        <Route exact path='/' component={Default}/> 
        <Route path='/home' component={Home}/> 
    
  • react-router基本使用:

    import React from 'react'
    import { BrowserRouter as Router, Link, NavLink, Switch, Route, Redirect } from 'react-router-dom'
    class Home extends React.Component {
        render() {
            return <div>
                <h1>home</h1>
                {/* 12,函数式路由跳转使用props.location.state获取上一个页面跳转时传入参数 */}
                {this.props.location.state?.info ? <span>函数式路由跳转中携带的参数信息:{this.props.location.state.info} </span> : null}
            </div>
        }
    }
    // 嵌套路由List
    class List extends React.Component {
        backHome = () => {
            // 11,push函数式路由跳转,且第二个参数为携带参数,将在跳转到的页面props.location.state中获取
            // props.match.params是获取路径中参数
            // props.location.state是获取手动传入的参数
            this.props.history.push('/home', { info: `从List返回到Home路由` })
        }
        render() {
            console.log(this.props);
    
            return <div>
                <h1>List</h1>
                {/* 8,link 为声明式路由跳转,类似于a标签点击跳转 */}
                <Link to='/list/detail/fox/1'>to list detail of fox-1</Link>&nbsp;&nbsp;&nbsp;&nbsp;
                {/* 9,link 路由跳转且路径带参/cat/2 */}
                <Link to='/list/detail/cat/2'>to list detail of cat-2</Link>&nbsp;&nbsp;&nbsp;&nbsp;
                <button onClick={this.backHome}>go Home</button>
                <h2>Detail:</h2>
                {/* 10,路由页面通过/:animal/:id匹配带参路径,并在props.match.params中获取路径参数 */}
                <Route path='/list/detail/:animal/:id' render={props => <div>{props.match.params.animal}{props.match.params.id}</div>} ></Route>
            </div>
        }
    }
    
    class Index extends React.Component {
        render() {
            return <Router>
                <div style={{ borderWidth: 5, borderColor: 'rosybrown', borderStyle: 'solid', padding: 20, marginBottom: 20 }}>
                    {/* 1,使用NavLink配合自带activeClassName实现命中高亮 */}
                    <NavLink to='/' activeClassName='selected'>to default </NavLink>&nbsp;&nbsp;&nbsp;&nbsp;
                    <NavLink to='/home' activeClassName='selected'>to home </NavLink>&nbsp;&nbsp;&nbsp;&nbsp;
                    <NavLink to='/list' activeClassName='selected'>to list</NavLink>
                </div>
                <div style={{ borderWidth: 5, borderColor: 'rosybrown', borderStyle: 'solid', padding: 20 }}>
                    {/* 2,Router中路径匹配为贪婪模式(挨个匹配),比如当前url中路径为/home,那么如果不做特别设置,path为/的路由页面与path为/home的路由页面都将会被匹配并展示 */}
                    {/* 3,如果只希望匹配一个就停止,我们需要再Route外面包上Switch */}
                    <Switch>
                        {/* 4,我们可以通过设置Route中的exact属性,让当前路由完全匹配url路径(包括路径携带参数)才显示,如果当前路由匹配为贪婪模式,那么除了被设置的路由页面,其他路由依旧会进行贪婪匹配 */}
                        <Route exact path='/' render={props => <div>default route</div>} />
                        {/* 5,component 形式渲染路由组件,虽然没有显式将路由api传入Home,但是我们可以在Home中通过props获取到路由相关api */}
                        <Route path='/home' component={Home} />
                        {/* 6,render形式渲染路由组件List,将props传递过去,props中包含路由相关的api,不传的话子组件接收不到这些api */}
                        <Route path='/list' render={props => <List {...props} />} />
                        <Route path='/notFount' render={props => <div>404</div>} />
                        {/* 7,如果匹配路径当前没有比如当前匹配路径为/asdasdasd,那么最后会被Redirect即重定向到/notFount路由页面*/}
                        {/* 一般Redirect需要配合Switch使用,否则贪婪匹配总是可以匹配到最后Redirect,导致重定向 */}
                        <Redirect from='*' to='/notFount' />
                    </Switch>
                </div>
            </Router>
        }
    }
    
    export default Index
    

21,为什么componentDidMount对比componentWillMount是执行组件与服务器通信最佳地点。

  • componentDidMount保证了获取到数据时候,当前组件已经处于挂载状态,直接操作dom也是安全的,componentWillMount做不到这点。
  • 组件使用服务器端渲染,componentWillMount会调用两次,服务器端与浏览器端各调用一次,而componentDidMount能保证任何情况下只调用一次,不会发送多余数据请求。
  • componentWillMount能做的,构造函数也能做,所以componentWillMount并非是必须的一个生命周期函数,即将废弃。

22,react中ssr使用场景与构建步骤

  • ssr使用场景:

    • 1,需要提高首屏加载速度,解决白屏问题:单页面应用浏览器接收到该页面,但页面内容此时为空,需要通过js加载出来页面内容,因此首屏加载速度会比较慢,在js加载页面内容出现之前出现白屏

    • 2,需要进行SEO优化:单页面应用存在的问题是内容都是通过js加载出来,而搜索引擎爬虫不会读取js输出内容,而服务端渲染直接返回渲染好的html,此时即可保证搜索引擎爬虫可以正确读取当前页面内容

  • ssr构建步骤

    • 1,将react应用打包生成对应的html文件与打包后的js文件(此时html文件引入了打包后的js文件)

    • 2,使用renderToString将react组件转换成html字符串,此时读取react打包后的html文件,将根节点(root节点下面的内容)内容替换成renderToString生成的html,然后发送给客户端(此时没有对dom元素进行事件绑定,renderToString仅是将react组件渲染成html字符串)

    • 3,[同构] :客户端解析服务端发送过来的数据html文件,解析到步骤1中html中引用react代码(react应用打包后的js文件)完成并运行,此时react会检查当前根节点(root节点)下的dom和react要生成dom结构是否一致,如果一致,react就不再会重新生成该dom结构,如果不一致,react会去覆盖不一致的dom结构,在此期间,js也会完成dom事件的绑定

  • react同构:客户端与服务端使用同样的组件,服务端负责首次渲染dom内容(renderToString),客户端运行js代码完成dom行为交互(事件绑定)

  • react-dom中renderToString与renderToStaticMarkup区别:

    • renderToString:将react组件转化成html字符串,同时dom节点会带有data-react-id属性,第一个节点还有额外的data-checksum属性,这是属性其实是为了提高react在渲染时的性能

      • data-react-id 标识服务端渲染组件,react能够直到服务端渲染后的组件是哪一个dom元素,从而进行管理

      • data-checksum 判断组件传入数据是否改变,数据改变引起data-checksum变化,data-checksum变化意味着组件需要重新渲染

    • renderToStaticMarkup:将react组件转化成html字符串,但不会有额外属性,节省html字符串大小

参考整理来源 :

14121