tx前端面试1

128 阅读15分钟

www.nowcoder.com/feed/main/d… usestate和useeffect模拟usecallback实现

useCallback 是 React 中用于优化性能的一个 Hook,它返回一个记忆化的回调函数。虽然 useCallback 本身已经非常简洁和高效,但我们可以通过 useState 和 useEffect 来模拟其行为。下面是一个示例,展示了如何使用 useState 和 useEffect 来模拟 useCallback 的功能。

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

function useCustomCallback(callback, dependencies) {
  const [memoizedCallback, setMemoizedCallback] = useState(() => callback);

  useEffect(() => {
    setMemoizedCallback(() => callback);
  }, dependencies);

  return memoizedCallback;
}

function ExampleComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCustomCallback(() => {
    console.log('Button clicked:', count);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleClick}>Log Count</button>
    </div>
  );
}

export default ExampleComponent;

解释 useCustomCallback Hook:

useCustomCallback 是一个自定义 Hook,它接受一个回调函数 callback 和一个依赖数组 dependencies。 使用 useState 来存储记忆化的回调函数。 使用 useEffect 来更新记忆化的回调函数,当依赖数组中的值发生变化时,更新 memoizedCallback。 ExampleComponent 组件:

ExampleComponent 使用 useState 来管理 count 状态。 handleClick 是通过 useCustomCallback 创建的记忆化回调函数,它依赖于 count。 当点击 "Increment" 按钮时,count 增加。 当点击 "Log Count" 按钮时,调用 handleClick,并在控制台中输出当前的 count 值。 通过这种方式,我们可以使用 useState 和 useEffect 来模拟 useCallback 的行为,确保回调函数在依赖项变化时被重新创建,从而优化性能。

react fiber有什么优势,用户打断原理

React Fiber 是 React 的一种新的协调引擎,它在 React 16 中引入,旨在提高 React 应用的性能和响应性。以下是 React Fiber 的一些主要优势以及用户打断的原理:

优势 时间分片(Time Slicing):

React Fiber 允许将渲染工作分成多个小任务,这样可以在每一帧中执行一部分渲染工作,而不是一次性完成所有渲染。这种方式可以使得应用在处理大量更新时仍然保持响应性。 优先级调度(Priority Scheduling):

React Fiber 可以根据任务的优先级来调度渲染工作。高优先级的任务(如用户输入)可以优先处理,而低优先级的任务(如后台数据加载)可以延后处理。 可中断渲染(Interruptible Rendering):

传统的 React 渲染是同步的,一旦开始渲染就无法中断。而 React Fiber 允许渲染过程被中断和恢复,这样可以在必要时暂停渲染以响应用户交互。 错误边界(Error Boundaries):

React Fiber 引入了错误边界组件,允许开发者捕获并处理渲染过程中的错误,而不会导致整个应用崩溃。 更好的调试和开发工具支持:

React Fiber 提供了更详细的调试信息和更强大的开发工具支持,使得开发者可以更容易地诊断和解决性能问题。 用户打断原理 React Fiber 的一个核心特性是它的可中断和恢复的渲染机制,这使得用户交互可以在渲染过程中打断并优先处理。其原理如下:

任务分片:

React Fiber 将渲染工作分成多个小任务,每个任务称为一个“单元(unit of work)”。这些任务可以在多个帧中分批完成。 任务调度:

React Fiber 使用一个调度器来管理这些任务。调度器会根据任务的优先级来决定哪些任务应该先执行,哪些任务可以延后。 任务中断:

在每个任务单元完成后,React Fiber 会检查是否有更高优先级的任务需要处理。如果有,它会暂停当前任务,转而处理高优先级任务(如用户输入)。 任务恢复:

被暂停的任务会被保存起来,等待高优先级任务完成后再继续执行。这种机制确保了用户交互的流畅性,同时也能逐步完成复杂的渲染任务。 通过这种方式,React Fiber 能够在处理复杂的渲染任务时仍然保持应用的高响应性,提供更好的用户体验。

参考资料 React 官方文档 React Fiber 架构 希望这些信息对你理解 React Fiber 的优势和用户打断原理有所帮助!

axios和fetch区别,axios拦截器

Axios 和 Fetch 的区别

1. 基本介绍

  • Axios:一个基于 Promise 的 HTTP 库,可以用在浏览器和 Node.js 中。
  • Fetch:一个原生的 JavaScript API,用于发起 HTTP 请求。

