查看原文
其他

什么是注册中心?

编程导航和鱼友们 面试鸭 2024-03-29

大家好呀,今天是编程导航 30 天面试题挑战的第二十五天,一起来看看今天有哪些优质面试题吧。

后端

题目一

什么是 Spring 的依赖注入,依赖注入的基本原则以及好处?

官方解析

依赖注入(Dependency Injection,简称 DI)是 Spring 框架中的一种设计模式,用于实现控制反转(Inversion of Control,简称 IoC)。它是一种将对象之间的依赖关系从硬编码中解耦的方法。通过依赖注入,Spring 框架可以在运行时自动为类的属性、构造函数或方法参数提供所需的依赖对象,从而实现对象之间的松耦合。

依赖注入的基本原则:

  1. 高层模块不应该依赖低层模块。它们都应该依赖抽象。
  2. 抽象不应该依赖具体实现。具体实现应该依赖抽象。

依赖注入的好处:

  1. 解耦:依赖注入降低了组件之间的耦合度,使得组件可以独立地进行开发、测试和维护。通过将组件的创建和管理交给IoC容器,组件之间的依赖关系变得更加清晰,有助于提高代码的可维护性。
  2. 提高代码的可测试性:依赖注入使得对组件进行单元测试变得更加容易。通过使用依赖注入,我们可以轻松地为组件提供模拟(Mock)的依赖对象,从而实现组件在隔离环境中的测试。
  3. 更好的代码重用:由于组件之间的依赖关系变得更加清晰,这有助于提高代码的可重用性。组件可以在不同的上下文中重用,而无需对其进行修改。
  4. 更简洁的代码:依赖注入使得代码更加简洁,因为组件不再需要直接处理依赖对象的创建和管理。这使得开发人员可以更加专注于组件的核心功能,从而提高开发效率。
  5. 更容易进行配置管理:依赖注入允许我们将组件的配置与实现代码分离,从而使得配置管理变得更加容易。通过使用外部配置文件或注解,我们可以在不修改代码的情况下,调整组件之间的依赖关系。

鱼友的精彩回答

Gundam 的回答

Spring 的依赖注入(Dependency Injection,简称 DI)是指,在一个对象创建时,由 Spring 框架动态地将其依赖的其他对象注入到它当中,以解耦对象之间的关系,达到灵活、可维护、可测试的目的。

依赖注入的基本原则是控制反转(Inversion of Control,简称 IoC),即将对象之间的关系由调用者的控制转变为被调用者的控制。在传统的开发模式中,一个对象创建时通常需要主动创建它所依赖的其他对象,这些对象的创建、配置、连接等关系都由这个对象主动控制,这会导致对象之间高耦合、难以扩展、难以维护。而采用依赖注入的方式,则将这些依赖关系交给 Spring 框架来处理,大大降低了对象之间的耦合度,提高了代码的可读性、可维护性和可扩展性。

依赖注入的好处主要有以下几点:

  1. 灵活性:通过依赖注入,对象之间的关系不再由程序员手动控制,而是由 Spring 容器动态地维护,可以灵活地组合和切换不同的对象实现。
  2. 可测试性:依赖注入可以使各个对象之间的依赖关系更加松散,方便进行单元测试和集成测试。
  3. 可扩展性:依赖注入将各个对象之间的依赖关系解耦,方便对系统进行扩展和升级。
  4. 可维护性:依赖注入可以减少重复代码,降低了代码的复杂度,使代码更加易于维护和修改。

题目二

什么是注册中心?如何实现一个注册中心?

官方解析

注册中心(Service Registry)是微服务架构中的一个关键组件,负责管理服务实例的信息,包括服务实例的地址、端口、元数据等。它允许服务实例在启动时注册自己,并在关闭时注销。服务消费者可以通过查询注册中心来发现可用的服务提供者,并根据负载均衡策略选择合适的服务实例进行调用。

实现一个注册中心需要以下几个关键功能:

  1. 服务注册:允许服务实例在启动时将自己的信息注册到注册中心,包括地址、端口、元数据等。
  2. 服务注销:允许服务实例在关闭时从注册中心注销自己。
  3. 服务发现:允许服务消费者查询可用的服务实例列表,以便找到合适的服务实例进行调用。
  4. 健康检查:注册中心需要定期检查已注册的服务实例的健康状况,以确保服务实例列表的准确性。如果发现某个服务实例不再可用,注册中心应将其从列表中移除。

