前端核心面试题:React框架

2,767 阅读27分钟

@进阶问题

+ 对React-Fiber的理解,它解决了什么问题?

面试官系列中的回答

人话版Fiber架构

  • 解决什么问题:React16以前由于JS线程/逻辑线程与渲染线程相互独立且彼此互斥的原因,由于JS本质是单线程的,当一个庞大diff长期占据逻辑线程时渲染线程被阻塞,导致页面卡顿,体验狠辣鸡
  • Fiber的问世:React16采用了Fiber架构来解决这一问题
  • 解决思路:将虚拟DOM树的递归遍历过程,将每个节点按照被访问顺序一次丢入一个有序链表中,按window.requestAnimationFrame的节奏(一秒60帧,每帧16.67毫秒)切换逻辑线程与渲染线程的执行权,逻辑线程挂起时记录最后一个节点在链表中的序号,逻辑线程拿回执行权时再按照序号恢复原来的diff任务;
  • Fiber的本质是一种数据结构:Fiber即节点,节点即Fiber(乔布梭姑且称之为纤维结点),每个节点保存了其子、弟、叔节点的引用/地址(也就是diff的递归顺序),也就是形成链表的逻辑,首先diff自己的子节点,无子看弟,无弟看叔,如下图↓
  • Fiber架构的线程调度:一种能挂起和恢复diff的逻辑线程,姑且称之为纤维线程——是一套调度机制
  • 总结:diff阻塞渲染难题→fiber架构→本质是数据结构(纤维节点)→配套调度逻辑(纤维线程/纤程)=能挂起和恢复的diff线程→diff线程到点就换岗(给渲染线程)回来还能恢复→diff阻塞渲染问题完美解决!

fiber.jpg


@基础知识

+ React中除了在构造函数中绑定this,还有别的方式吗

demo地址

import React, { Component } from 'react'

export default class ThisDemo extends Component {
    constructor(props) {
        super(props)
        // 给this飘忽的常规函数绑定this为当前组件实例
        this.handleClick1 = this.handleClick1.bind(this)
    }

    handleClick0() {
        // this指向调用者,不一定是当前组件实例
        console.log(this);
    }

    handleClick1() {
        // this指向调用者,不一定是当前组件实例
        console.log(this);
    }

    handleClick2 = () => {
        // this 指向当前组件实例
        console.log(this);
    }

    render() {
        // render中的 this 指向当前组件实例
        return (
            <>
                <div>ThisDemo</div>
                {/* render中的 this 指向当前组件实例 */}
                {/* 每次render都要重新构造clickHandler 性能垃圾 */}
                <p><button onClick={e => console.log(this)}>render函数中的this为当前组件实例</button></p>

                {/* 每次render都要重新绑定this 性能垃圾 */}
                <p><button onClick={this.handleClick0.bind(this)}>在JSX中绑定this</button></p>

                {/* 构造函数中一次性绑定 性能OK 但不优雅 */}
                <p><button onClick={this.handleClick1}>在构造函数中绑定this</button></p>

                {/* 箭头函数无需绑定this 其上下文this直接为当前组件实例 高效又优雅! */}
                <p><button onClick={this.handleClick2}>箭头函数中的this</button></p>
            </>
        )
    }
}

+ React.createClass和extends Component的区别有哪些?

在 React 中,React.createClassextends Component 都可以用于创建组件类,但它们之间有一些重要的区别。

  1. 语法区别 React.createClass() 是一个全局函数,用于创建组件类,而 extends Component 是 ES6 中的语法糖,用于继承自 React.Component 的组件。
  2. 支持的特性不同 createClass 支持 Mixins 特性, extends Component 不支持。
  3. 状态初始化方式不同 使用 createClass 创建的组件可以通过 getInitialState 方法来初始化组件状态,例如:
jsxCopy Code
const MyComponent = React.createClass({
  getInitialState() {
    return { count: 0 };
  },
  handleClick() {
    this.setState({ count: this.state.count + 1 });
  },
  render() {
    return (
      <button onClick={this.handleClick}>
        Click ({this.state.count})
      </button>
    );
  },
});

而使用 extends Component 创建的组件必须在构造函数中初始化状态,例如:

jsxCopy Code
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }
  render() {
    return (
      <button onClick={this.handleClick}>
        Click ({this.state.count})
      </button>
    );
  }
};
  1. Mixins 行为的差异 Mixins 是一种将多个组件间共享的逻辑进行抽象和封装的方式,createClass 支持 Mixins,而 extends Component 需要使用第三方库才能实现类似功能。