2. 语法和用法

  • Axios

    axios.get('/user?ID=12345')
      .then(function (response) {
        console.log(response);
      })
      .catch(function (error) {
        console.log(error);
      });
    
  • Fetch

    fetch('/user?ID=12345')
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => console.log(error));
    

3. 默认行为

  • Axios:自动将 JSON 数据转换为 JavaScript 对象。
  • Fetch:需要手动调用 .json() 方法来解析 JSON 数据。

4. 拦截器

  • Axios:支持请求和响应拦截器,可以在请求或响应被处理之前进行操作。
  • Fetch:不支持拦截器,需要手动封装。

5. 错误处理

  • Axios:在 HTTP 状态码不在 2xx 范围内时会自动抛出错误。
  • Fetch:只会在网络错误时抛出错误,对于 HTTP 状态码错误需要手动处理。

6. 取消请求

  • Axios:内置取消请求功能。
  • Fetch:需要使用 AbortController 来取消请求。

Axios 拦截器

1. 请求拦截器

可以在请求发送之前对请求进行修改或添加额外的配置。

axios.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  config.headers.Authorization = 'Bearer token';
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

2. 响应拦截器

可以在响应到达之前对响应数据进行处理。

axios.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
});

3. 移除拦截器

可以通过拦截器的 ID 来移除特定的拦截器。

const myInterceptor = axios.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config;
});

// 移除拦截器
axios.interceptors.request.eject(myInterceptor);

总结

  • Axios 提供了更丰富的功能和更简洁的 API,适合需要复杂 HTTP 请求处理的场景。
  • Fetch 是原生 API,适合简单的请求场景,但需要更多的手动处理。

选择使用哪一个取决于具体的需求和项目的复杂度。 在软件设计中,继承和组合是两种常见的模式,用于实现代码的复用和模块化。设计公共组件时,选择继承还是组合取决于具体的需求和场景。以下是两种模式的详细解释及其在公共组件设计中的应用:

继承模式

优点

  1. 代码复用:子类可以复用父类的代码,减少重复代码。
  2. 清晰的层次结构:通过继承可以建立清晰的类层次结构,便于理解和维护。

缺点

  1. 耦合度高:子类与父类耦合度高,父类的修改可能会影响到所有子类。
  2. 灵活性差:继承是静态的,不能在运行时改变父类的行为。

适用场景

  • 当类之间存在明显的“is-a”关系时。
  • 需要复用父类的大量代码时。

示例

class Component:
    def operation(self):
        pass

class ConcreteComponent(Component):
    def operation(self):
        print("ConcreteComponent operation")

class Decorator(Component):
    def __init__(self, component):
        self._component = component

    def operation(self):
        self._component.operation()

class ConcreteDecorator(Decorator):
    def operation(self):
        super().operation()
        print("ConcreteDecorator operation")

# 使用示例
component = ConcreteComponent()
decorator = ConcreteDecorator(component)
decorator.operation()

组合模式

优点

  1. 低耦合:组件之间的耦合度低,修改一个组件不会影响其他组件。
  2. 高灵活性:可以在运行时动态组合对象,增加系统的灵活性。

缺点

  1. 复杂性增加:需要管理更多的对象和接口,增加了系统的复杂性。
  2. 性能开销:由于需要更多的对象和接口调用,可能会带来性能开销。

适用场景

  • 当类之间存在“has-a”关系时。
  • 需要在运行时动态组合对象时。

示例

class Component:
    def operation(self):
        pass

class ConcreteComponent(Component):
    def operation(self):
        print("ConcreteComponent operation")

class Decorator(Component):
    def __init__(self, component):
        self._component = component

    def operation(self):
        self._component.operation()

class ConcreteDecoratorA(Decorator):
    def operation(self):
        super().operation()
        print("ConcreteDecoratorA operation")

class ConcreteDecoratorB(Decorator):
    def operation(self):
        super().operation()
        print("ConcreteDecoratorB operation")

# 使用示例
component = ConcreteComponent()
decoratorA = ConcreteDecoratorA(component)
decoratorB = ConcreteDecoratorB(decoratorA)
decoratorB.operation()

