壹 ❀ 引

就在昨天,与朋友聊到JS基础时,她突然想起之前在面试时,遇到了一道难以理解的Promise执行顺序题。由于我之前专门写过手写promise的文章,对于部分原理也还算了解,出于兴趣我便要了这道题的代码,想看看自己现在的理解能做到什么程度,顺便也给对方解疑答惑,代码如下:

function doSomething() {
return Promise.resolve(1);
}; function doSomethingElse() {
return Promise.resolve(2);
};
// 执行1
doSomething()
.then(() => {
return doSomethingElse()
})
.then(val => console.log('a', val))
// 执行2
doSomething()
.then(() => {
doSomethingElse()
})
.then(val => console.log('b', val))
// 执行3
doSomething()
.then(doSomethingElse())
.then(val => console.log('c', val))
// 执行4
doSomething()
.then(doSomethingElse)
.then(val => console.log('d', val)) // 要求给出上面4个promise的执行顺序,以及输出内容

可以看到这是一个纯Promise的执行顺序题,没有setTimeoutasync await的干扰。但在我尝试分析后我发现,这道题考研的细节还挺多,如果没有深入了解过Promise源码,可以毫不夸张的说,这道题不可能做的对。而我自己手写Promise也是2个多月前的事情了,所以初次分析果然做错了。

在我重读了自己手写Promise的文章后,重新捡起了当时记录的部分细节,也非常顺利的把这道题的细节都给挖了出来,为了方便讲给朋友听,也方便自己日后回顾,这里我就做个记录,那么本文开始。

贰 ❀ 前置概念与细节分析

在题解分析之前,我先把这道题的考点单独拧出来先讲一遍。毕竟当一个问题我们觉得难到自己无法理解时,那往往是我们对于需要使用的知识掌握是非常薄弱的。如果知道考什么,我们还能根据现有的知识体系去做分析和猜测,如果考了什么都不知道,那就是猜就不知道怎么猜了。所以这一小节,我们先普及这道题中考察的细节与概念,在此之后我们再去看题解分析,起码能做到有部分概念能为自己做理解支撑。

注意,以下四个细节点在因两道Promise执行题让我产生自我怀疑,从零手写Promise加深原理理解一文中均有讲解,要了解底层原理还是建议阅读此文,当然你也可以先不理解原理,只是作为硬性概念先记忆,这一点大家自行安排。

贰 ❀ 壹 then()中创建微任务的次数

我们知道,new一个Promise的过程是同步的,包括.then()注册callback的行为也是同步的,真正异步的是被注册的成功以及失败的回调(它们得等待状态改变,而且不知道什么时候才会改变),且它们都是微任务。

const P1 = new Promise((resolve, reject) => {
console.log('new Promise是同步操作')
resolve(1);
});
setTimeout(() => {
console.log('我是异步宏任务')
}, 0)
P1.then((e) => {
console.log('我是异步微任务')
console.log(e);
})
// new Promise是同步操作
// 我是异步微任务
// 我是异步宏任务

而当存在多个Promise调用需要区分执行顺序时,我们往往以.then()注册callback的顺序来决定执行顺序:

const P1 = Promise.resolve(1);
const P2 = Promise.resolve(2);
P1.then((e) => {
console.log('我先注册的,所以先输出1')
console.log(e);
})
P2.then((e) => {
console.log('我后注册的,所以我后面输出2')
console.log(e);
})
// 我先注册的,所以先输出1
// 1
// 我后注册的,所以我后面输出2
// 2

.thencallback中往往也能再返回一个Promise,这时候就是我们所说的链式调用,而当存在多个链式调用时,我们心里会默认,只要你多.then()一次,你的执行就得往后排一次,比如:

const P1 = Promise.resolve(1);
const P2 = Promise.resolve(2);
P1.then((e) => {
console.log(e)
return Promise.resolve(3)
})
.then((e) => console.log(e));
P2.then((e) => {
console.log(e);
})
// 1
// 2
// 3