总的来说,虽然 createClass 依然可以工作,但 extends Component 是一个更加现代的做法,因为它更符合 ES6 的语法规范,并且在未来的 React 版本中可能会成为标准写法。因此,建议在新项目中尽量使用 extends Component 创建组件。

关于Mixins

Mixins 是一种将多个组件间共享的逻辑进行抽象和封装的方式,让我们可以在不同的组件中重复使用这些逻辑。在 React 早期版本中,Mixins 是一个非常流行的特性,但在 React v16 中已经被废弃了,推荐使用 React 组件和高阶组件(Higher-Order Components, HOCs)来实现类似的功能。

+ React插槽Portal及使用场景

React 的插槽 (Portals) 可以让你在任意位置将组件渲染到 DOM 结构的不同层次中,而不会受到组件层次限制的影响。

举个例子:假设你有一个带有 Modal 组件的 React 应用,当用户点击按钮时,弹出一个包含表单的模态框。如果在弹出窗口的组件树内直接渲染表单组件,则它会继承来自模态框父组件的 CSS 样式,这可能会导致样式问题。

使用 Portals,我们可以将模态框的内容渲染到顶级 DOM 节点(例如 body),这样就可以避免在组件树内嵌套的样式冲突问题,同时也可以确保模态框始终显示在页面的顶部。

下面是一个简单的例子,演示如何使用 Portals 在 body 中渲染模态框:

import { createPortal } from 'react-dom';

const Modal = ({ children }) => {
  const modalRoot = document.getElementById('modal-root'); // modal-root 是一个位于 body 中的 DOM 元素
  return createPortal(children, modalRoot);
}

const App = () => {
  return (
    <div>
      <h1>Hello, world!</h1>
      <Modal>
        <div>Modal content here</div>
      </Modal>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

在这个例子中,我们创建了一个 Modal 组件并将其渲染到页面上。然后,我们使用 createPortal 函数将 Modal 子元素(即模态框的内容)渲染到位于 body 中的一个 DOM 元素中。

Portals 通常用于实现以下情况:

  1. 模态框、对话框或聊天窗口等需要显示在页面上方的组件。
  2. 在页面的顶部或底部渲染悬浮工具条或通知条。
  3. 渲染到 iframe 中。
  4. 在应用程序的边界之外渲染组件,例如嵌入式小部件。

总的来说,Portals 可以帮助我们在组件树以外的其他地方渲染 React 组件,并使我们能够轻松处理在组件树内部无法解决的各种问题。

使用demo

@常用第三方

+react-intl

  • react-intl 简介

React-Intl 是一个为 React 应用程序提供本地化和国际化支持的库。它可以帮助我们轻松地实现日期、数字、货币等格式化,以及对语言环境的处理和翻译。

React-Intl 提供了一系列 React 组件和 API,其中包括:

  • FormattedMessage 组件:可以根据当前语言环境自动翻译和格式化文本内容。
  • FormattedNumberFormattedDateFormattedTimeFormattedPluralFormattedRelativeTime 和 FormattedMessage 等组件:可以根据当前语言环境格式化数字、日期、时间、复数形式和相对时间。
  • IntlProvider 组件:可以将所需的语言环境和相关配置传递给整个应用程序,并且支持动态更新语言环境。
  • injectIntl 高阶组件和 useIntl 钩子函数:可以在组件中使用当前语言环境和翻译方法。

React-Intl 支持各种语言和地区的本地化和国际化,同时还支持与 Redux 等状态管理库的集成。需要注意的是,在使用 React-Intl 进行国际化之前,需要先设置好所需的语言环境和相关配置。

  • 并不直接提供翻译功能

React-Intl 不直接提供翻译功能,但它提供了一些用于本地化和国际化处理的工具和组件。要进行翻译,需要使用其他工具或服务。

通常情况下,翻译是通过将文本发送到翻译服务来实现的。有很多免费和付费的在线翻译服务可以使用,例如 Google Translate、Baidu Translate 等。可以将要翻译的文本发送给这些服务,然后将返回的翻译结果插入到 React-Intl 的 FormattedMessage 组件或 formatMessage 方法中。

@疑难杂症

+ React中父组件如何调用到子组件的函数

在 React 中,父组件可以通过 ref 属性来访问子组件的函数或方法。

以下是一个例子:

import React, { Component } from 'react';

class Child extends Component {
  handleClick = () => {
    console.log('Button clicked!');
  };

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>Click me!</button>
      </div>
    );
  }
}

class Parent extends Component {
  childRef = React.createRef();

  handleClick = () => {
    this.childRef.current.handleClick();
  };