实现一个简单的注册中心可以参考以下步骤:

  1. 选择一个合适的数据结构(例如哈希表)存储服务实例的信息。
  2. 实现一个HTTP API,允许服务实例在启动时向注册中心发送注册请求,将自己的信息添加到数据结构中。
  3. 实现一个HTTP API,允许服务实例在关闭时向注册中心发送注销请求,将自己的信息从数据结构中移除。
  4. 实现一个HTTP API,允许服务消费者查询可用的服务实例列表。
  5. 实现一个定时任务,定期检查已注册的服务实例的健康状况,如果发现某个服务实例不再可用,将其从数据结构中移除。

然而,实现一个可靠、高性能、可扩展的注册中心是一个复杂的任务。在实际应用中,通常推荐使用现有的成熟的注册中心组件,例如 Eureka、Consul、Zookeeper 等。这些注册中心提供了丰富的功能,包括高可用性、数据持久化、动态配置等。

鱼友的精彩回答

小火龙的回答

注册中心是一种用于服务发现和服务注册的系统,它用于管理多个微服务实例的元数据信息。在一个微服务架构中,可能有数十个、数百个或数千个微服务实例,这些实例可能分布在多台机器上。注册中心通过维护微服务实例的元数据信息,如服务的名称、IP地址、端口号、协议等,使得微服务能够相互发现、调用。

一个简单的注册中心可以使用类似于键值对的方式存储元数据信息,比如使用Redis、Zookeeper等分布式数据存储系统,通过存储每个服务实例的信息,以及对应的服务名称和版本号,来实现服务发现和注册。通常,注册中心还会提供健康检查、负载均衡、故障转移等功能,以确保服务的高可用性和稳定性。

实现一个注册中心需要考虑以下几个方面:

  1. 选择分布式数据存储系统,如 Redis、Zookeeper 等,以存储服务实例的元数据信息。
  2. 定义服务注册和发现的接口规范,如 RESTful API 或 RPC 接口。
  3. 实现服务注册和发现的逻辑,包括服务注册、服务发现、健康检查、负载均衡、故障转移等功能。
  4. 集成到微服务架构中,与微服务框架进行集成,如 Spring Cloud、Dubbo 等,以实现微服务的注册和发现。

一个高可用的注册中心还需要考虑数据的备份和恢复、集群管理、安全认证等方面的问题。

用 java 实现一个简单的的注册中心:

import java.util.HashMap;
import java.util.Map;

public class RegistryCenter {
    // 存储服务实例的 map,key 为服务名,value 为该服务的实例列表
    private Map<String, Object> serviceMap = new HashMap<>();

    /**
     * 注册服务实例
     * @param serviceName 服务名
     * @param serviceInstance 服务实例
     */
    public void registerService(String serviceName, Object serviceInstance) {
        if (serviceMap.containsKey(serviceName)) {
            serviceMap.get(serviceName).add(serviceInstance);
        } else {
            List<Object> serviceInstances = new ArrayList<>();
            serviceInstances.add(serviceInstance);
            serviceMap.put(serviceName, serviceInstances);
        }
    }

    /**
     * 获取服务实例列表
     * @param serviceName 服务名
     * @return 服务实例列表
     */
    public List<Object> getServiceInstances(String serviceName) {
        return serviceMap.get(serviceName);
    }
}

这个注册中心实现了服务实例的注册和查找功能。registerService 方法将一个服务实例注册到注册中心,getServiceInstances 方法可以获取指定服务名的服务实例列表。这个注册中心的实现是一个简单的 HashMap,存储了所有服务实例。实际上,真正的注册中心需要考虑高可用性、数据一致性等问题。

HeiHei 的回答

注册中心:

注册中心是服务实例信息的存储仓库,也是服务提供者和服务消费者进行交互的桥梁。它主要提供了服务注册和服务发现这两大核心功能。在一个分布式系统中,不同的服务会以微服务的形式运行在不同的机器上,它们需要相互通信以完成业务逻辑。而注册中心则充当了服务之间的“黄页”,记录了所有可用的服务及其网络地址,方便其他服务进行查找和调用。

当一个新的服务启动时,它会向注册中心注册自己的网络地址和一些元数据信息(例如服务名称、版本号、健康状态等),注册中心会将这些信息存储在自己的数据中心中。当其他服务需要调用这个新服务时,它们可以通过向注册中心查询来获取该服务的地址和元数据信息,然后与该服务建立网络连接。常见的注册中心包括 ZooKeeper、Consul、Eureka 、Nacos 等等

