查看原文
其他

MySQL 中的索引是怎么实现的?

编程导航和鱼友们 面试鸭 2024-01-21

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

后端

题目一

String 和 StringBuffer、StringBuilder 的区别是什么?

官方解析

String 和 StringBuffer/StringBuilder 是 Java 中两种不同的字符串处理方式,主要的区别在于 String 是不可变的(immutable)对象,而 StringBuffer 和 StringBuilder 则是可变的(mutable)对象。

String 对象一旦被创建,就不可修改,任何的字符串操作都会返回一个新的 String 对象,这可能导致频繁的对象创建和销毁,影响性能。而 StringBuffer 和 StringBuilder 允许进行修改操作,提供了一种更高效的字符串处理方式。

StringBuffer 和 StringBuilder 的主要区别在于线程安全性和性能方面。StringBuffer 是线程安全的,所有方法都是同步的,因此可以被多个线程同时访问和修改。而 StringBuilder 不是线程安全的,适用于单线程环境下的字符串处理,但是相比于 StringBuffer,StringBuilder 具有更高的性能。

因此,当字符串处理需要频繁修改时,建议使用 StringBuffer 或 StringBuilder;而当字符串处理不需要修改时,可以使用 String。

鱼皮补充:这道题是一个高频考点,感兴趣的同学可以实际测试下 StringBuffer 和 StringBuilder 的性能,加深印象

鱼友的精彩回答

林风的回答

总的来说这三者都是用来操作字符串的类。

String 是不可变类,在 Java 当中提供了多种操作字符串的方法,比如字符串的拼接、字符串的裁剪等。每次这些操作的发生都会产生新的对象

这样来看的话似乎 String 并不适合去做字符串的各种操作,这种不可变的特性也带来了一定的好处。

1、免去了后续 String 字符串的创建。

由于 Strint Pool 的存在,当第一次字符串创建了之后,那么就可以根据引用直接在 String Pool 中获取到相应数据。

如果 String 不是不可变的,那么这个引用就会一直改变,不利于维护字符串常量池。

2、保证安全性

String 经常作为参数,String 不可变性可以保证参数不可变。

例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

StringBuffer 解决 String 拼接产生太多中间对象的问题.append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。

线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

苏打饼干的回答

题目二

MySQL 中的索引是怎么实现的?B+ 树是什么,B 树和 B+ 树的区别,为什么 MySQL 要用 B+ 树?

官方解析

MySQL 中的索引是通过 B+ 树实现的。B+ 树是一种多叉树,它可以将数据按照一定的顺序组织起来,从而提高查询效率。

B+ 树与 B 树的区别在于,B+ 树的所有数据都存储在叶子节点上,而非叶子节点只存储索引,这样可以提高数据查询效率。B+ 树的叶子节点之间使用指针相连,这样可以实现区间查找,也就是说,可以快速定位某个区间内的数据。

MySQL 之所以采用 B+ 树作为索引的实现方式,主要是因为 B+ 树具有以下优点:

  • 能够支持高效的范围查找和排序。

  • 叶子节点之间使用指针相连,能够支持高效的区间查询。

  • B+ 树具有较高的数据密度,可以减少磁盘 I/O 次数,提高查询效率。

  • B+ 树对于插入和删除操作也比较高效。

在 MySQL 中,B+ 树的实现主要是通过 InnoDB 存储引擎来实现的。InnoDB 存储引擎中的索引主要有聚簇索引辅助索引两种类型,聚簇索引是根据主键创建的索引,而辅助索引是根据非主键列创建的索引

对于辅助索引,MySQL 中会同时创建一个对应的聚簇索引,这样可以提高查询效率。

鱼友的精彩回答

Gundam 的回答

MySQL 中的索引采用B+树实现。

B+树是一种多路搜索树,是对B树的一种改进。