  render() {
    return (
      <div>
        <Child ref={this.childRef} />
        <button onClick={this.handleClick}>Call child function</button>
      </div>
    );
  }
}
  • 在上面的例子中,我们定义了一个 Child 组件和一个 Parent 组件。
  • Child 组件中,我们定义了一个 handleClick 方法,它将在按钮被点击时被调用。
  • Parent 组件中,我们使用 React.createRef() 方法来创建一个 childRef 引用。
  • 然后我们在 Child 组件上使用 ref 属性将 childRef 引用传递给子组件。
  • 最后,在 Parent 组件中,我们定义了一个 handleClick 方法,它将调用子组件的 handleClick 方法。

@问题调试

+ 列举一些错误边界的具体使用场景

  • 在 React 中,错误边界是一种特殊的组件,它可以捕获并处理子组件中的 JavaScript 错误,从而 避免整个应用程序崩溃
  • 捕捉到错误之后,常见的处理包括 打印和上报日志内容的优雅降级
  • 另外值得一提的是,错误边界还能够捕获到异步错误,例如 setTimeoutfetch

以下代码在程序出错时对界面进行了优雅降级

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null, info: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, info) {
    this.setState({ info });
    console.error(error, info);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Error</h1>
          <p>{this.state.error.toString()}</p>
          <p>{this.state.info.componentStack}</p>
        </div>
      );
    }
    return this.props.children;
  }
}

class MyComponent extends React.Component {
  render() {
    throw new Error('Something went wrong');
    return <div>Hello World</div>;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}

@性能调试

+ React性能调试的具体手段

React 应用的性能调试可以使用 React 官方提供的工具 React Developer Tools 和 Chrome 开发者工具的 Performance 面板。

React Developer Tools

  • React Developer Tools 是一个 Chrome 浏览器插件,可以帮助你检查 React 组件树、组件状态和 Props 等信息,以及检查应用的性能问题。

  • 安装 React Developer Tools 插件后,在 Chrome 浏览器中打开开发者工具,选择 React 面板,可以看到当前页面的 React 组件树结构和状态信息。

  • 除了查看组件树和状态信息,React Developer Tools 还提供了一些性能调试工具,例如 Profiler 和 Highlight Updates。

Chrome中的Performance面板

  • Chrome 开发者工具的 Performance 面板可以帮助你检查应用的性能问题,例如页面加载时间、资源加载时间、JavaScript 执行时间等。

  • 在 Chrome 浏览器中打开开发者工具,选择 Performance 面板,点击录制按钮,然后操作应用,停止录制后就可以看到性能分析报告。

  • 性能分析报告可以帮助你找到应用中的性能瓶颈和优化点,例如过长的 JavaScript 执行时间、过多的重绘和回流等问题。

总之,React 应用的性能调试是一个复杂的过程,需要综合使用不同的工具和技术,从多个角度来分析和优化应用的性能。

参考资料

+ Chrome的performance面板的具体使用

浏览器控制台的 Performance 面板是一个非常有用的工具,可以帮助你分析网页的性能问题。

下面是 Performance 面板的具体使用方法:

  1. 打开 Performance 面板:在 Chrome 中,你可以按下 F12 键打开开发者工具,然后点击 Performance 标签。在 Firefox 中,你可以按下 F12 键打开开发者工具,然后点击 Performance 标签。
  2. 开始记录性能数据:在 Performance 面板中,你可以点击左上角的录制按钮开始记录性能数据。当你点击录制按钮时,浏览器会开始记录网页的性能数据。
  3. 进行性能测试:在 Performance 面板中,你可以进行一些性能测试,例如加载网页、执行 JavaScript 代码、渲染 DOM 等。当你完成测试后,你可以点击停止按钮停止记录性能数据。
  4. 分析性能数据:在 Performance 面板中,你可以查看网页的性能数据,例如 CPU 使用率、内存使用率、网络请求等。你可以使用 Performance 面板提供的各种工具来分析这些数据,例如时间轴、摘要、瀑布图等。
  5. 优化网页性能:根据性能数据,你可以找出网页中的性能问题,并采取相应的措施来优化网页性能。例如,你可以减少 DOM 操作、压缩 JavaScript 代码、使用缓存等。

总之,Performance 面板是一个非常有用的工具,可以帮助你分析网页的性能问题,并采取相应的措施来优化网页性能。希望这些信息对你有所帮助。

+ 首屏渲染优化

代码层面