这个例子的3一定是晚于2输出的,我们就站在宏观的角度去理解,因为3多了一层.then(),那么你肯定得比2还要往后面排一排,最后输出毫无疑问,我想很多人对于这个问题的理解都是这样的。

但是,现在请大家记住第一个硬性概念,假设.then()内部返回了一个Promise,那么这个Promise的执行得往后延后两次次,而不是一次,这是因为.then()中的Promise在改变状态到执行,底层会创建2次微任务,导致它的执行往后推两次。

来看个例子:

Promise.resolve()
.then(() => {
console.log(0);
// then内部返回promise,默认理解成延迟2次后执行
return Promise.resolve(4);
}).then((res) => {
console.log(res)
}) Promise.resolve()
.then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
})
// 0 1 2 3 4

先输出0,再输出1肯定毫无悬念,但是由于第一个的then()返回了一个Promise,我们默认它得往后延迟2位,所以2,3先走,4最后执行。这个例子,也是我当时手写Promise的起因,细节如果再在这里展开讲会很复杂,还是建议阅读手写Promise一文,这里大家先作为概念去记忆。

贰 ❀ 贰 Promise.then的有无返回值的区别

我们知道.then()可以返回一个Promise然后使用链式调用,但事实上也存在返回不是Promise或者不返回的情况。我们先说返回的情况。

假设返回的是Promise,那么下一个.then肯定得等待这个Promise状态发生改变才能执行,但其实我们还有返回不是Promise的情况:

Promise.resolve(1)
.then(
(e) => {
console.log(1)
return 2;
}
).then((e) => {
console.log(e)
return 3;
})
.then((e) => {
console.log(e)
});
// 1 2 3

上述例子中返回的数字,有点类似于:

Promise.resolve(1)
.then(
(e) => {
console.log(1)
return Promise.resolve(2);
}
).then((e) => {
console.log(e)
return Promise.resolve(3);
})
.then((e) => {
console.log(e)
});

你现在只用知道当返回一个数字或者字符串,只要不是Promise,它本质上都会被resolve转化成成功的状态,需要注意的是return 4return Promise.resolve(4)还是有区别,比如上面创建2次微任务的例子,我们假设改成return 4

Promise.resolve()
.then(() => {
console.log(0);
// then内部返回promise,默认理解成延迟2次后执行
return 4;
}).then((res) => {
console.log(res)
}) Promise.resolve()
.then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
})
// 0 1 4 2 3

可以看到此时4跑到了2,3前面,类似于只创建了一次微任务,return 4return Promise.resolve(4)相比,后者比前者多创建一次微任务,这个细节在手写中也能体现,这里就不花篇幅再说了。

说了返回Promise和非Promise的情况,我们再来聊聊无返回的情况,比如:

Promise.resolve()
.then(() => {
console.log(0);
// 没有返回
}).then((res) => {
console.log(res)
})
// undefined

为什么是undefined,因为函数如果没返回值时,默认表示为返回undefined,那既然return undefined,不就是类似于return Promise.resolve(undefined),因此输出undefined毫无疑问。

关于返回我们先聊到这,理解了这个知识点,我们可以回到文章开头的题目,看看执行2。

贰 ❀ 叁 Promise.then的值穿透

在手写Promise的文章中,我们特意提及过.then()方法的值穿透问题,如果你没看过本文,那么请将下面的话当成硬性的概念记下来。

当Promise.then()的没有提供callback,或者callback不是一个函数时,Promise会发生值穿透

我们先来看第一个例子,.then()没有提供callback的情况:

const P1 = Promise.resolve('值穿透');
P1.then((res) => {
console.log(res); // 值穿透;
}); P1.then()
.then()
.then()
.then((res) => {
console.log(res); //值穿透
});

上述代码中,我们创建了一个promise P1,那么第一段执行毫无悬念肯定输出值穿透。而第二个执行,我们链式调用了多个.then()并且都没有提供成功或者失败的回调,这种情况就会导致状态和值发生穿透,因此最后一个.then()还是能成功获取到值穿透