B树和B+树的主要区别在于 :

  1. B+树只存储数据的索引信息,并且把叶子节点存储在同一层上,中间节点只起到索引作用,查询时只需要遍历一次树就能找到目标数据,因此B+树具有更好的查询性能和更少的磁盘I/O次数,B+树的叶子节点都连接成了一个有序的链表,因此可以很方便地进行范围查询等操作。适合于数据库等存储大量数据的场景。

  2. B树是一种平衡树,它的每个节点都存储有序的关键字,并且每个节点的子节点数目介于m/2和m之间。B树中的每个节点既可以存储数据,也可以存储索引信息,因此B树可以减少I/O次数,适合于文件系统等存储大量数据的场景。但是B树的查询性能相对较低,因为每个节点都可能存储数据,查询时需要遍历多个节点才能找到目标数据。

在MySQL中使用B+树作为索引的数据结构,主要是因为B+树的查询性能好、支持范围查询、支持数据分页等操作,适用于存储海量数据的场景。此外,B+树的索引结构相对简单,易于实现和维护,能够满足高并发、高可用性的数据库要求。

维萨斯的回答

答案 + 解释

Mysql 中的索引是怎么实现的

  • B+树、哈希索引、全文索引

    • 哈希索引 ( 一般用于等值操作,而且人工一般不能干涉 )
    • 全文索引 ( 没有了解,因为比起使用 Mysql 的全文索引,用 ES 实现搜索更好 )
  • 根据存储引擎,b+树的具体实现有细微区别

    • 分聚簇索引和非聚簇索引
    • 聚簇索引只有一个,它根据主键实现了b+树
    • 非聚簇索引有多个,它的叶子节点存的是索引 + 主键
    • why? 因为InnoDB的数据持久化方法是:数据和索引一个文件
    • 无聚簇索引
    • 叶子节点存的是  数据的地址
    • 每次查询都需要去回表
    • why?因为MyISAM的数据持久化方法是:数据和索引分别存储
    • MyISAM
    • InnoDB

B+树 vs B树

  • B+树
    • 叶子节点存数据
  • B树
    • 所有节点都存数据
  • why choose B+
    • B+树支持范围查找,同时查询更高效
    • why ?因为叶子节点中,页于页之间是双向链表,而簇于簇之间有单向指针连接。
    • B树由于所有节点都保存所有数据,每当插入一条数据,哪怕是自增的,都可能造成整个树的自旋重构,当数据量很大的时候,这个时间成本和风险是巨大的
    • B+树使用叶子节点保存数据,插入一条数据只会在叶子节点上插入,一般不会影响树的结构
    • INSERT / DELETE
    • SELECT

言的回答

http://www.rmboot.com/Algorithms.html 数据结构可视化,可以体验B树与B+树数据操作时,直观的样子。

使用B+树相比于B树的优点在于: 回旋查找 的问题,也就是说当我们查询一段范围内的数据时,B+树是通过指针直接获取,而B树存在回旋查找的操作。下面我们可以通过图来展示区别:

B树插入9个数值

B+树插入9个数值

比如说我们现在要找 id >= 1 范围的数据

  • 如果使用B树,找到1时,需要回旋查找2,再继续操作
  • 如果使用B+树,找到1时,因为有指针,所以可以直接获取后面的数据

Yilin 的回答

数据库的查询,如何设计存储结构使得数据访问更快(假设有1亿条数据)

查询磁盘中的数据起决定性因素的就是

  1. 遍历数据的次数
  2. 从磁盘加载到内存的IO时间,最重要的次数少了加载IO时间也少。
线性查找

线性存储数据显然是不行的,运气不好每次都要追溯到最后一个数据块才能查询到。比如查找数据0

二叉树查找

我们都学过二分查找,如果我们一开始在插入数据就将数据按顺序排列,那查找效率就会大大提高。将数据按照顺序二叉树排列好每次查询就可以使用二分法即从根节点出发,查询效率就增加了。

但是如果删除了一些数据比如0,6,1,3,4删除了那么右边的数据就有变成了线性的了,查询效率就会有所浪费,

平衡二叉树