实现一个注册中心:
  1. 设计数据模型:设计注册中心的数据模型,包括服务的元数据信息、服务实例的网络地址等。
  2. 实现服务注册:当服务启动时,它需要向注册中心注册自己的元数据信息和网络地址。可以通过 REST API、RPC 等方式实现服务的注册。
  3. 实现服务发现:当一个服务需要调用其他服务时,它需要向注册中心查询目标服务的网络地址。可以通过 REST API、RPC 等方式实现服务的发现。
  4. 实现健康检查:为了保证服务的可用性,注册中心需要定期检查服务实例的健康状况,并将不健康的实例从服务列表中移除。
  5. 实现高可用:注册中心是一个分布式系统的核心组件,需要保证高可用性。可以采用主从复制、集群等方式实现注册中心的高可用性。
  6. 实现安全机制:注册中心涉及到服务的元数据信息和网络地址等敏感信息,需要采取合适的安全措

题目三

什么是工厂模式?使用工厂模式有什么好处?工厂模式有哪些分类?各自的应用场景是什么?

官方解析

工厂模式(Factory Pattern)是一种创建型设计模式,它提供了一种封装对象创建过程的方法。工厂模式将对象的创建和使用分离,让一个专门的工厂类负责创建对象实例,而不是在代码中直接使用 new 操作符。这有助于降低代码的耦合度,提高可维护性和可扩展性。

使用工厂模式的好处:

  1. 降低耦合度:工厂模式将对象的创建与使用分离,使得客户端代码不直接依赖具体的类,降低了耦合度。
  2. 提高可扩展性:当需要添加或修改产品类时,只需修改工厂类,而不需要修改客户端代码,提高了系统的可扩展性。
  3. 提高可维护性:通过集中管理对象的创建,提高了代码的可维护性。
  4. 提高代码复用性:工厂类可以被多个客户端代码复用,减少了重复代码。

工厂模式可以分为以下几类:

  1. 简单工厂模式(Simple Factory Pattern):一个工厂类根据传入的参数决定创建哪个具体产品类的实例。简单工厂模式适用于产品种类较少且不易变化的场景。应用场景:例如,一个简单的图形绘制工具,可以根据传入的参数创建不同类型的图形(如圆形、矩形等)。
  2. 工厂方法模式(Factory Method Pattern):定义一个接口或抽象类来创建对象,将实际创建对象的工作推迟到子类中。工厂方法模式适用于产品种类较多且可能增加的场景。应用场景:例如,一个日志记录器,可以根据不同的需求(如文件日志、数据库日志等)使用不同的工厂子类创建相应的日志记录器实例。
  3. 抽象工厂模式(Abstract Factory Pattern):提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。抽象工厂模式适用于产品族的场景。应用场景:例如,跨平台UI框架,可以为不同平台(如Windows、macOS等)提供不同的UI控件实现,通过抽象工厂来创建对应平台的一系列UI控件。

各种工厂模式的应用场景取决于实际需求,需要根据具体问题来选择合适的工厂模式。

鱼友的精彩回答

维萨斯的回答

循序渐进循序渐进
从定义开始
  • 工厂模式
    • 松耦合
    • 封装
    • 可拓展
    • 复用
    • 把对象的构造交给一个类,这样在对象构造过程中,调用者不需要知道对象的产生过程。降低了代码的耦合度,同时体现了面向对象的封装的特征
    • 好处 ( 不是绝对的,仍然需要具体问题具体分析 )
再看分类 - 从缺点看他的用处
简单工厂模式 (一个工厂生产所有的对象 )
  • 如果它负责的对象太多,简单工厂容易庞大,变成超级类
  • 简单工厂的拓展是竖向拓展(拓展需要访问工厂内部,职责不够单一)
  • 因此,他适合在'简单场景' - 对象少且固定
工厂方法模式 ( 一个工厂一个对象 )
  • 工厂方法是横向拓展,由于工厂对象1对1,耦合度没有任何减少,要生产类就要知道那个对应的工厂
  • 当需要生产新的产品时,无需更改既有的工厂,只需要添加新的工厂即可。保持了面向对象的可扩展性(纵向,横向都很不错),符合开闭原则。
  • 因此,工厂方法模式适用于对象多而且可能增加的场景