我们再来看callback不是函数的情况,例子如下:

const P1 = Promise.resolve('值穿透');

const fn1 = () => {
console.log(1)
}; P1.then(fn1())
.then(1)
.then('2')
.then((res) => {
console.log(res); //值穿透
});

在这个例子中,我们并没有为任何一个.then提供一个函数,有的.then()直接丢了一个数字进去了,有的then()丢了字符串进去。这时候有同学就要说了,不对啊,你第一个then()传递的fn1()难道不是函数?

这里我们就要搞清楚一个概念了,所谓callback是传递一个函数作为callback,等状态确定了Promise帮你调用,而fn1()这玩意自己直接被调用,它哪里是一个函数呢?

我们用一个最基本的例子来解释函数调用函数引用

const fn2 = () => {
return '函数调用与函数引用的区别';
} const o = {
f1: fn2, // 这个叫函数引用
f2: fn2() // 这个叫函数调用
};
console.log(o.f1()); // 函数调用与函数引用的区别
console.log(o.f2); // 函数调用与函数引用的区别

我们定义了一个函数叫fn2,然后把fn2的引用赋予给了对象of1属性,因此我们能通过o.f1()调用fn2,这就是函数引用。

而我们将fn2()赋予给了of2属性,因此o.f2根本就不是一个函数,它保存的是fn2()执行之后的返回结果,这就是函数调用。

所以回到上面值穿透第二个例子,我们传递的是一个函数调用,你自己调用了,我堂堂Promise不要面子的吗,你让我等会状态改变了调用什么?而后面的.then(1) .then('2')同理,它们都不是函数,Promise会直接忽略这些无意义的传递,依旧值穿透。

而关于值穿透,若你要问我为什么,我只能说没有为什么,因为Promise就是这么实现的。这就跟你问我为什么1 + 1 = 2一样,没有为什么,因为1 + 1就是等于2,此刻它不是作为要理解的概念,而是已成为我们理解更宏观概念的工具或者基石。值穿透的本质也是为了让Promise实现链式调用,至于Promise是如何实现的这种链式调用,请参考手写Promise一文。

那么关于值穿透就说到这里,这时候你可以根据文章开头题目的执行结果,尝试的去理解题目的执行3了。

贰 ❀ 肆 Promise.then中callback的简写方式

在前文,我们提到了函数引用与函数调用,而.then()callback在简写上其实也跟函数引用有一定关系,比如:

const fn1 = (e) => {
console.log(e) // 1
}
Promise.resolve(1)
.then(fn1); // 本质上等同于
Promise.resolve(2)
.then((e) => {
console.log(e) //2
})

比如上面的.then(fn1)本质上跟下面的写法是一样的,无非是前者把函数提前定义了,然后把fn1作为callback传递给了.then,后者是直接在.then里面写了一个匿名箭头函数。

同类型的简写还有:

setTimeout(console.log, 0, 'echo'); // echo
// 等同于
setTimeout((e) => {
console.log(e)
}, 0, 'echo'); // echo

知道了这个,我们可以回头看看文章开头的执行4。

叁 ❀ 题解分析

上文我们阐述了这道题的4个考点,大家可以看完后结合我给的概念再回头根据输出,反向思考下为什么,接下来我来改写上面题目,给出我的题解分析:

function doSomething() {
return Promise.resolve(1);
}; function doSomethingElse() {
return Promise.resolve(2);
};
// 执行1
doSomething()
.then(() => {
// 因为返回了一个promise,它又在then里面,3次微任务后输出 a 2
return doSomethingElse()
})
.then(val => console.log('a', val))
// 执行2
doSomething()
.then(() => {
// 这里有执行,但是没返回,所以等同于resolve(undefined),2次微任务后输出b undefined
doSomethingElse()
})
.then(val => console.log('b', val))
// 执行3
doSomething()
// callback直接不是一个函数,值穿透,相当于把前面的状态和值传递下去,所以也是2次微任务后输出 c 1
.then(doSomethingElse())
.then(val => console.log('c', val))
// 执行4
doSomething()
// .then(doSomethingElse)
// callback是函数引用,等同于
.then(() => {
// 与执行1相同,then里面返回promise,3次微任务后输出 d 2
return doSomethingElse();
})
.then(val => console.log('d', val)) // 综合一下,2次微任务的先执行,3次微任务的谁先注册谁先执行,因此输出为:
// b undefined
// c 1
// a 2
// d 2