如果有这样一棵树左右子树高度差不超过 1,而且也满足顺序排列就可以继续高效下去,然后平衡二叉树就出现了。每当删除一个节点都应该发生相应的节点位置转换反转保证二叉树的平衡。

说了这么多我们无非就是想让查找效率降低,从线性的 O(n) 到 O (logn),好像还有疑问为什么不用更高效的 Hash 地址法来查找呢?这样可以降到 O(1),答案是在查询过程中我们不仅有等值查询还有范围查询 模糊查询,使用 Hash 存储其位置的不确定性,如果要查询 范围我们就要遍历全表。而二叉树只要遍历左右节点。

B树

由于平衡二叉树的二叉特点,它每一个节点最多只有 2 个叉,假设有 100000 个数据,那么树的深度将会变得特别深,而每次比较就是拿比较的树和节点上的数在内存比较,所以每比较一次就是一次 IO 操作就下降一层,层数越多时间就越久。所以B树就来了,他是多叉平衡树,每个节点维护了多个比较范围(即子节点)

这样就降低了高度,每个圆圈可以理解为一页,16kb的数据. 所以他的每个节点都存储数据就会造成每个结点的分叉数减少,而且会造成先靠近根节点的先查到,靠近叶子结点的后查到。同样范围查找也会出现多次回退到父节点在到另一个兄弟节点的低效率问题。

B+树

我们改造一下 B树 为 B + 树 ,每个非叶子节点只存索引,真实数据都存在叶子节点,这样非叶子节点的空间 单个数据空间 减少 数量即分叉就可以增大。每次查询无论如何必须遍历到叶子节点才会结束,这样深度又减少了,同时我们把每个叶子结点用双向链表连接起来,范围查询就更快。

这种存储方式就是B+ 树实现的 聚簇索引 叶子节点索引即数据,数据即索引。

题目三

Spring 框架中都用到了哪些设计模式?

官方解析

Spring 框架中使用了许多设计模式,以下列举一些比较重要的:

  • 单例模式:Spring 的 Bean 默认是单例模式,通过 Spring 容器管理 Bean 的生命周期,保证每个 Bean 只被创建一次,并在整个应用程序中重用。

  • 工厂模式:Spring 使用工厂模式通过 BeanFactory 和 ApplicationContext 创建并管理 Bean 对象。

  • 代理模式:Spring AOP 基于动态代理技术,使用代理模式实现切面编程,提供了对 AOP 编程的支持。

  • 观察者模式:Spring 中的事件机制基于观察者模式,通过 ApplicationEventPublisher 发布事件,由 ApplicationListener 监听事件,实现了对象间的松耦合。

  • 模板方法模式:Spring 中的 JdbcTemplate 使用了模板方法模式,将一些固定的流程封装在父类中,子类只需实现一些抽象方法即可。

  • 策略模式:Spring 中的 HandlerInterceptor 和 HandlerExecutionChain 使用了策略模式,允许开发者自定义处理器拦截器,按照一定顺序执行。

  • 责任链模式:Spring 中的过滤器和拦截器使用了责任链模式,多个过滤器和拦截器按照一定顺序执行,每个过滤器和拦截器可以拦截请求或者响应并做出相应的处理。

总之,Spring 框架中充分利用了许多设计模式,提供了良好的扩展性和灵活性,降低了代码的耦合度,提高了代码的可维护性。

鱼友补充:这题感兴趣的同学可以去看下部分源码,不用记所有的设计模式,重点把单例、代理、工厂、责任链理解了即可

鱼友的精彩回答

Gundam 的回答

依赖注入/控制反转(DI/IOC):这是 Spring 框架最为核心的设计模式,通过依赖注入实现对象的解耦和松耦合,使得对象之间的关系更加灵活。

模板方法模式:Spring框架中的JdbcTemplate和HibernateTemplate等模板类都是采用模板方法模式,定义了一些抽象方法,由具体的子类实现。

工厂模式:Spring框架中的BeanFactory和ApplicationContext等容器类都是采用工厂模式,通过工厂方法创建对象,从而实现对象的解耦。