抽象工厂模式 ( 抽象接口工厂实现 )
  • 抽象工厂是定义了一个接口,然后由具体工厂去实现抽象方法,突出特点就是运用了多态,耦合够松。
  • 缺点也很明显,在接口不变的情况下,无论是横向拓展性还是松耦合都很好,但是一旦接口有新的功能增加,所有的工厂实现都需要纵向拓展。即横向拓展能力强,但是纵向拓展能力=0
  • 因此,抽象工厂模式适合产品族(即有相似属性)的生产

林寻的回答

创建者模式
二.工厂模式

在 java 中,万物皆对象,这些对象都需要创建,如果创建的时候直接 new 该对象,就会对该对象耦合严重,假如我们要更换对象,所有 new 对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦

  • 简单工厂模式(不属于 GOF 的 23 种经典设计模式)
  • 工厂方法模式
  • 抽象工厂模式
1.简单工厂模式

简单工厂不是一种设计模式,反而比较像是一种编程习惯。

1.1 结构

简单工厂包含如下角色:

抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。具体产品 :实现或者继承抽象产品的子类 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。

1.2 实现


public class SimpleCoffeeFactory {

    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        if("americano".equals(type)) {
            coffee = new AmericanoCoffee();
        } else if("latte".equals(type)) {
            coffee = new LatteCoffee();
        }
        return coffee;
    }
}

工厂(factory)处理创建对象的细节,一旦有了 SimpleCoffeeFactory,CoffeeStore 类中的 orderCoffee()就变成此对象的客户,后期如果需要Coffee 对象直接从工厂中获取即可。这样也就解除了和Coffee实现类的耦合,同时又产生了新的耦合,CoffeeStore 对象和 SimpleCoffeeFactor y工厂对象的耦合,工厂对象和商品对象的耦合。

后期如果再加新品种的咖啡,我们势必要需求修改 SimpleCoffeeFactory 的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。

1.3 优缺点

优点:封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。

缺点:增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。

1.4扩展

静态工厂在开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是23种设计模式中的。代码如下:

public class SimpleCoffeeFactory {

    public static Coffee createCoffee(String type) {
        Coffee coffee = null;
        if("americano".equals(type)) {
            coffee = new AmericanoCoffee();
        } else if("latte".equals(type)) {
            coffee = new LatteCoffee();
        }
        return coffe;
    }
}
2.工厂方法模式

针对上例中的缺点,使用工厂方法模式就可以完美的解决,完全遵循开闭原则。

2.1 概念

定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。

2.2 结构

工厂方法模式的主要角色:

抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

2.3实现


public interface CoffeeFactory {

    Coffee createCoffee();
}
public class LatteCoffeeFactory implements CoffeeFactory {

    public Coffee createCoffee() {
        return new LatteCoffee();
    }
}

public class AmericanCoffeeFactory implements CoffeeFactory {

    public Coffee createCoffee() {
        return new AmericanCoffee();
    }
}
public class CoffeeStore {

    private CoffeeFactory factory;

    public CoffeeStore(CoffeeFactory factory) {
        this.factory = factory;
    }

    public Coffee orderCoffee(String type) {
        Coffee coffee = factory.createCoffee();
        coffee.addMilk();
        coffee.addsugar();
        return coffee;
    }
}

要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。

工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。

2.4 优缺点

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
  • 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;缺点:
  • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
3.抽象工厂模式

前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机、传智播客只培养计算机软件专业的学生等。

这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。

本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。

3.1 概念

是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。

3.2 结构

抽象工厂模式的主要角色如下:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
3.2 实现

现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。所以这个案例可以使用抽象工厂模式实现。类图如下:

public interface DessertFactory {

    Coffee createCoffee();

    Dessert createDessert();
}
//美式甜点工厂
public class AmericanDessertFactory implements DessertFactory {

    public Coffee createCoffee() {
        return new AmericanCoffee();
    }

    public Dessert createDessert() {
        return new MatchaMousse();
    }
}
//意大利风味甜点工厂
public class ItalyDessertFactory implements DessertFactory {

    public Coffee createCoffee() {
        return new LatteCoffee();
    }

    public Dessert createDessert() {
        return new Tiramisu();
    }
}

如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。

3.3 优缺点

优点:

当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

缺点:

当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

3.4 使用场景
  • 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
  • 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
  • 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。

