React面试复习一:React的基本使用

124 阅读9分钟

0. React hooks面试题

useEffect

useEffect如果有返回值,返回的函数是在执行下一次useEffect之前或者组件卸载时调用。

image.png useEffect在组件第一次挂载时执行一次,输出0.
多次点击加一按钮之后,输出

更新0
1

更新1
2

更新2
3

1. JSX如何进行条件判断和列表渲染

  • if else
  • 三元运算符
  • 逻辑表达式 && ||

image.png

2. React事件

  • bind this
  • 关于event参数
  • 传递自定义参数

juejin.cn/post/684490…
为什么React类组件中需要使用bind来绑定this

image.png

2.1 React事件和DOM事件的区别

React16 事件绑定在document上,React 17之后 React事件不再被绑定在document上,而是绑定在root容器上!!
回顾:为什么React16的时候stopPropagation无效了

image.png React17 事件机制

事件委托的节点从 React16 的 document 更改为 React17 的 React 树的根 DOM 容器。

这一改动的出发点是如果页面中存在多个 React 应用,由于他们都会在顶层document注册事件处理器,如果你在一个 React 子应用的 React 事件中调用了e.stopPropagation(),无法阻止事件冒泡到外部树,因为真实的事件早已传播到document

而将事件委托在 React 应用的根 DOM 容器则可以避免这样的问题,减少了多个 React 应用并存可能产生的问题,并且事件系统的运行也更贴近现在浏览器的表现。

在 React16 中,对 document 的事件委托都委托在冒泡阶段,当事件冒泡到 document 之后触发绑定的回调函数,在回调函数中重新模拟一次 捕获-冒泡 的行为,所以 React 事件中的e.stopPropagation()无法阻止原生事件的捕获和冒泡,因为原生事件的捕获和冒泡已经执行完了。

在 React17 中,对 React 应用根 DOM 容器的事件委托分别在捕获阶段和冒泡阶段。即:

  • 当根容器接收到捕获事件时,先触发一次 React 事件的捕获阶段,然后再执行原生事件的捕获传播。所以 React 事件的捕获阶段调用e.stopPropagation()阻止原生事件的传播。

  • 当根容器接受到冒泡事件时,会触发一次 React 事件的冒泡阶段,此时原生事件的冒泡传播已经传播到根了,所以 React 事件的冒泡阶段调用e.stopPropagation()不能阻止原生事件向根容器的传播,但是能阻止根容器到页面顶层的传播。

3. React表单

  • 受控组件
  • 非受控组件

受控组件

受控组件:value + onChange事件
value={this.state.name} onChange={()=>{this.setState({name: e.target.value})}}

  • input textarea select 用value
  • checkbox radio 用checked

非受控组件

  • ref

  • defaultValue 和 defaultChecked

  • 手动操作DOM元素

image.png

image.png

image.png File类型的input,使用受控组件无法处理,必须使用DOM操作。
某些富文本编辑器,需要传入DOM元素。

  • 优先使用受控组件
  • 必须操作DOM时,再使用非受控组件

zh-hans.reactjs.org/docs/uncont… 官方 非受控组件

4. 组件通讯

  • props传递数据
  • props传递函数
  • 类型检查

组件间通信一般就是四种情况,父传子,子传父,跨多级传递,同级传递。
30分钟精通十种React组件之间通信的方法

总结

  • 父组件 => 子组件:

    1. Props
    2. Instance Methods
  • 子组件 => 父组件:

    1. Callback Functions
    2. Event Bubbling
  • 兄弟组件之间:

    1. Parent Component
  • 不太相关的组件之间:

    1. Context
    2. Portals
    3. Global Variables
    4. Observer Pattern
    5. Redux等

5. setState

  • 不可变值
  • 可能是异步更新
  • 可能会被合并

setState更改的时候应该使用不可变值。不能直接对state里面的数组进行push pop shift unshift splice等会直接改变当前数组的函数。应该使用一个临时的值,然后直接去替换原来的值。 image.png

关于setState的异步更新和合并,主要是性能的问题。 React setState合并和批量处理

一、State的更新什么时候是同步,什么时候是异步

 正如我们所知,react中使用setState更新state,而state的更新大多数情况下是异步的,但是有些情况却是同步的。
在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

二、为什么是异步

 如果setState是同步更新state,而state的更新又会触发组件的重新渲染,那么每次setState都会渲染组件,这对性能是很大的消耗。所以react进行了setState的合并和批量延迟更新,正如官网所述:

作者:xiaowoniu
链接:juejin.cn/post/684490…

React--setState 同步与异步更新
setState第一个参数可以是对象或者函数,如果是用对象来更新,那么就会被合并,并且是异步的。如果使用回调函数来更新的话,那么就是同步的。
为什么会被合并呢,因为首先对象形式下,它是异步的,且类似于使用 Object.assign({count: 1}, {count:1}) 合并的结果就是1

this.setState({
    count: count + 1 //相当于 count: 1,因为count初始是0
})
this.setState({
    count: count + 1 //相当于 count: 1,因为前一个是异步的,还没更新,count初始是0
})
this.setState({
    count: count + 1 //相当于 count: 1,因为前一个是异步的,还没更新,count初始是0
})