单例模式:Spring框架中的Bean默认是单例模式,即在容器中只创建一次,并且在整个应用中都可以共享使用。

观察者模式:Spring框架中的事件机制就是采用观察者模式,通过注册监听器和发布事件的方式实现对象之间的解耦。

装饰器模式:Spring框架中的AOP就是采用装饰器模式,通过动态代理和切面编程实现对方法的增强和横切关注点的分离。

适配器(Adapter)模式 :Spring MVC 框架中的 HandlerAdapter 就是一个适配器模式的实现,它将不同类型的处理器适配成 Spring MVC 可以处理的处理器。

注册模式:Spring 的 BeanFactory 和 ApplicationContext 就是基于注册模式实现的,容器会将所有的 Bean 实例都注册到容器中,然后通过 Bean 名称或类型来获取实例。

代理(Proxy)模式:Spring AOP 框架就是基于动态代理实现的,通过代理可以对目标对象进行增强,比如添加事务管理等功能。

责任链模式:Spring 中的过滤器和拦截器使用了责任链模式,多个过滤器和拦截器按照一定顺序执行,每个过滤器和拦截器可以拦截请求或者响应并做出相应的处理。

前端

题目一

怎么用 CSS 实现一个宽高自适应的正方形?

官方解析

可以使用 CSS 的 padding 或者利用vw单位来实现一个宽高自适应的正方形。

一种比较常见的实现方法是:

<div class="square"></div>
.square {
  width: 25vw; /* 利用vw单位实现自适应宽度 */
  padding-top: 25vw; /* 利用padding实现自适应高度 */
  background-color: red;
}

上面的代码中,利用vw单位设置了正方形的宽度为屏幕宽度的 1/4(25%),同时利用 padding-top 设置正方形的高度也为屏幕宽度的 1/4,这样就实现了一个宽高自适应的正方形。

另外,也可以使用伪元素来实现:

<div class="square"></div>

.square {
  position: relative;
  width: 50%;
  background-color: red;
}

.square::before {
  content: "";
  display: block;
  padding-top: 100%;
}

上面的代码中,通过设置 div 元素的宽度为 50% ,再利用 ::before 伪元素设置 padding-top 为 100% ,就可以实现一个宽高自适应的正方形。

鱼皮评论:这题建议大家自己实现一下

鱼友的精彩回答

Kristen 的回答

方法1、CSS3 vw单位 CSS3 中新增了一组相对于可视区域百分比的长度单位 vw, vh, vmin, vmax。其中 vw 是相对于视口宽度百分比的单位,1vw = 1% viewport width, vh 是相对于视口高度百分比的单位,1vh = 1% viewport height;vmin 是相对当前视口宽高中 较小 的一个的百分比单位,同理 vmax 是相对当前视口宽高中 较大 的一个的百分比单位。