  • 组件库的按需引入
  • 路由的异步加载
  • 使用服务端渲染SSR,直接由服务端返回渲染好的HTML
  • 事件防抖节流,减少事件处理次数
  • 减少DOM层级
  • 减少DOM渲染次数(批量渲染documengFragment,差量渲染Vue/React)

通信层面

  • 缓存网路数据在localStorage里或vuex/redux里,减少请求次数
  • 小图片直接使用base64以减少网络请求次数
  • 使用HTTP协议的强缓,服务端给更新频率低的数据一个较长的缓存时间(响应头Cache-Control:maxAge(3600247))
  • 从CDN或静态资源服务器去获取图片、js、css、图标字体等静态资源

打包层面

  • 对图片进行物理压缩,如在线压缩工具TinyPNG
  • 打包时对HTML,CSS,JS进行压缩处理
  • 开启Gzip压缩;
  • 合理分包,将第三方包、轮子包、业务代码包分开,这样在前端版本升级时,往往还需要更新业务代码包,而第三方包和轮子包由于内容未变导致hash未变,因此直接被服务端判断为304,从而直接使用本地缓存;
  • 打包时开启Tree-Shaking,摇掉无用代码,减少包体积;
  • 从HTML中抽离CSS文件,以便单独下载与缓存;

@生命周期

+ state 和 props 触发更新的生命周期分别有什么区别?

在 React 中,当组件的 propsstate 发生变化时,会触发组件的重新渲染。不同的是,props 的变化可能来自于父组件,而 state 的变化则来自于组件自身。

因此,在触发更新时,propsstate 所触发的生命周期方法有所不同:

  • props 发生变化时,会调用以下方法:

    • static getDerivedStateFromProps(props, state):在接收到新的 props 时被调用,可以根据传入的 props 计算出并返回一个新的 state
    • shouldComponentUpdate(nextProps, nextState):在接收到新的 props 或 state 后被调用,用于决定是否需要重新渲染组件,默认返回 true
  • state 发生变化时,会调用以下方法:

    • shouldComponentUpdate(nextProps, nextState):在接收到新的 props 或 state 后被调用,用于决定是否需要重新渲染组件,默认返回 true

需要注意的是,当 propsstate 发生变化时,会触发 render 方法,这是必须实现的方法,用于渲染组件的内容。同时,componentDidUpdate(prevProps, prevState) 方法也会在组件更新完成后被调用,通常用于更新 DOM。

总之,propsstate 都能触发组件的更新,但它们所触发的生命周期方法略有不同。在组件中,通常可以通过 propsstate 来控制不同的行为和状态,并且根据需要进行更新和渲染。


+ 在React中组件的props改变时更新组件有哪些方法?

类组件玩法

当 React 组件的 props 发生改变时,组件可以选择重新渲染以响应这些变化。React 提供了以下几种方法来更新组件:

  • componentWillReceiveProps(nextProps) componentWillReceiveProps 生命周期方法会在父组件传递给当前组件的 props 发生变化时被调用。我们可以在该方法中根据 nextProps 来判断需要更新的属性,然后通过 setState 方法来更新组件。
  • shouldComponentUpdate(nextProps, nextState) shouldComponentUpdate 生命周期方法会在组件 state 或 props 发生变化时被调用。如果返回值为 false,则 React 不会重新渲染组件;如果为 true,则表明需要重新渲染组件。我们可以在该方法中比较 nextProps 和 this.props 的差异,来决定是否需要更新组件。
  • componentDidUpdate(prevProps, prevState) componentDidUpdate 生命周期方法会在组件完成更新后被调用。在该方法中,我们可以比较 prevProps 和 this.props 的差异,并进行相应处理,如发起网络请求等。
  • 使用 hooks React 提供了一系列 Hooks,例如 useEffect、useMemo、useCallback 等,它们可以帮助我们更好地管理组件状态,并在组件 props 发生变化时触发更新。

需要注意的是,以上这些方法主要是针对类组件而言的,如果你正在使用函数式组件(Functional Components),则可以使用 React Hooks 里的 useEffect Hook 来处理 props 变化时组件的更新。在 useEffect 中可以根据 props 的变化触发状态更新,从而实现组件的重新渲染。

函数式组件玩法

useEffect 可以侦听 props 的变化。但是,需要注意的是,与类组件中的 componentWillReceiveProps 不同,在函数组件中使用 useEffect 时,props 的变化不会自动触发 useEffect,需要在 useEffect 中指定依赖项来实现。

例如,下面的代码演示了如何使用 useEffect 监听某个特定的 props:

jsxCopy Code
import React, { useEffect } from 'react';

function MyComponent(props) {
  const { data } = props;

  useEffect(() => {
    console.log('data prop changed:', data);
    // 在这里添加处理逻辑
  }, [data]); // 这里指定了 data 作为依赖项
  // 当 data 发生变化时,useEffect 执行回调函数

  return (
    <div>
      <h1>Hello, {data}!</h1>
    </div>
  );
}

在上述代码中,我们使用 useEffect 监听 data prop 的变化,而且指定了 data 作为 useEffect 的依赖项。当 data 发生变化时,useEffect 会调用回调函数,并且打印出 'data prop changed:' 和新的 data 值。

需要注意的是,每个依赖项都必须是一个基本类型或对象引用,否则 useEffect 可能会失效。如果需要侦听对象的变化,可以使用深度比较方法,或者将对象属性拆分为单独的依赖项,以便精确侦听变化。

总的来说,React 在组件更新这方面提供了多种解决方案,我们可以根据具体的场景选择合适的方法。同时,需要注意在处理组件更新时要遵循 React 的设计原则,尽可能地减少组件的重新渲染,提高应用程序的性能。


+ getSnapshotBeforeUpdate生命周期有何用处