setState第二个参数是一个函数,作为更新完state之后的回调函数使用,类似于vue中的nextTick。待验证)

6. React生命周期

  • componentWillMount 在渲染前调用。
  • componentDidMount 在第一次渲染后调用。
  • componentWillReceiveProps 在组件接收到一个新的props时被调用。这个方法在第一次渲染时不会被调用。
  • shouldComponentUpdate 返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。
  • componentWillUpdate 在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。
  • componentDidUpdate 在组件完成更新后立即调用。在初始化时不会被调用。
  • componentWillUnmount 在组件移除之前调用

7. Portals

  • 组件默认会按照既定层次嵌套渲染
  • 如何让组件渲染到父组件以外?

image.png

这是一个固定定位的内容 image.png 此时这个div已经不在root容器下了,而在body下。

一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

image.png

8. Context

createContext,函数式组件使用 useContext 这个hook获取。 类式组件通过.Provider

zh-hans.reactjs.org/docs/contex…

9. 异步组件

  • import()
  • React.lazy
  • React.Suspense

image.png

10. 性能优化

  • shouldComponentUpdate
  • PureComponent 和 React.memo
  • 不可变值 immutable.js

10.1 SCU基本用法

React默认情况下 只要父组件有更新,那么子组件也会跟着更新。
想象这样一个场景: 一个TodoList组件,分为Header,List,Footer三个子组件。
往里添加新的todolist的时候,父组件必然更新,但是此时Footer组件中的componentDidUpdate()生命周期函数也会执行,进行更新。

image.png

默认情况下 shouldComponentUpdate()是默认返回true的 —— React默认重新渲染所有子组件

shouldComponentUpdate(){
    return true
}

React为什么要这么做呢?
因为如果开发人员在使用setState的时候 违反了不可变值规则 导致出现bug
例如在修改数组的时候,使用了会改变原数组的push方法。

this.state.list.push(1)
this.setState({
    list: this.state.list
})

这样的话 list原本就被改变了,相当于在setState的时候,给list赋值的时候是两边相等的。这样的话,就不会出发SCU

shouldComponentUpdate(nextProps, nextState){
    if(_.isEqual(nextProps.list, this.props.list)){
    // _.isEqual 做对象或者数组的深度比较(一次性递归到底)不建议使用 比较耗费性能
    //不会触发 因为此时list本来就是相等的 这样就会导致在添加todoList的时候 不触发渲染。
        return true
    }else{
        return false
    }
        
}

因此 React为了防止因为开发人员的错误产生的bug,干脆就都放行,把性能优化的实现交给开发人员自身。

总结:

  • SCU默认返回true, 即React默认重新渲染所有子组件
  • 必须配合不可变值一起使用
  • 可先不用SCU,有性能问题再考虑使用

10.2 PureComponent和memo

  • PureComponent,SCU中实现了浅比较
  • memo是函数组件中的PureComponent
  • 浅比较已经适用大部分情况(尽量不要使用深度比较)
class List extends React.PureComponent

React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

React.memo 为高阶组件

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。

注意

与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

11. 高阶组件 关于组件公共逻辑的抽离

React构造函数中为什么要写super(props)

  • mixin,已被React弃用
  • HOC 高阶组件
  • Render Props

11.1 高阶组件基本用法

// HOC不是一种功能,而是一种工厂模式
const HOCFactory = (Component)=>{
    class HOC extends React.Component{
        // 在此定义多个组件的公共逻辑
        render(){
            return <Component {...this.props} />
        }
    }
    return HOC
}

const EnhancedComponent1 = HOCFactory(WrappedComponent1)

const EnhancedComponent2 = HOCFactory(WrappedComponent2)

import React from 'react'

// 高阶组件
const withMouse = (Component) => {
    class withMouseComponent extends React.Component {
        constructor(props) {
            super(props)
            this.state = { x: 0, y: 0 }
        }
  
        handleMouseMove = (event) => {
            this.setState({
                x: event.clientX,
                y: event.clientY
            })
        }
  
        render() {
            return (
                <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
                    {/* 1. 透传所有 props 2. 增加 mouse 属性
                        这里的props可以通过外部调用高阶组件的时候来传递
                        比如<HOCDemo a="100" />
                    */}
                    
                    <Component {...this.props} mouse={this.state}/>
                </div>
            )
        }
    }
    return withMouseComponent
}

const App = (props) => {
    const a = props.a
    const { x, y } = props.mouse // 接收 mouse 属性
    return (
        <div style={{ height: '500px' }}>
            <h1>The mouse position is ({x}, {y})</h1>
            <p>{a}</p>
        </div>
    )
}

export default withMouse(App) // 返回高阶函数

redux connect 是高阶组件

思考:Vue如何实现高阶组件?

11.2 render props

image.png

  • HOC 模式简单 增加组件层级
  • Render Props 代码简洁,但是学习成本比较高
  • 按需使用

12. Redux

  • 基本概念
  • 单项数据流
  • react-redux
  • 异步action
  • 中间件

如何在函数组件中使用redux