<div class="box"></div>
<style>
    .box{width: 100vw;height: 100vw;background: #F2DEDE;}
</style>

优点:简洁方便 缺点:浏览器兼容不好

方法2、设置垂直方向的padding撑开容器

在 CSS 盒模型中,margin, padding的百分比数值是相对 父元素的宽度计算的。只需将元素垂直方向的一个 padding 值设定为与 width 相同的百分比就可以制作出自适应正方形

<div class="box"></div>
 
<style>
     .box{
        width: 100%;
        padding-bottom: 100%;/* padding百分比相对父元素宽度计算 */
        height: 0;//避免被内容撑开多余的高度
    }
</style>  

padding百分比+height设置0; 这种方案简洁明了,且兼容性好;通常会给父元素设置固定的高度,子元素设置width百分比布局,通过padding是基于父元素宽度前提下,进行适配

方法3、利用伪元素的 margin(padding)-top 撑开容器

<div class="box"></div>
 
<style>
    .box {
    width: 100%;
    overflow:hidden; 
    background: #F2DEDE;
    }
    .box::after {
    content: "";
    display: block;
    margin-top: 100%;
    }
</style>

由于容器与伪元素在垂直方向发生了外边距折叠,所以我们想象中的撑开父元素高度并没有出现。而应对的方法是在父元素上触发 BFC:overflow:hidden;

若使用垂直方向上的 padding 撑开父元素,则不需要触发 BFC,如下:

<div class="box"></div>
 
<style>
    .box {
    width: 100%;
    background: #F2DEDE;
    }
    .box::after {
    content: "";
    display: block;
    padding-top: 100%;
    }
</style>

注意 当元素内部添加内容时高度出现溢出,可以将内容放到独立的内容块中,利用绝对定位消除空间占用,如下:

<div class="container">
    <div class="box">内容</div>
</div>
 
<style>
    .container {
    width: 100%;
    background: #F2DEDE;
    position: relative;
    }
    .container::after {
    content: "";
    display: block;
    padding-top: 100%;
    }
    .container .box{
    position: absolute;
    width: 100%;
    height: 100%;
    }
</style>

题目二

什么是防抖和节流?如何用 JS 编码实现?

官方解析

防抖和节流都是一种优化技术,用来降低函数调用的频率,提高性能。

防抖(Debounce):在连续触发某个事件时,只有当一定时间内没有再次触发事件,才会执行事件处理函数。比如说,我们需要监听用户输入框中的文字,只有用户停止输入一段时间,才去发送请求获取数据。

节流(Throttle):在一段时间内只执行一次函数,无论事件被触发多少次。比如说,当我们需要监听用户滚动页面时,我们可以在用户滚动时,每隔一定时间就执行一次滚动事件。

下面是防抖和节流的实现代码:

防抖实现:

function debounce(func, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

节流实现:

function throttle(func, delay) {
  let timer;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

其中 func 是需要进行防抖或节流处理的函数,delay 是事件执行的最小时间间隔。在防抖中,只有当事件触发后 delay 时间内没有再次触发事件,才会执行一次 func 函数;在节流中,每隔 delay 时间执行一次 func 函数。

需要注意的是,在实际使用时,防抖和节流的处理函数应该在需要进行防抖或节流的事件上进行绑定,而不是在函数内部进行处理。

鱼皮的补充:这题可以补充一些防抖或节流在页面中的应用场景,会加分

鱼友的精彩回答

mos 的回答

防抖和节流都是一种优化技术,用来降低函数调用的频率,提高性能。

浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

防抖(Debounce):在连续触发某个事件时,只有当一定时间内没有再次触发事件,才会执行事件处理函数。比如说,我们需要监听用户输入框中的文字,只有用户停止输入一段时间,才去发送请求获取数据。

节流(Throttle):在一段时间内只执行一次函数,无论事件被触发多少次。比如说,当我们需要监听用户滚动页面时,我们可以在用户滚动时,每隔一定时间就执行一次滚动事件。

防抖 简单版本的实现

function debounce(func, wait) {
    let timeout;

    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象

        clearTimeout(timeout) //进行清除,重新计时
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:

function debounce(func, wait, immediate) {

    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout); // timeout 不为null
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
            timeout = setTimeout(function () {
                timeout = null;
            }, wait)// 一段时间后timeout设为null,又可以立即执行了
            if (callNow) {
                func.apply(context, args)
            }
        }
        else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}

节流:

时间戳 在时间段内开始的时候立刻执行

function throttle(func, wait) {
    var previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}
content.onmousemove = throttle(count,1000);

定时器

function throttle(func, wait) {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null;
                func.apply(context, args)
            }, wait)
        }
    }
}

第一次调用节流函数,还没有定时器,创建一个定时器,并在时间间隔结束时触发功能函数

在时间间隔内再次调用节流函数,由于定时器已经存在,不响应

当时间间隔结束后将本定时器标识符timeout清除,再创建一个定时器。

由于定时器标识符timeout被设置为null,再次调用节流函数便可再次触发。

Kristen 的回答

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 、mousewheel等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