  • getSnapshotBeforeUpdate 是 React 生命周期中的一个方法,它在组件更新之前被调用。
  • 它的返回值将作为第三个参数传递给 componentDidUpdate 方法。
  • 该方法的主要用途是在更新之前获取组件的当前状态,以便在更新后进行比较,从而实现某些特定的效果。
  • 它可以帮助你提高应用程序的用户体验,从而提高应用程序的质量和可靠性。

以下列举几个 getSnapshotBeforeUpdate 的具体使用场景:

  • 获取 DOM 元素的位置信息:在组件更新之前,你可以使用 getSnapshotBeforeUpdate 方法获取 DOM 元素的位置信息,并将其作为返回值传递给 componentDidUpdate 方法。然后你可以在 componentDidUpdate 方法中使用这些位置信息来实现一些特定的效果,例如滚动到某个位置。
  • 在组件更新之前保存滚动位置:在组件更新之前,你可以使用 getSnapshotBeforeUpdate 方法获取当前的滚动位置,并将其作为返回值传递给 componentDidUpdate 方法。然后你可以在 componentDidUpdate 方法中使用这些滚动位置来还原之前的滚动位置,从而提高用户体验。
  • 在组件更新之前保存输入框中的光标位置:在组件更新之前,你可以使用 getSnapshotBeforeUpdate 方法获取输入框中的光标位置,并将其作为返回值传递给 componentDidUpdate 方法。然后你可以在 componentDidUpdate 方法中使用这些光标位置来还原之前的光标位置,从而提高用户体验。

@逻辑复用

+ HOC

答题要点

  • 两者都是React复用逻辑的主要方式,另外个主要方式是大名鼎鼎的天王【自定义Hook】
  • HOC的逻辑是子组件进去,父组件出来,父组件在直接渲染子组件之余,为子组件注入了新的props、新的生命周期、新的DOM事件监听、新的副作用...这一大堆东西即你想复用的数据与逻辑
  • RenderProps的逻辑是组件进去,怀了孕(Baby)的组件出来,Baby组件自带响应式数据与变化逻辑,即你想复用的数据与逻辑
  • 案例:以【实时显式鼠标移动位置】这一复用逻辑为例:

HOC案例

/* 子组件App进去 增强型的父组件XApp出来 */
const withMouse = (App) => {
  class XApp extends Component {
    constructor(props) {
      super(props);

      // 为子组件注入响应式数据x,y
      this.state = {
        x: 0,
        y: 0,
      };
    }

    /* 鼠标移动动态修改状态x,y的值 */
    onMouseMove = (e) => {
      this.setState(() => ({
        x: e.pageX,
        y: e.pageY,
      }));
    };

    render() {
      // 在此以外做的一切事情都是增强
      // return <App {...this.props}></App>

      return (
        // 给子组件的顶层DOM添加鼠标移动监听器
        <div onMouseMove={this.onMouseMove}>

          {/* 为子组件注入响应式数据x,y */}
          {/* <Com name={props.name} x={this.state.x} y={this.state.y} /> */}
          <App {...this.props} {...this.state} />
        </div>
      );
    }
  }

  // 普通的App进来 增强型App出去
  // 增强1:在顶层DOM身上绑定了鼠标移动事件监听器
  // 增强2:注入了响应式数据x,y即鼠标实时移动到的位置
  return XApp;
};

+ RenderProps

RenderProps案例

import React, { Component } from 'react'

/* 对鼠标移动的state与事件的封装提取 */
class Mouse extends Component {
    constructor(props) {
        super(props);

        // 老鼠baby自带响应式数据
        this.state = {
            x: 0,
            y: 0
        }
    }

