• 前端常见面试题之react高级特性(Portals、Context、HOC等)


    1. Portals将子组件渲染到父组件以外的DOM节点上

    在React中,Portals是一种机制,可以将子组件渲染到父组件以外的DOM节点上。通常情况下,React组件会被渲染到它们的父组件的DOM结构中,但有时我们希望将某个组件渲染到DOM结构的其他位置或根节点上,这时就可以使用Portals。

    Portals的主要用途包括但不限于以下几点:

    1. 在应用中的任意位置渲染组件,无需将所有组件都嵌套在同一个层级结构中。
    2. 在模态框或弹出窗口等场景下,将组件渲染到最顶层的DOM节点上,确保它们处于最前面且不受其他布局影响。
    3. 在复杂布局下,将组件渲染到特定的DOM节点上,实现更灵活的布局控制。

    举个例子,假设我们有一个模态框组件Modal,并希望将其渲染到页面的最顶层节点上,可以使用Portals实现:

    import { createPortal } from 'react-dom';
    
    const modalRoot = document.getElementById('modal-root');
    
    class Modal extends React.Component {
      render() {
        return createPortal(
          this.props.children,
          modalRoot
        );
      }
    }
    
    // 在父组件中使用Modal组件
    class App extends React.Component {
      render() {
        return (
          <div>
            <h1>Hello, React!</h1>
            <Modal>
              <div>
                This is a modal content.
              </div>
            </Modal>
          </div>
        );
      }
    }
    
    ReactDOM.render(<App />, document.getElementById('app'));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在上面的例子中,Modal组件使用Portals将其子组件渲染到了id为’modal-root’的DOM节点上,而不是直接嵌套在App组件内部。这样就实现了在页面的不同位置渲染组件的效果。

    2. Context组件树中传递数据

    React中的Context是一种全局状态管理的方法,它允许在组件树中传递数据,而不必一层层地手动传递props。通常用于在组件之间共享一些全局的信息。

    Context包括两部分:Provider和Consumer。Provider提供数据并传递给子组件,而Consumer消费Provider提供的数据。

    一个简单的例子是,在一个多层嵌套的组件结构中,如果需要在底层组件访问或修改顶层组件的状态数据,使用Context就会很方便。

    // 创建一个Context
    const MyContext = React.createContext();
    
    // 顶层组件提供数据
    class ParentComponent extends React.Component {
      state = {
        message: 'Hello from ParentComponent!',
      };
    
      render() {
        return (
          <MyContext.Provider value={this.state.message}>
            <ChildComponent />
          </MyContext.Provider>
        );
      }
    }
    
    // 底层组件消费数据
    const ChildComponent = () => {
      return (
        <MyContext.Consumer>
          {value => <p>{value}</p>}
        </MyContext.Consumer>
      );
    }
    
    // 渲染
    ReactDOM.render(<ParentComponent />, document.getElementById('root'));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在此例中,ParentComponent提供了一个message数据,并通过MyContext.Provider传递给ChildComponent,ChildComponent通过MyContext.Consumer来消费并显示message的值。

    在函数式组件中可以使用useContext来实现,具体可以看看这篇react中的context实现深层数据传递

    3. react中如何加载异步组件

    在 React 中加载异步组件通常使用 React.lazy() 方法。React.lazy() 接收一个函数,这个函数需要动态 import 这个组件,返回一个 Promise,React 将在组件加载完成后渲染它。

    举例说明:

    import React, { Suspense } from 'react';
    
    const AsyncComponent = React.lazy(() => import('./AsyncComponent'));
    
    function App() {
      return (
        <div>
          <h1>异步组件加载示例</h1>
          <Suspense fallback={<div>Loading...</div>}>
            <AsyncComponent />
          </Suspense>
        </div>
      );
    }
    
    export default App;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在上面的示例中,我们创建了一个 App 组件,通过 React.lazy() 方法加载了一个异步组件 AsyncComponent。在 Suspense 组件中,我们可以设置一个 fallback 属性,在组件加载完成前会显示该属性的内容。

    需要注意的是,React.lazy() 目前只支持默认导出的组件,所以被异步加载的组件需要使用默认导出。

    4. shouldComponentUpdate有什么用

    shouldComponentUpdate是React组件中一个重要的生命周期函数,用于控制组件的更新频率。当组件的props或state发生变化时,React会调用shouldComponentUpdate方法来判断是否需要重新渲染组件。如果shouldComponentUpdate返回false,React则会阻止组件的更新,从而提高性能。

    举个例子,假设有一个列表组件List,当用户点击某个按钮时,会向列表中添加一个新的项。但是,如果列表中已经包含相同的项,则不需要更新列表。在这种情况下,我们可以通过shouldComponentUpdate来判断新的项是否已经存在于列表中,如果存在则返回false,否则返回true。

    class List extends React.Component {
      shouldComponentUpdate(nextProps) {
        // 检查新的项是否已经存在于列表中
        if (this.props.items.includes(nextProps.newItem)) {
          return false;
        }
        return true;
      }
    
      render() {
        return (
          <ul>
            {this.props.items.map(item => (
              <li key={item}>{item}</li>
            ))}
          </ul>
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在上面的例子中,当shouldComponentUpdate返回false时,新的项不会被添加到列表中,从而避免不必要的组件更新。这样可以减少组件的渲染次数,提高性能。

    5. state中值的不可变性

    在React中,保持原有state值的不可变性是为了确保组件的可预测性和性能。如果直接修改state中的值,可能会导致不可预测的行为,因为React可能无法正确地检测到state的变化,进而无法正确地重新渲染组件。

    通过确保state的不可变性,可以减少出现bug的可能性,因为我们可以清晰地追踪state的变化。此外,React使用了一个称为“浅比较”的机制来检测state的变化,如果直接修改state中的值,可能会导致React无法正确地检测到state的变化,导致组件不会重新渲染,从而影响性能

    因此,在使用setState更新state时,应该始终创建一个新的对象或数组,而不是直接修改原始state,以确保state的不可变性和组件的可靠性。

    举例来说,假设有一个组件的state中有一个数组:

    this.state = {
      numbers: [1, 2, 3, 4, 5]
    }
    
    • 1
    • 2
    • 3

    如果我们想将数组中第一个元素改为6,正确的做法是先创建一个新的数组,然后再更新state:

    const newNumbers = [...this.state.numbers];
    newNumbers[0] = 6;
    
    this.setState({numbers: newNumbers});
    
    • 1
    • 2
    • 3
    • 4

    如果直接修改state中的数组,例如:

    this.state.numbers[0] = 6;
    this.setState({numbers: this.state.numbers});
    
    • 1
    • 2

    这种方式会直接修改原有的state中的数组,可能导致React无法正确检测到state的变化,从而导致界面没有得到正确的更新。

    这是因为React在进行state更新时,会进行引用比较来确定是否需要重新渲染组件。如果直接修改state中的数组而没有保持其不可变性,即没有创建一个新的数组来替换原有的数组,React可能会认为引用未发生改变,从而不会触发重新渲染。这样就会导致界面没有得到正确的更新,因为实际上state中的数据已经发生了变化,但React并没有正确检测到这一点。因此,在React中,强调保持原有值的不可变性是为了确保React能够正确地检测到state的变化并触发组件的重新渲染。

    因此我们在操作数组时,要使用返回新数组的方法

    常用的JS数组方法中,会返回新数组的方法有:

    1. map()
    2. filter()
    3. concat()
    4. slice()
    5. flat()
    6. reduce()

    修改原数组的方法有:

    1. pop()
    2. push()
    3. shift()
    4. unshift()
    5. splice()
    6. reverse()
    7. sort()

    6. HOC和Render Props代码复用和逻辑分离

    HOC(Higher Order Component)和Render Props都是React中常用的模式,用于组件之间的代码复用和逻辑分离。

    HOC是一个函数,接受一个组件作为参数并返回一个新的增强组件。通过HOC可以将通用的逻辑封装到一个函数中,然后在多个地方重复使用。例如,下面是一个HOC,用于给组件添加loading状态:

    const withLoading = (WrappedComponent) => {
      return class WithLoading extends React.Component {
        state = {
          loading: true
        }
    
        componentDidMount() {
          setTimeout(() => {
            this.setState({ loading: false });
          }, 2000);
        }
    
        render() {
          return this.state.loading ? <div>Loading...</div> : <WrappedComponent {...this.props} />;
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    Render Props则是通过组件的props将函数传递给子组件,子组件可以调用该函数来获取值或者逻辑。通过Render Props可以更加灵活地共享代码逻辑。下面是一个Render Props的示例,用于共享鼠标坐标:

    class MouseTracker extends React.Component {
      state = { x: 0, y: 0 };
    
      handleMouseMove = (event) => {
        this.setState({ x: event.clientX, y: event.clientY });
      }
    
      render() {
        return (
          <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
            {this.props.children(this.state)}
          </div>
        );
      }
    }
    
    const App = () => (
      <MouseTracker>
        {({ x, y }) => (
          <div>
            <h1>Mouse coordinates:</h1>
            <p>X: {x}, Y: {y}</p>
          </div>
        )}
      </MouseTracker>
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在上面的例子中,MouseTracker组件通过props.children将函数传递给子组件,子组件可以通过调用该函数获取到鼠标坐标的值。

    7. redux

    Redux是一个用于JavaScript应用程序的开源状态管理库,它被设计为一个可预测状态容器,使得应用的状态管理更加可控并且易于调试。

    Redux的主要用途是管理应用程序的状态,例如用户的登录状态、页面的加载状态以及组件之间的通信等。通过Redux,我们可以将应用程序的状态统一管理在一个容器中,通过定义action、reducer和store来管理数据的流动,从而确保状态的一致性和可控性。

    举例说明,我们可以创建一个简单的计数器应用程序来演示Redux的用法:

    首先,我们定义一个action,例如增加计数器的动作:

    const increment = () => {
      return {
        type: 'INCREMENT'
      };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后,我们定义一个reducer来处理action,并更新应用程序的状态:

    const counterReducer = (state = 0, action) => {
      switch(action.type) {
        case 'INCREMENT':
          return state + 1;
        default:
          return state;
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    接着,我们创建一个Redux store,并将reducer注册到store中:

    import { createStore } from 'redux';
    
    const store = createStore(counterReducer);
    
    • 1
    • 2
    • 3

    最后,在应用程序中,我们可以通过dispatch一个action来更新应用的状态,并通过getState方法获取最新的状态值:

    store.dispatch(increment());
    console.log(store.getState()); // 输出1
    
    • 1
    • 2

    通过以上示例,我们可以看到Redux通过action和reducer来管理应用程序的状态,使得状态的变化变得可预测和容易管理。

    Action
    Reducer
    State
    View

    8.React-Redux

    React-Redux 是一个用于 React 应用程序的 JavaScript 库,用于管理应用程序状态并帮助在组件之间传递数据。React-Redux 绑定了 Redux 的状态管理功能与 React 组件,使得在 React 应用程序中使用 Redux 变得更加简单和高效。

    使用 React-Redux 有以下几个主要的优势:

    1. 提供了一个统一的状态管理中心,使得应用程序的状态更加集中化和易于维护。
    2. 可以避免将状态传递多个组件层级,通过 React-Redux 提供的 connect 函数,可以直接从状态管理中心获取数据。
    3. 帮助优化组件的性能,可以避免不必要的渲染和数据更新。

    使用 React-Redux 的基本步骤如下:

    1. 安装 React-Redux:在项目中安装 react-redux 包,可以使用 npm 或者 yarn 进行安装。
    2. 创建 Redux store:在项目中创建 Redux store,并定义相关的 reducer、action 和 middleware。
    3. 使用 Provider:在根组件中使用 Provider 组件,并将创建的 Redux store 作为 props 传递给 Provider。
    4. 使用 connect 函数:在需要访问 Redux store 数据的组件中,使用 connect 函数来连接组件和 Redux store,将状态作为 props 传递给组件。

    举例说明:

    // 定义 reducer
    const counterReducer = (state = 0, action) => {
      switch(action.type) {
        case 'INCREMENT':
          return state + 1;
        case 'DECREMENT':
          return state - 1;
        default:
          return state;
      }
    };
    
    // 创建 Redux store
    const store = createStore(counterReducer);
    
    // 创建 React 组件
    const Counter = ({ count, increment, decrement }) => (
      <div>
        <h1>{count}</h1>
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
      </div>
    );
    
    // 连接组件和 Redux store
    const mapStateToProps = state => ({
      count: state
    });
    
    const mapDispatchToProps = dispatch => ({
      increment: () => dispatch({ type: 'INCREMENT' }),
      decrement: () => dispatch({ type: 'DECREMENT' })
    });
    
    const ConnectedCounter = connect(mapStateToProps, mapDispatchToProps)(Counter);
    
    // 在根组件中使用 Provider
    const App = () => (
      <Provider store={store}>
        <ConnectedCounter />
      </Provider>
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    在以上的示例中,我们创建了一个简单的计数器应用程序,使用了 React-Redux 进行状态管理。通过 connect 函数连接了 Counter 组件和 Redux store,并使用 Provider 在根组件中传递了 Redux store。当点击 Increment 和 Decrement 按钮时,会分别触发对应的 action,从而更新组件中的状态。

    9. react-reducx异步action

    在React-Redux中使用异步action通常需要借助中间件,最常用的中间件是redux-thunk。redux-thunk允许action创建函数返回一个函数而不是一个action对象,这样就可以在返回的函数中进行异步操作。

    下面是一个使用redux-thunk的异步action的示例:

    // actions.js
    const fetchPostsRequest = () => {
      return {
        type: 'FETCH_POSTS_REQUEST'
      }
    }
    
    const fetchPostsSuccess = (posts) => {
      return {
        type: 'FETCH_POSTS_SUCCESS',
        payload: posts
      }
    }
    
    const fetchPostsError = (error) => {
      return {
        type: 'FETCH_POSTS_ERROR',
        payload: error
      }
    }
    
    export const fetchPosts = () => {
      return (dispatch) => {
        dispatch(fetchPostsRequest());
        fetch('https://jsonplaceholder.typicode.com/posts')
          .then(response => response.json())
          .then(data => {
            dispatch(fetchPostsSuccess(data));
          })
          .catch(error => {
            dispatch(fetchPostsError(error));
          });
      }
    }
    
    // reducer.js
    const initialState = {
      posts: [],
      loading: false,
      error: null
    }
    
    const postsReducer = (state = initialState, action) => {
      switch (action.type) {
        case 'FETCH_POSTS_REQUEST':
          return {
            ...state,
            loading: true
          }
        case 'FETCH_POSTS_SUCCESS':
          return {
            ...state,
            loading: false,
            posts: action.payload,
            error: null
          }
        case 'FETCH_POSTS_ERROR':
          return {
            ...state,
            loading: false,
            error: action.payload
          }
        default:
          return state
      }
    }
    
    export default postsReducer;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    在上面的示例中,fetchPosts函数是一个异步action创建函数,它返回一个函数,这个函数内部执行异步操作,获取数据后分发相应的成功或失败action。在reducer中根据不同的action类型更新state。

    需要注意的是,在创建store时要应用redux-thunk中间件,示例代码如下:

    // store.js
    import { createStore, applyMiddleware } from 'redux';
    import thunk from 'redux-thunk';
    import rootReducer from './reducers';
    
    const store = createStore(rootReducer, applyMiddleware(thunk));
    
    export default store;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这样就可以在React组件中dispatch这个fetchPosts函数来获取数据,并在数据获取成功或失败后更新UI。

  • 相关阅读:
    Packet Tracer - 在 OSPFv2 中传播默认路由
    HTML <tr> 标签
    如何调用Metabase开放API
    复现一个循环问题以及两个循环问题
    【新功能】Ambire 钱包集成了 Metis 网络
    【Kotlin基础系列】第6章 包与导入
    ROI的投入产出比是什么?
    leetcode做题笔记228. 汇总区间
    Yolov5 v7.0目标检测——详细记录环境配置、自定义数据处理、模型训练与常用错误解决方法(数据集为河道漂浮物)
    【快应用】Win7系统使用华为IDE无法运行和调试项目
  • 原文地址:https://blog.csdn.net/jieyucx/article/details/136233471