防抖 (debounce)

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。 为什么要防抖 有的操作是高频触发的,但是其实触发一次就好了,比如我们短时间内多次缩放页面,那么我们不应该每次缩放都去执行操作,应该只做一次就好。再比如说监听输入框的输入,不应该每次都去触发监听,应该是用户完成一段输入后在进行触发。

总结:等用户高频事件完了,再进行事件操作。

防抖怎么做

设计思路:事件触发后开启一个定时器,如果事件在这个定时器限定的时间内再次触发,则清除定时器,在写一个定时器,定时时间到则触发

策略:当事件被触发时,设定一个周期延时执行动作,若周期又被触发,则重新设定周期,直到周期结束,执行动作。 在后期有拓展了前缘防抖函数,即执行动作在前,设定延迟周期在后,周期内有事件被触发,不执行动作,且周期重新设定。

var oInput = $('.input-box');
var oShow = $('.show-box');
var timeOut;
oInput.addEventListener('input'function () {
    timeOut && clearTimeout(timeOut);
    timeOut = setTimeout(function () {
        oShow.innerText = translate(oInput.innerText);
    }, 500);
}, false);

function translate(str) {
    return str.split("").reverse().join("");
}

节流 (throttling) 所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。

在很多时候 我们不希望这些持续性触发事件触发频率太高

通过设定在一定时间周期内只能触发一次的方式进行节流

对于节流,有多种方式可以实现 时间戳 定时器 束流等。

策略: 固定周期内,只执行一次动作,若没有新事件触发,不执行。周期结束后,又有事件触发,开始新的周期。 特点: 连续高频触发事件时,动作会被定期执行,响应平滑

计时器版

var oCon = $('.container');
var num = 0;
var valid = true;
oCon.addEventListener('mousemove'function () {
    if (!valid) {
        return false;
    }
    valid = false;
    setTimeout(function () {
        count();
        valid = true;
    }, 500);
}, false);

function count() {
    oCon.innerText = num++;
}

时间戳版

var oCon = $('.container');
var num = 0;
var time = Date.now();
oCon.addEventListener('mousemove'function () {
    if (Date.now() - time < 600) {
        return false;
    }
    time = Date.now();
    count();
}, false);

function count() {
    oCon.innerText = num++;
}

束流器版

var oCon = $('.container');
var num = 0;
var time = 0;
oCon.addEventListener('mousemove'function () {
    time++;
    if (time % 30 !== 0) {
        return false;
    }
    console.log(time)
    count();
}, false);

function count() {
    oCon.innerText = num++;
}

防抖和节流的区别

防抖和节流相同点:

防抖和节流都是为了阻止操作高频触发,从而浪费性能。降低回调执行频率,节省计算资源。

防抖和节流区别:

防抖是让你多次触发,只生效最后一次。适用于只需要一次触发生效的场景。

节流是让你的操作,每隔一段时间触发一次。适用于多次触发要多次生效的场景。

防抖是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段事件执行,函数防抖一定连续触发的事件,只在最后执行一次,而函数节流会每隔一定时间调用一次函数。

题目三

什么是 ES6 中的 Promise?它的使用场景有哪些?

官方解析

ES6 中的 Promise 是一种处理异步操作的方式,它是一个对象,用于表示一个异步操作的最终完成或失败及其结果值的表示。Promise 对象有三种状态:pending(等待中)、fulfilled(已完成)和 rejected(已失败)。

Promise 有以下几个优点:

  1. 可以避免回调地狱:将回调函数转换成了链式调用,代码可读性更好。
  2. 可以支持多个并发请求:Promise.all() 可以让多个 Promise 并行执行,提高了执行效率。
  3. 可以在异步代码中捕获错误:Promise.catch() 可以捕获异步代码中的错误。

Promise 的使用场景包括:

  1. 处理异步操作:比如 Ajax 请求、文件读取等。
  2. 优化回调函数:将回调函数转换成 Promise 链,提高代码可读性。
  3. 实现并发请求:Promise.all() 可以让多个请求并行执行。
  4. 解决回调地狱:将嵌套的回调函数转换成链式调用,提高代码可读性。