    /* 在DOM事件中改变响应式数据 */
    onMouseMove = (e) => {
        this.setState({
            x: e.pageX,
            y: e.pageY
        })
    }

    render() {
        return (
            // 老鼠baby自带DOM事件监听——用以动态改变响应式数据
            <div onMouseMove={this.onMouseMove}>
                {/* 拿出我妈给我的renderFn(x,y)执行得到DOM */}
                {this.props.renderFn(this.state.x, this.state.y)}
            </div>
        )
    }
}

class App extends Component {
    render() {
        return (
            <div>
                {this.props.name}
                <br />

                {/* 将一只老鼠子组件放在此处 即相当于把【实时显式鼠标移动位置】放在这 */}
                {/* <div style={{ height: "1000px", backgroundColor: "#eee" }}>鼠标实时位置:{x},{y}</div> */}
                <Mouse

                    // 告诉我的baby拿到响应式数据x,y后如何渲染JSX
                    renderFn={
                        (x, y) => <div style={{ height: "1000px", backgroundColor: "#eee" }}>鼠标实时位置:{x},{y}</div>
                    }
                />

            </div>
        )
    }
}

export default App

+ 自定义Hook

下面是一个使用 React 和 TypeScript 编写的自定义 Hook,名为 useMousePosition,它可以帮助你获取鼠标的位置信息:

import { useState, useEffect } from 'react';

interface Position {
  x: number;
  y: number;
}

const useMousePosition = (): Position => {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const updatePosition = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', updatePosition);

    return () => {
      window.removeEventListener('mousemove', updatePosition);
    };
  }, []);

  return position;
};

export default useMousePosition;
  • 这个 Hook 使用 useState 和 useEffect 两个 React Hook 来实现。
  • useState 用于存储鼠标位置信息
  • useEffect 用于监听鼠标移动事件并更新鼠标位置信息。
  • 当组件卸载时,useEffect 方法会清除事件监听器。

使用该 Hook 的示例代码:

import React from 'react';
import useMousePosition from './useMousePosition';

const App = () => {
  const { x, y } = useMousePosition();

  return (
    <div>
      <p>Mouse position: {x}, {y}</p>
    </div>
  );
};

export default App;

+HOC、Render props、hooks 有什么区别,为什么要不断迭代?

React 高阶组件(Higher-Order Components, HOCs)、Render Props 和 Hooks 都是 React 中用于实现组件复用的技术,它们的主要差别在于实现方式不同,具体如下:

  • 高阶组件 高阶组件是一个函数,它接受一个组件作为参数,并返回一个新的组件。新的组件可以对原有组件进行增强或修改,这种模式的核心思想就是函数复合。HOCs 可以用于实现横切关注点(Cross-Cutting Concerns)比如:鉴权、日志记录、路由守卫等等。
  • Render Props Render Props 是一种组件复用技术,它通过使用 render 函数作为组件的 props,将渲染逻辑交给组件的使用者,从而达到代码重用的目的。Render Props 模式下组件通过 props 中传入的 render 函数来获取需要渲染的内容,通常用于实现组件逻辑复用。
  • Hooks Hooks 是 React v16.8 版本推出的新特性,它允许我们在函数组件中添加状态以及其他 React 特性。Hooks 在设计上是基于函数的,允许我们在函数组件中添加状态和副作用。React 提供了一些基本的 Hooks,包括 useState、useEffect、useContext 等等,还可以自定义 Hooks 来封装逻辑和状态。

总的来说,高阶组件、Render Props 和 Hooks 在实现功能上有所重叠,但由于技术不断发展,新的解决方案可能比旧的方案更加简单、易用或者效率更高。所以 React 也在不断迭代中,推出新的技术来提高开发效率,同时也为了更好地满足开发者对于可维护、可测试、易扩展的需求。


@常用的hooks

+ 原生Hook

  • useState
  • useEffect
  • useRef
  • useMemo
  • useCalllback
  • useReducer

+ ReactRedux

  • useSelector
  • useDispatch

+ 第三方Hooks

  • useVirtualList(虚拟列表)
import { useVirtualList } from 'ahooks';

const { list, containerProps, wrapperProps } = useVirtualList(Array.from(Array(99999).keys()), {
    overscan: 30, // 视区上、下额外展示的 dom 节点数量
    itemHeight: 60, // 行高度,静态高度可以直接写入像素值,动态高度可传入函数
});