如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。

3.5 模式扩展

简单工厂+配置文件解除耦合

可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。

第一步:定义配置文件

为了演示方便,我们使用 properties 文件作为配置文件,名称为 bean.properties

american=com.itheima.pattern.factory.config_factory.AmericanCoffee

latte=com.itheima.pattern.factory.config_factory.LatteCoffee

public class CoffeeFactory {

    private static Map<String,Coffee> map = new HashMap();

    static {
        Properties p = new Properties();
        InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
        try {
            p.load(is);
            //遍历Properties集合对象
            Set<Object> keys = p.keySet();
            for (Object key : keys) {
                //根据键获取值(全类名)
                String className = p.getProperty((String) key);
                //获取字节码对象
                Class clazz = Class.forName(className);
                Coffee obj = (Coffee) clazz.newInstance();
                map.put((String)key,obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Coffee createCoffee(String name) {

        return map.get(name);
    }
}

静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及创建对象写在静态代码块中,目的就是只需要执行一次。

JDK源码解析-Collection.iterator 方法

public class Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("令狐冲");
        list.add("风清扬");
        list.add("任我行");

        //获取迭代器对象
        Iterator<String> it = list.iterator();
        //使用迭代器遍历
        while(it.hasNext()) {
            String ele = it.next();
            System.out.println(ele);
        }
    }
}

使用迭代器遍历集合,获取集合中的元素。而单列集合获取迭代器的方法就使用到了工厂方法模式。我们看通过类图看看结构

Collection 接口是抽象工厂类,ArrayList是具体的工厂类;Iterator 接口是抽象商品类,ArrayList 类中的 Iter 内部类是具体的商品类。在具体的工厂类中 iterator()方法创建具体的商品类的对象。

另:

1,DateForamt 类中的 getInstance()方法使用的是工厂模式;

2,Calendar 类中的 getInstance()方法使用的是工厂模式;

前端

题目一

怎么使用 JS 实现元素拖拽功能?

官方解析

要使用 JavaScript 实现元素的拖拽功能,您可以按照以下步骤操作:

为需要拖拽的元素添加 mousedown、mousemove和mouseup 事件监听器。在 mousedown 事件处理函数中,记录鼠标按下时的位置,以及鼠标相对于元素的位置。在 mousemove 事件处理函数中,计算鼠标移动的距离,并更新元素的位置。在 mouseup 事件处理函数中,移除 mousemove 和 mouseup 事件监听器。以下是一个简单的实现示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Draggable Element</title>
    <style>
        #draggable {
            width: 100px;
            height: 100px;
            background-color: blue;
            position: absolute;
            cursor: move;
        }
    </style>
</head>
<body>
    <div id="draggable"></div>

    <script>
        const draggableElement = document.getElementById('draggable');