以下是使用 JS 编码实现 Promise 的例子

// 定义一个 Promise
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve('成功');
    } else {
      reject(new Error('失败'));
    }
  }, 1000);
});

// 处理 Promise 的结果
promise.then((value) => {
  console.log(value);
}).catch((error) => {
  console.error(error);
});

鱼皮补充:这题可以多说说自己在项目中是如何运用 promise 的

鱼友的精彩回答

useGieGie 的回答

ES6 中的 Promise 是一种用于处理异步操作的机制,它可以更加优雅地处理异步操作,避免了回调地狱的问题。在 JavaScript 中,Promise 对象代表一个异步操作的最终状态(成功或失败),并且可以链式调用。

Promise 的三种状态:

  • Pending(进行中):初始状态,不是成功也不是失败。
  • Fulfilled(已成功):操作成功完成,返回结果。
  • Rejected(已失败):操作失败,返回错误信息。

Promise 对象提供了 then 方法,可以将异步操作成功的回调函数和失败的回调函数注册到 Promise 对象上,当 Promise 对象状态变为 Fulfilled 或 Rejected 时,会自动调用对应的回调函数。

Promise 的使用场景有很多,比如:

  • Ajax 请求:Promise 可以用于异步获取数据,当数据请求成功时,调用 Promise 的 resolve 方法,否则调用 reject 方法。
  • 定时器:Promise 可以结合定时器一起使用,实现定时器的延时操作。
  • 图片加载:Promise 可以用于图片的异步加载,当图片加载成功时,调用 Promise 的 resolve 方法,否则调用 reject 方法。
  • 异步编程:Promise 可以帮助开发者更好地处理异步编程,避免了回调地狱的问题。

需要注意的是,Promise 的 then 方法是异步执行的,也就是说,当 Promise 对象状态变为 Fulfilled 或 Rejected 时,then 方法中的回调函数并不是立即执行,而是放入微任务队列中,在 JavaScript 主线程执行栈为空时,才会被调用执行。

除了 then 方法外,Promise 还提供了一些其他方法:

  1. catch 方法:捕获 Promise 对象的错误信息,相当于 then(null, onRejected)。

  2. finally 方法:无论 Promise 对象状态如何,finally 方法总是会执行,通常用于资源清理等操作。

  3. Promise.all 方法:接收一个 Promise 对象数组,当所有 Promise 对象状态都变为 Fulfilled 时,返回一个 Promise 对象,状态为 Fulfilled,并携带所有 Promise 对象的结果;当有任意一个 Promise 对象状态变为 Rejected 时,返回的 Promise 对象状态变为 Rejected,并携带第一个 Promise 对象的错误信息。

  4. Promise.race 方法:接收一个 Promise 对象数组,当其中任意一个 Promise 对象状态变为 Fulfilled 或 Rejected 时,返回一个 Promise 对象,状态为对应 Promise 对象的状态,并携带对应 Promise 对象的结果或错误信息。

通过这些方法,可以更加方便地处理异步编程的问题。需要注意的是,Promise 也有一些缺点,比如无法取消 Promise,一旦创建就会立即执行,无法处理同步操作等。

Kristen 的回答

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果 Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。 Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。 then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。

因此可以采用链式写法,即then方法后面再调用另一个then方法。


getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

上面的代码使用 then 方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。 下面是一个Promise对象的简单例子。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');//setTimeout的第三个参数是给第一个函数的参数
  });
}

timeout(100).then((value) => {
  console.log(value);
});

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。

Promise 新建后就会立即执行。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

下面是异步加载图片的例子。

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。

下面是一个用Promise对象实现的 Ajax 操作的例子。

const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept""application/json");
    client.send();

  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出错了', error);
});

星球活动

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

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

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

加入我们

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

💎 加入星球后,您可以:

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

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

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

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

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

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

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


继续滑动看下一个

MySQL 中的索引是怎么实现的?

编程导航和鱼友们 面试鸭
向上滑动看下一个

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

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