<div {...containerProps}>
    <div {...wrapperProps}>
        {list.map(item => <div key={item.index}>{item.data}		</div>)}
    </div>
</div>

@自定义Hook

+ useFetch网络请求

下面是一个使用 React 和 TypeScript 编写的自定义 Hook,名为 useFetch,它可以帮助你发起 HTTP 请求并获取响应数据:

import { useState, useEffect } from 'react';

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

const useFetch = <T>(url: string): FetchState<T> => {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const json = await response.json();
        setData(json);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
};

export default useFetch;
  • 这个 Hook 使用 useState 和 useEffect 两个 React Hook 来实现。
  • useState 用于存储响应数据、加载状态和错误信息
  • useEffect 用于发起 HTTP 请求并更新状态。
  • 当 URL 发生变化时,useEffect 方法会重新发起 HTTP 请求。

使用该 Hook 的示例代码:

import React from 'react';
import useFetch from './useFetch';

/* 邮件 */
interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

const App = () => {
  const { data, loading, error } = useFetch<Post[]>(
    'https://jsonplaceholder.typicode.com/posts'
  );

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {data?.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
};

export default App;

在这个示例中,我们使用 useFetch Hook 来发起 HTTP 请求并获取响应数据,并在组件中显示它们。

总之,自定义 Hook 是 React 中的一种强大的工具,可以帮助你抽象出可重用的逻辑,从而提高代码的复用性和可维护性。希望对你有所帮助。

+ useLocalStorage随心缓存

下面又是一个经典的使用 React 和 TypeScript 编写的自定义 Hook,名为 useLocalStorage,它可以帮助你在本地存储中存储和获取数据:

import { useState } from 'react';

const useLocalStorage = <T>(
  key: string,
  initialValue: T
): [T, (value: T) => void] => {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value: T) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
};

export default useLocalStorage;
  • 这个 Hook 使用 useState 来存储和更新本地存储中的数据。
  • 当组件挂载时,useLocalStorage 方法会从本地存储中获取数据;
  • 如果本地存储中没有数据,则使用初始值。
  • 当数据发生变化时,useLocalStorage 方法会将数据存储到本地存储中。

使用该 Hook 的示例代码:

import React from 'react';
import useLocalStorage from './useLocalStorage';

const App = () => {
  const [name, setName] = useLocalStorage<string>('name', 'John');

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setName(event.target.value);
  };

  return (
    <div>
      <p>Your name: {name}</p>
      <input type="text" value={name} onChange={handleChange} />
    </div>
  );
};

export default App;

在这个示例中,我们使用 useLocalStorage Hook 来存储和获取名字数据,并在组件中显示它们。

+ useDebounce事件防抖

下面叒是一个经典的使用 React 和 TypeScript 编写的自定义 Hook,名为 useDebounce,它可以帮助你在输入框中实现防抖功能:

import { useState, useEffect } from 'react';

const useDebounce = <T>(value: T, delay: number): T => {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
};

export default useDebounce;
  • 这个 Hook 使用 useState 和 useEffect 两个 React Hook 来实现。
  • useState 用于存储防抖后的值
  • useEffect 用于设置定时器并在定时器结束时更新防抖后的值。
  • 当值或延迟时间发生变化时,useEffect 方法会重新设置定时器。

使用该 Hook 的示例代码:

import React, { useState } from 'react';
import useDebounce from './useDebounce';

const App = () => {
  const [searchTerm, setSearchTerm] = useState<string>('');
  const debouncedSearchTerm = useDebounce<string>(searchTerm, 500);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSearchTerm(event.target.value);
  };

  return (
    <div>
      <input type="text" value={searchTerm} onChange={handleChange} />
      <p>Debounced value: {debouncedSearchTerm}</p>
    </div>
  );
};

export default App;

在这个示例中,我们使用 useDebounce Hook 来实现输入框的防抖功能,并在组件中显示防抖后的值。

+ useIntersectionObserver判断是否可见

下面叕是一个经典的使用 React 和 TypeScript 编写的自定义 Hook,名为 useIntersectionObserver,它可以帮助你观察元素是否进入视窗:

import { useState, useEffect, RefObject } from 'react';

const useIntersectionObserver = <T extends HTMLElement>(
  ref: RefObject<T>,
  options?: IntersectionObserverInit
): IntersectionObserverEntry | null => {
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => setEntry(entry), options);

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      if (ref.current) {
        observer.unobserve(ref.current);
      }
    };
  }, [ref, options]);

  return entry;
};