公共组件设计建议

  1. 明确需求:在设计公共组件时,首先要明确需求,确定哪些功能是必须的,哪些是可选的。
  2. 接口优先:优先设计接口,确保组件之间的低耦合和高内聚。
  3. 组合优先:在大多数情况下,优先使用组合模式,因为它更灵活且耦合度低。
  4. 适当使用继承:在需要复用大量代码且类之间存在明显“is-a”关系时,可以使用继承。
  5. 单一职责原则:每个组件应只负责一个功能,避免过多的职责集中在一个组件中。

通过合理地选择继承和组合模式,可以设计出高效、灵活且易于维护的公共组件。 在面向对象编程中,继承和组合是两种常见的设计模式,用于实现代码的重用和扩展。下面是对这两种模式的详细解释:

继承(Inheritance)

定义

继承是一种面向对象编程中的机制,通过这种机制,一个类(子类)可以继承另一个类(父类)的属性和方法。继承使得子类可以重用父类的代码,并且可以在子类中添加新的属性和方法,或者重写父类的方法。

优点

  1. 代码重用:子类可以重用父类的代码,减少重复代码。
  2. 层次结构:通过继承可以建立类的层次结构,清晰地表示类之间的关系。
  3. 多态性:通过继承和方法重写,可以实现多态性,使得同一方法在不同的类中有不同的实现。

缺点

  1. 耦合度高:子类和父类之间耦合度高,子类的实现依赖于父类的实现。
  2. 灵活性差:继承是静态的,不能在运行时改变继承关系。
  3. 复杂性:多层继承会增加系统的复杂性,难以维护。

示例

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.makeSound();  // 输出: Dog barks
    }
}

组合(Composition)

定义

组合是一种设计模式,通过这种模式,一个类可以包含另一个类的实例,从而实现代码的重用。组合使得一个类可以将某些功能委托给另一个类来实现,而不是通过继承来实现。

优点

  1. 低耦合:组合使得类之间的耦合度低,一个类的实现可以独立于另一个类。
  2. 高灵活性:组合是动态的,可以在运行时改变对象的组合关系。
  3. 易于维护:组合关系比继承关系更容易维护和修改。

缺点

  1. 代码复杂度:组合可能会增加代码的复杂度,因为需要显式地管理对象的组合关系。
  2. 性能开销:组合可能会带来一些性能开销,因为需要通过委托来调用方法。

示例

class Engine {
    void start() {
        System.out.println("Engine starts");
    }
}

class Car {
    private Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }

    void start() {
        engine.start();
    }
}

public class Main {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();  // 输出: Engine starts
    }
}

选择继承还是组合

在选择使用继承还是组合时,可以参考以下原则:

  1. IS-A 关系:如果类之间存在明显的 IS-A 关系(即子类是父类的一种),可以考虑使用继承。
  2. HAS-A 关系:如果类之间存在 HAS-A 关系(即一个类包含另一个类),可以考虑使用组合。
  3. 灵活性:如果需要更高的灵活性和可维护性,优先考虑组合。
  4. 重用性:如果需要重用现有类的实现,并且不需要修改其行为,可以考虑继承。

总的来说,组合通常被认为是比继承更灵活和更强大的设计模式,因为它遵循了“组合优于继承”的设计原则。

好的,下面是一个简单的手写发布订阅模式的实现示例,使用JavaScript编写:

class EventEmitter {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
  }

  // 取消订阅事件
  off(event, listener) {
    if (!this.events[event]) return;

    this.events[event] = this.events[event].filter(l => l !== listener);
  }

  // 触发事件
  emit(event, ...args) {
    if (!this.events[event]) return;

    this.events[event].forEach(listener => {
      listener(...args);
    });
  }
}

// 使用示例
const emitter = new EventEmitter();

function responseToEvent(msg) {
  console.log(msg);
}

// 订阅事件
emitter.on('data', responseToEvent);

// 触发事件
emitter.emit('data', '事件已触发!');

// 取消订阅事件
emitter.off('data', responseToEvent);

// 再次触发事件(不会有任何输出,因为已经取消订阅)
emitter.emit('data', '这次不会触发');

在这个示例中,我们创建了一个 EventEmitter 类,它包含三个主要方法:

  1. on(event, listener):用于订阅特定事件。
  2. off(event, listener):用于取消订阅特定事件。
  3. emit(event, ...args):用于触发特定事件,并传递参数给事件监听器。

通过这种方式,我们可以实现一个简单的发布订阅模式。你可以根据需要扩展这个基础实现,例如添加一次性订阅(只触发一次的事件)、事件优先级等功能。

tcp如何进行流量控制,3次重复adk发生,此时窗口怎么变?