先看执行1:

doSomething()
.then(() => {
return doSomethingElse()
})
.then(val => console.log('a', val))

概念1说了,.then里面返回Promise时,创建2次微任务,因为自己又被包裹在.then里面,结合起来就是doSomething().then()创建了一次微任务,然后returndoSomethingElse()自己又是一个Promise,所以doSomethingElse().then()创建了2次微任务,一起三次微任务,我们记录为3-a-2;

再来分析执行2:

doSomething()
.then(() => {
// 这里有执行,但是没返回,所以等同于resolve(undefined),2次微任务后输出b undefined
doSomethingElse()
})
.then(val => console.log('b', val))

根据概念2,很明显这个doSomethingElse()前面没有return,这就导致doSomethingElse返回的Promise不能被二次return出去,既然没返回,那就是默认理解成resolve(undefined),由于不是返回了一个Promise,所以只会创建2次微任务,我们记录为2-b-undefined

执行3:

doSomething()
// callback直接不是一个函数,值穿透,相当于把前面的状态和值传递下去,所以也是2次微任务后输出 c 1
.then(doSomethingElse())
.then(val => console.log('c', val))

参考概念3,由于.then()接收的是一个函数调用,根本就不是一个函数,这里直接值穿透,与执行2类似,一共创建2次微任务,这里我们记录为2-c-1。(1是doSomething穿透传递下来的)。

最后看执行4:

doSomething()
.then(doSomethingElse)
.then(() => {
// 与执行1相同,then里面返回promise,3次微任务后输出 d 2
return doSomethingElse();
})
.then(val => console.log('d', val))

参考概念4,这里的doSomethingElse是函数引用,所以等同于:

doSomething()
.then(() => {
return doSomethingElse();
})
.then(val => console.log('d', val))

这样一改,是不是跟执行1其实是一样的,.then里面返回了一个Promise,创建了一共3次微任务,所以这里我们记录为3-d-2

综合一下,创建微任务越少,肯定越先执行,而相同微任务次数的,谁先注册谁先执行,因此输出结果以及顺序为:

b undefined, c 1, a 2, d2

到这里,我们从概念普及到题解分析已经完成结束,不知道与你脑中的理解是否一致呢。

肆 ❀ 总

本来是一道看起来非常简单的执行题,结果真要拆开说,里面真的暗藏玄机,而我也没想到一篇题解居然写了四千多字,我想我自己应该是非常透彻了去介绍了这道题考核的知识点,若还有疑问欢迎留言,我会一一解答,那么到这里本文结束。