export default useIntersectionObserver;
  • 这个 Hook 使用 useState、useEffect 和 RefObject 三个 React Hook 来实现。
  • useState 用于存储 IntersectionObserverEntry 对象;
  • useEffect 用于创建和销毁 IntersectionObserver 对象,并在元素进入视窗时更新 IntersectionObserverEntry 对象。
  • RefObject 用于获取组件的 DOM 元素。

使用该 Hook 的示例代码:

import React, { useRef } from 'react';
import useIntersectionObserver from './useIntersectionObserver';

const App = () => {
  const ref = useRef<HTMLDivElement>(null);
  const entry = useIntersectionObserver(ref);

  return (
    <div>
      <div style={{ height: '100vh' }}></div>
      <div ref={ref} style={{ height: '50vh', backgroundColor: 'red' }}>
        {entry?.isIntersecting ? 'Visible' : 'Not visible'}
      </div>
      <div style={{ height: '100vh' }}></div>
    </div>
  );
};

export default App;

在这个示例中,我们使用 useIntersectionObserver Hook 来观察红色区域是否进入视窗,并在组件中显示它们是否可见。


@组件封装

+ 递归组件

  • 自己作为自己的子组件,例如:无穷子菜单,博文的无穷回复;
  • 核心逻辑,以无穷回复为例:
  • 每篇博文有它的回复数据,假设叫replies
  • 组件CommentItem正常渲染出每个回复item的用户名、回复时间
  • 每个回复的item依然有它的子回复replies
  • 将replies中的每一项,继续映射为一个新的CommentItem
function CommentItem({item}){
    return <>
        <h3>item.username</h3>
        <span>item.date</span>
        
        {/*渲染item的子回复replies*/}
        {
            item.replies.map(
                it=><CommentItem item={it}/>
            )
        }
    </>
}

参考链接:React 中的递归组件


@权限控制

+ 一个简单回答

登录后存储用户信息

  • 登录成功后,服务端返回该用户的角色/权限;
  • 可以将该角色/权限等级数据存储在全局(例如全局状态管理);

路由层面

  • 路由层面A计划-动态路由表:根据该用户的等级动态添加它有权访问的路由(一旦用户访问自己无权访问的路由时命中404);
  • 路由层面B计划-路由守卫:使用路由守卫,当用户访问越权的路由时一脚踹到登录页;
  • 路由守卫封装参考:基于React的简单权限设计

组件层面

  • 组件层面A计划-条件渲染:根据用户的权限条件渲染它能访问的Link,组件、具体元素等;
  • 组件层面B计划-自行封装
  • React中可以使用HOC实现例如 WithCustomAuth(MyButton,3) 在返回JSX的时候使用条件渲染
function WithCustomAuth(Com,level){
    function Parent(props){
        reurn ({
            authFromRedux >= level 
            ? <Com {...props} /> 
            : <a href="/login">登录</a>
        })
    }
    return Parent
}

+ 再来一个版本

在 React 中进行权限控制通常有两种方式:路由级别的权限控制和组件级别的权限控制。

路由级别的权限控制

  • 路由级别的权限控制是指在路由层面进行权限控制
  • 只有具有特定权限的用户才能访问某些路由
  • 一种常见的实现方式是使用 React Router 的 Route 组件的 render 属性
  • 根据用户权限动态渲染组件或重定向到其他路由
  • 其本质是 renderProps
import { Route, Redirect } from 'react-router-dom';

const PrivateRoute = ({ component: Component, isAuthenticated, ...rest }) => (
  <Route
    {...rest}
    render={(props) =>
      isAuthenticated ? (
        <Component {...props} />
      ) : (
        <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
      )
    }
  />
);
  • 在这个示例中,PrivateRoute 组件接受一个 isAuthenticated 属性,表示用户是否已经登录。
  • 如果用户已经登录,则渲染 Component 组件,否则重定向到登录页面。

组件级别的权限控制

  • 组件级别的权限控制是指在组件层面进行权限控制
  • 只有具有特定权限的用户才能看到或操作某些组件
  • 一种常见的实现方式是在组件中使用条件渲染来控制组件的显示和隐藏
const MyComponent = ({ user }) => (
  <div>
    {user.role === 'admin' && <AdminPanel />}
    {user.role === 'editor' && <EditorPanel />}
    {user.role === 'viewer' && <ViewerPanel />}
  </div>
);

在这个示例中,MyComponent 组件根据用户的角色来渲染不同的面板组件。

总之,在 React 中进行权限控制有多种实现方式,具体取决于你的应用场景和需求。无论采用哪种方式,都需要注意安全性和性能问题。

+ 推荐阅读