TCP(传输控制协议)通过多种机制进行流量控制,以确保数据在发送方和接收方之间可靠传输,并防止网络拥塞。以下是TCP流量控制的主要机制:

1. 滑动窗口机制

TCP使用滑动窗口机制来控制数据流量。发送方和接收方各自维护一个窗口,窗口的大小决定了可以发送或接收的数据量。

  • 发送窗口:发送方可以在未收到确认的情况下发送的数据量。
  • 接收窗口:接收方可以在不丢弃数据的情况下接收的数据量。

2. 拥塞控制

TCP的拥塞控制机制包括慢启动、拥塞避免、快速重传和快速恢复等算法。

  • 慢启动:初始时,发送方的拥塞窗口(cwnd)较小,随着每次确认(ACK)的到来,窗口大小指数级增长。
  • 拥塞避免:当窗口大小达到一定阈值(ssthresh)时,窗口大小线性增长。
  • 快速重传:当发送方收到三个重复的ACK时,认为数据包丢失,立即重传丢失的数据包。
  • 快速恢复:在快速重传后,拥塞窗口减半,然后进入拥塞避免阶段。

3. 流量控制

TCP使用接收窗口(rwnd)来进行流量控制。接收方通过ACK报文中的窗口大小字段通知发送方当前可以接收的数据量。

3次重复ACK的情况

当发送方收到三个重复的ACK时,表明网络中可能发生了数据包丢失。此时,TCP会进行以下操作:

  1. 快速重传:立即重传丢失的数据包,而不等待重传计时器超时。
  2. 调整窗口大小
    • ssthresh(慢启动阈值)设置为当前拥塞窗口(cwnd)的一半。
    • cwnd(拥塞窗口)设置为ssthresh加上3个MSS(最大报文段大小),即 cwnd = ssthresh + 3 * MSS
    • 进入快速恢复阶段。

窗口变化示例

假设当前 cwnd = 16 MSS,ssthresh = 32 MSS,发生3次重复ACK后:

  1. ssthresh 设置为 cwnd 的一半,即 ssthresh = 16 / 2 = 8 MSS。
  2. cwnd 设置为 ssthresh + 3 * MSS,即 cwnd = 8 + 3 = 11 MSS。

在快速恢复阶段,cwnd 会逐步增加,直到丢失的数据包被确认,然后进入拥塞避免阶段。

总结

TCP通过滑动窗口和拥塞控制机制进行流量控制。当发生3次重复ACK时,TCP会进行快速重传,并调整窗口大小以应对可能的网络拥塞。 事件捕获和绑定是前端开发中常见的操作,特别是在处理用户交互时。为了防止在循环中绑定事件时出现意外行为(如闭包问题),可以使用一些技巧来确保事件处理函数能够正确地引用循环中的变量。

事件捕获和绑定,防止for循环

以下是一个示例,展示了如何在循环中绑定事件处理函数,并避免常见的闭包问题:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>事件捕获和绑定示例</title>
</head>
<body>
    <button class="my-button">按钮1</button>
    <button class="my-button">按钮2</button>
    <button class="my-button">按钮3</button>

    <script>
        // 获取所有按钮
        const buttons = document.querySelectorAll('.my-button');

        // 使用for循环绑定事件处理函数
        for (let i = 0; i < buttons.length; i++) {
            // 使用立即执行函数表达式(IIFE)来创建闭包
            (function(index) {
                buttons[index].addEventListener('click', function() {
                    alert('按钮 ' + (index + 1) + ' 被点击了');
                });
            })(i);
        }
    </script>
</body>
</html>

在这个示例中,我们使用了立即执行函数表达式(IIFE)来创建一个闭包,从而确保每个事件处理函数都能正确地引用循环中的变量 i。这样,当按钮被点击时,弹出的警告框会显示正确的按钮编号。

其他方法

除了使用 IIFE,还有其他方法可以避免闭包问题,例如使用 let 关键字声明循环变量,因为 let 具有块级作用域:

for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', function() {
        alert('按钮 ' + (i + 1) + ' 被点击了');
    });
}

在这个示例中,由于 let 具有块级作用域,每次循环迭代都会创建一个新的 i 变量,因此事件处理函数能够正确地引用当前的 i 值。

总结

在循环中绑定事件处理函数时,确保正确引用循环变量是关键。可以使用 IIFE 或 let 关键字来避免闭包问题,从而确保事件处理函数能够正确地工作。