        draggableElement.addEventListener('mousedown', (event) => {
            // 记录鼠标按下时的位置
            let startX = event.clientX;
            let startY = event.clientY;

            // 计算鼠标相对于元素的位置
            let offsetX = startX - draggableElement.offsetLeft;
            let offsetY = startY - draggableElement.offsetTop;

            // 处理鼠标移动事件
            const onMouseMove = (event) => {
                // 计算鼠标移动的距离
                let deltaX = event.clientX - startX;
                let deltaY = event.clientY - startY;

                // 更新元素的位置
                draggableElement.style.left = `${event.clientX - offsetX}px`;
                draggableElement.style.top = `${event.clientY - offsetY}px`;
            };

            // 处理鼠标松开事件
            const onMouseUp = () => {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            };

            // 添加事件监听器
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });
    </script>
</body>
</html>

这个示例将实现一个简单的拖拽功能,您可以在此基础上根据需要进行调整和优化。注意,这个示例未考虑边界限制和性能优化,请根据实际需求进行相应调整。

鱼友的精彩回答

codexgh 的回答

使用元素拖拽效果,我们需要给 需要拖拽的元素添加 mousedown mousemove mouseup 这三个事件。当鼠标按下需要拖拽的元素时, 给元素的 mousemove 事件绑定拖动事件,处理对应的逻辑,当鼠标松开时移出拖动事件。下面举个常见的例子,我们在使用编辑器时,会发现文件目录模块和内容模块之间可以拖动开调整对应大小,如下图所示:

那我们来实现以下这个效果:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      display: flex;
      margin: 0;
      min-height: 100vh;
    }

    .scalable {
      min-width: 200px;
      position: relative;
      background-color: beige;
    }

    .content img {
      display: block;
      width: 150px;
      user-select: none;
    }

    .main {
      flex: 1;
      background-color: aqua;
    }

    .scalable .content {
      padding: 20px;
      padding-right: 34px;
    }

    .scalable .separator {
      width: 14px;
      height: 100%;
      background-color: #fff;
      box-shadow: 0 0 2px rgba(0, 0, 0, 0.35);
      position: absolute;
      top: 0;
      right: 0;
      cursor: col-resize;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .scalable .separator i {
      width: 2px;
      height: 14px;
      display: inline-block;
      background-color: #e9e9e9;
      margin: 0 1px;
    }
  </style>
</head>

<body>
  <div class="scalable">
    <div class="content">
      <img src="https://avatars.githubusercontent.com/u/61002730?v=4" alt="">
    </div>
    <div class="separator">
      <i></i>
      <i></i>
    </div>
  </div>
  <div class="main"></div>

  <script>
    let startX, startWidth;
    let $ = tag => document.querySelector(tag)

    let getWidth = () => {
      return parseInt(window.getComputedStyle($('.scalable')).width)
    }

    let onDrag = (e) => {
      let newWidth = e.clientX - startX + startWidth;
      $('.scalable').style.width = newWidth + 'px'
    }
    let stopDrag = () => {
      document.documentElement.removeEventListener('mousemove', onDrag)
      document.documentElement.removeEventListener('mouseup', stopDrag)
    }

    let startDrag = (e) => {
      startX = e.clientX
      startWidth = getWidth()
      document.documentElement.addEventListener('mousemove', onDrag)
      document.documentElement.addEventListener('mouseup', stopDrag)
    }

    $('.separator').addEventListener('mousedown', startDrag)


  </script>
</body>

</html>

效果如下:

题目二

什么是回流和重绘?什么场景下会触发?怎么减少回流和重绘?

官方解析

回流(Reflow)和重绘(Repaint)是浏览器渲染过程中的两个关键概念,它们对网页性能产生重要影响。理解回流和重绘以及它们的触发场景有助于我们优化网页性能。

回流(Reflow):回流是当浏览器需要重新计算元素的布局、尺寸和位置时所发生的过程。回流通常发生在以下场景:

  1. 添加或删除可见的 DOM 元素。
  2. 元素尺寸发生变化,如宽度、高度、边距、填充等。
  3. 页面布局发生变化,如改变窗口大小。
  4. CSS 伪类激活,如:hover。
  5. 计算 offsetWidth、offsetHeight 等属性。

重绘(Repaint):重绘是浏览器在元素的外观发生变化,但不影响布局时所发生的过程。重绘通常发生在场景:改变元素的颜色、背景、边框等样式,但不影响元素尺寸和位置。

减少回流和重绘的方法:

  1. 避免频繁操作样式:多次修改样式时,可以使用一个 class 来修改,或者通过修改 style 属性进行批量操作。
  2. 避免逐项改变样式:使用 CSS 的 transform、opacity 等属性进行动画,而不是改变宽度、高度、位置等会触发回流的属性。
  3. 使用文档片段(DocumentFragment)或者 offscreen 元素进行批量操作:在对 DOM 元素进行大量操作时,可以先将它们从文档流中移除,然后在内存中进行操作,最后再插入文档流中。
  4. 避免使用 table 布局:table 布局中的元素发生回流时,会影响到整个表格,从而导致更多的回流。
  5. 对具有复杂动画的元素使用绝对定位:将动画元素脱离文档流,减少回流对其他元素的影响。
  6. 避免频繁访问布局信息:如 offsetWidth、offsetHeight 等,可以在访问前将这些值缓存起来,避免多次触发回流。

回流和重绘对性能影响较大,尤其是在移动设备上。因此,优化代码以减少回流和重绘次数是提高网页性能的重要手段。

鱼友的精彩回答

Kristen 的回答

回流与重绘

回流一定会伴随这重绘,但是重绘可以单独发生。

页面渲染过程

  • 解析 html 生成 DOM 树
  • 处理 css 生成 cssom 树
  • 将 dom 树和 cssom 树合并生成 render Tree(渲染树:不包括 display:none,head 节点,但是包括 visibility:hidden 的节点)
  • 根据 render Tree, 得到每个节点的几何信息(位置与大小:margin,padding,width,height 等)
  • 将上面的到的几何信息传送到 GPU进行绘制。

回流(reflow) 与 重绘(repaint) 回流(reflow): render Tree 中节点的 大小、边距等发生改变时候需要重新计算几何信息的过程,叫回流 重绘(repaint): 改变元素的字体颜色、背景颜色等 不会影响到页面的布局变化,叫做重绘。

简而言之:影响页面布局的需要 回流、重绘;不会改变页面布局的只需要重绘

回流的消耗大(涉及到重新布局,而且一定会伴随着重绘),重绘的消耗小(相对回流)

引起回流的操作

  • 页面初始化渲染
  • Dom 结构改变:删除、插入元素
  • 改变节点的几何信息:margin/padding/width/height/display:none/border/fontSize
  • 改变窗口的大小(resize)
  • 获取某些属性的值(浏览器会为了获得准确的值也会触发回流的过程)
    • offsetTop, offsetLeft, offsetWidth, offsetHeight
    • scrollTop/Left/Width/Heigh
    • clientTop/Left/Width/Height
    • width,height
    • 调用了 getComputedStyle(), 或者 IE的 currentStyle

减少回流

  • 避免逐条样式的改变,这样会频繁触发。最好是一次性改变多条,或者通过 class 实现 (浏览器会对回流做优化,他会等到足够数量的变化发生,在做一次批处理回流)
  • 避免循环操作DOM元素,可以先创建一个元素(文档片段),在这个元素上操作完后再插入页面中
  • 读取部分引起回流的属性时可以通过缓存方式将结果保存,尽量不要频繁调用获取。

减少回流的代价 将回流的元素设置成绝对定位、固定定位,如此可使得元素脱离文本流,以达到减少代价的目的

luckythus 的回答

什么是回流和重绘?

回流:当渲染树中的一部分或全部因为元素的规模尺寸、布局、隐藏等改变而需要重新构建,就叫做回流(重排)。每个界面至少需要一次回流(重排)。

通俗来说就是:当增加或删除dom节点,改变元素的尺寸或者触发某些属性,引起页面结构改变时,浏览器就会重新构造dom树会重新渲染页面,这就是回流;页面必须要

重绘:一个元素外观改变触发浏览器的行为,浏览器会根据元素的新属性重新回值,使元素呈现新的外观;

某一个元素的样式改变了,虽然并不影响其在文档流中的位置,但是浏览器就会对元素进行重新绘制,这就是重绘;

什么场景下会触发?

回流:任何页面布局和几何属性的改变都会触发重排。

重绘:重绘一般发生在 UI 界面;是一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重 新绘制,使元素呈现新的外观。如 color、backgroundColor、size 等改变元素外观的属性

tip:

回流一定会触发重绘,但是重绘不一定会引起回流。

在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,引发重绘。

怎么减少回流和重绘?
  1. 尽可能使用 class 类名来修改样式
  2. 将需要多次重排的元素,position 属性设为 absolute 或 fixed ,元素脱离了文档流,它的变化不会影响到其他元素
  3. 不要使用 table 布局, 一个小的改动可能会使整个 table 进行重新;
  4. 操作 DOM 时,尽量在低层级的 DOM 节点进行操作;尽可能让局部小部分发生变动
  5. 减少 DOM 树的深度:避免在 DOM 树中嵌套过多的层级,这会增加回流的成本。
  6. 使用 CSS3 动画 transform 属性来实现平移、旋转、缩放等动画效果,避免使用 position,width,height 等属性来实现动画,使用 transform 属性不会影响元素的位置和大小,只会影响元素的绘制

题目三

什么是 npm?你用过哪些 npm 包?是否开发过自己的 npm 包?

官方解析

npm(Node Package Manager)是一个基于 Node.js 的包管理器,用于管理 Node.js 项目中的依赖和包。npm 提供了一种简单、方便的方式来安装、更新和卸载第三方库和工具,同时还可以用于发布和共享自己开发的包。通过 npm,开发者可以轻松地引入各种库,提高开发效率。

提供一些常见的 npm 包:

  1. express:一个简洁、灵活的 Node.js Web 应用框架,提供了一系列强大的功能来开发 Web 和 API 应用。
  2. react:一个用于构建用户界面的 JavaScrip t库,由 Facebook 开发,广泛应用于前端开发。
  3. lodash:一个强大的 JavaScript 实用库,提供了许多有用的工具函数,如数组操作、对象操作、函数操作等。
  4. axios:一个基于 Promise 的 HTTP 客户端库,用于浏览器和 Node.js,提供了简单、易用的 API 来发起 HTTP 请求。
  5. moment:一个用于解析、处理和显示日期和时间的 JavaScript 库,提供了丰富的 API 来处理各种日期和时间操作。
  6. nodemon:一个实用的开发工具,用于监视 Node.js 应用程序中的文件更改,并在发生更改时自动重启应用。

开发自己的 npm 包的过程大致如下:

  1. 创建项目文件夹,并在文件夹中初始化 npm 项目,使用 npm init 命令生成 package.json 文件。
  2. 开发功能模块,确保遵循模块化的原则,编写模块的导出和引入。
  3. 编写详细的 README 文件,说明模块的用途、安装方法、使用方法以及 API 文档。
  4. 确保项目中包含.gitignore 和 .npmignore 文件,以排除不需要发布到 npm 的文件和文件夹。
  5. 在 npm 官网 https://www.npmjs.com/ 注册一个账号,然后在命令行中使用 npm login 命令登录。
  6. 使用 npm publish 命令发布包到 npm 仓库。

发布后,其他开发者可以通过 npm install 命令安装和使用您的包。注意在开发过程中遵循语义化版本规范(Semantic Versioning),便于其他开发者理解版本变更。

鱼友的精彩回答

悟道的回答

npm 是 Node.js 的包管理器,可以方便地安装、升级、删除、发布和管理 Node.js 的包(也称为模块)。npm 包括一个命令行客户端,用于从 npm 仓库中下载、安装和管理依赖项,以及一个在线的包注册表,用于存储和分享 npm 包。

我使用过很多 npm 包,包括 Express、React、Lodash、Moment.js、Axios、Jest 等。这些包为 JavaScript 开发提供了许多便利的功能和工具,使得开发人员可以更快、更高效地构建应用程序。

我也曾经开发过自己的 npm 包。我开发的包名叫做 “my-utils”,提供了一些常用的 JavaScript 工具函数,如深度克隆、对象合并、类型判断等等。开发过程中,我首先在本地使用 npm init 命令初始化了一个新的 npm 包,并将代码上传到了我的 GitHub 仓库中。接着,我使用 npm publish 命令将包发布到了 npm 仓库中,使得其他开发人员可以方便地安装和使用我的工具函数。这个过程非常简单和便捷,使得我可以快速地分享我的代码并为其他开发人员提供帮助。

星球活动

1.欢迎参与 30 天面试题挑战活动 ,搞定高频面试题,斩杀面试官!

2.欢迎已加入星球的同学 免费申请一年编程导航网站会员 !

3.欢迎学习 鱼皮最新原创项目教程,手把手教你做出项目、写出高分简历!

加入我们

欢迎加入鱼皮的编程导航知识星球,鱼皮会 1 对 1 回答您的问题、直播带你做出项目、为你定制学习计划和求职指导,还能获取海量编程学习资源,和上万名学编程的同学共享知识、交流进步。

💎 加入星球后,您可以:

1)添加鱼皮本人微信,向他 1 对 1 提问,帮您解决问题、告别迷茫!点击了解详情

2)获取海量编程知识和资源,包括:3000+ 鱼皮的编程答疑和求职指导、原创编程学习路线、几十万字的编程学习知识库、几十 T 编程学习资源、500+ 精华帖等!点击了解详情

3)找鱼皮咨询求职建议和优化简历,次数不限!点击了解详情

4)鱼皮直播从 0 到 1 带大家做出项目,已有 50+ 直播、完结 3 套项目、10+ 项目分享,帮您掌握独立开发项目的能力、丰富简历!点击了解详情

外面一套项目课就上千元了,而星球内所有项目都有指导答疑,轻松解决问题

星球提供的所有服务,都是为了帮您更好地学编程、找到理想的工作。诚挚地欢迎您的加入,这可能是最好的学习机会,也是最值得的一笔投资!

长按扫码领优惠券加入,也可以添加微信 yupi1085 咨询星球(备注“想加星球”):

往期推荐

编程导航,火了!

Spring 支持哪几种事务管理类型?

什么是 Bean 的生命周期?

RPC是什么?

什么是零拷贝?

Redis 有哪些内存淘汰策略?



继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存