超耐心地毯式分析,来试试这道看似简单但暗藏玄机的Promise顺序执行题的更多相关文章

  1. UML和模式应用3:迭代和进化式分析和设计案例研究

    1.前言 如何进行迭代和进化式分析和设计?将采用案例研究的方式贯穿始终.案例研究所包含的内容: UI元素 核心应用逻辑层 数据库访问 与外部软硬构件的协作 本章关于OOA/D主要介绍核心应用逻辑层 2 ...

  2. 分层、链式分析、url、联系的长度

    分层.链式分析.url.联系的长度. 分层结构符合软件处理的工具链性和步骤性: 分层的每一次都是一个节点或步骤: 链式结构普遍存在于自然界,比如食物链: 联系是普遍存在的,不只是两个事物间的联系,而且 ...

  3. StreamDM:基于Spark Streaming、支持在线学习的流式分析算法引擎

    StreamDM:基于Spark Streaming.支持在线学习的流式分析算法引擎 streamDM:Data Mining for Spark Streaming,华为诺亚方舟实验室开源了业界第一 ...

  4. 响应式编程笔记三:一个简单的HTTP服务器

    # 响应式编程笔记三:一个简单的HTTP服务器 本文我们将继续前面的学习,但将更多的注意力放在用例和编写实际能用的代码上面,而非基本的APIs学习. 我们会看到Reactive是一个有用的抽象 - 对 ...

  5. python各种推导式分析

    推导式comprehensions(又称解析式),是Python的一种独有特性.推导式是可以从一个数据序列构建另一个新的数据序列的结构体. 共有三种推导,在Python2和3中都有支持: 列表(lis ...

  6. 【miscellaneous】星光级超低照度摄像机技术分析

    低照度摄像机采用了超灵敏度图像传感器和独有的电子倍增和噪点控制技术能够极大地提高摄像机的灵敏度,并且具备24小时全彩色实时效果,绝无普通低照度摄像机出现的拖尾现象,以满足对夜间高品质监控的需求.    ...

  7. KMP(超详细复杂度分析)

    从 stackoverflow中找到了一个时间复杂度分析很棒的链接 https://www.inf.hs-flensburg.de/lang/algorithmen/pattern/kmpen.htm ...

  8. JS地毯式学习四

    1  窗口的位置 用来确定和修改 window 对象位置的属性和方法有很多. IE . Safari . Opera 和 Chrome都提供了 screenLeft 和 screenTop 属性,分别 ...

  9. JS地毯式学习三

    1. 插件是一类特殊的程序 . 他可以扩展浏览器的功能 , 通过下载安装完成 . 比如 , 在线音乐.视频动画等等插件. // 检测非 IE 浏览器插件是否存在function hasPlugin(n ...

随机推荐

  1. 为MySQL加锁?

    在日常操作中,UPDATE.INSERT.DELETE InnoDB会自动给涉及的数据集加排他锁,一般的 SELECT 一般是不加任何锁的.我们可以使用以下方式显示的为 SELECT 加锁. 共享锁: ...

  2. java动态代理--代理接口无实现类

    转载:https://blog.csdn.net/weixin_45674354/article/details/103246715 1.接口定义: package cn.proxy; public ...

  3. Idea集成CSSO插件压缩css文件

    首先需要本地已安装node环境,并且csso-cli已通过npm安装到本地目录,只要能找到就行. 1. 打开Settings配置,确认图中的 File Watchers 插件是否已存在,如果不存在,去 ...

  4. 手撕代码之线程:thread类简单使用

    转载于:https://blog.csdn.net/qq_22494029/article/details/79273127 简单多线程例子: detch()启动线程: 1 #include < ...

  5. kafka follower如何与leader同步数据?

    Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制.完全同步复制要求All Alive Follower都复制完,这条消息才会被认为commit,这种复制方式极大的影响了吞吐率.而异步复制 ...

  6. java-方法引用

    /** * 方法引用格式: * 双冒号:: 引用运算符,它所在的表达式被称为方法引用.如果Lambda表达式 * 的函数方案已经存在于某个地方的实现中, * ===>那么可以通过双冒号来引用改方 ...

  7. vmware克隆Centos虚拟机网卡无法启动问题

    快速处理办法: cat /etc/sysconfig/network-scripts/ifcfg-eth0 sed -i '/UUID/d' /etc/sysconfig/network-script ...

  8. js技术之分割split()

    案例:把所有单词以空格为分割并将首字母转为大写 <!DOCTYPE html><html lang="en"><head> <meta c ...

  9. Docker镜像构建之docker commit

    我们可以通过公共仓库拉取镜像使用,但是,有些时候公共仓库拉取的镜像并不符合我们的需求.尽管已经从繁琐的部署工作中解放出来了,但是在实际开发时,我们可能希望镜像包含整个项目的完整环境,在其他机器上拉取打 ...

  10. 5. Git初始化及仓库创建和操作

    4. Git初始化及仓库创建和操作 基本信息设置 1. 设置用户名 git config --global user.name 'itcastphpgit1' 2. 设置用户名邮箱 git confi ...