JavaScript 是一种单线程编程语言,它本身没有异步编程的功能。它是如何实现异步的呢?如果是调用 V8 引擎的 c++ 代码,又是如何调用的呢?
JavaScript 单线程的性质,决定了需要执行的任务,只能排队执行,这个队列我们称为任务队列。如下图
当 JavaScript 每有一个任务需要执行,就会将这个任务放入宏任务这个队列中去排队。
执行栈负责执行任务,当执行栈为空,会优先从微任务队列中取一个微任务执行。
如果执行栈为空,微任务队列也为空,就会去宏任务队列中取一个宏任务进行执行。
在执行宏任务的时候,可能会遇见微任务,微任务会先放入微任务队列等待。
执行栈循环取任务执行的过程,就是事件循环 Event Loop 。如下图所示
当执行任务时,遇见异步任务,则将任务转移到异步处理模块,等待异步触发条件,例如定时器时间到,或者 ajax 请求被返回等。
当异步处理模块中的任务被触发时,就会将异步任务放入任务队列让执行栈执行。
我们用一段简单的代码,来演示执行的过程
console.log(1); function fn1() { console.log(2); } function fn2() { console.log(3); fn1(); } setTimeout(() => { console.log(4); }, 1000); fn2();执行栈为空,发现微任务队列为空,从宏任务队列取一个宏任务开始执行,加入整体代码的入口,如下图
开始执行第一句代码
执行完成后,出栈,继续向下执行
函数 fn1() 和 fn2() 没有被调用,所以不执行。遇见 setTimeout() 定时器,属于异步任务,交给异步处理模块,等待触发条件(计时结束)。
调用 fn2() 函数,将 fn2() 函数加入执行栈
执行 fn2() 中的代码,首先执行打印语句
打印完成后,打印语句出栈,然后向下执行。调用 fn1() 函数,将 fn1() 加入执行栈
继续执行 fn1() 中的代码,打印 2
打印结束,将打印语句出栈。我们假设此时任务队列新增了任务,它会等待当前宏任务执行完成并清空微任务队列。
打印语句出栈后,fn1() 就执行完了,所以 fn1() 出栈。
fn1() 出栈出栈后,fn2() 也执行完了,然后 fn2() 出栈。然后 script 任务就执行完毕了,script 也出栈。这里就不一个一个的画了,全部出栈后如下图
此时,执行栈为空了,优先去执行微任务队列中的任务,由于刚才的代码中没有微任务,所以微任务队列为空,就从宏任务队列中取一个新的宏任务执行。
假设此时定时器的时间到了,异步处理模块会将这个触发后的任务放入任务队列。
轮到触发后的定时器了,执行定时器中的代码
具体流程不再一步一画了,关键的一步如下
运行代码,验证一下整个流程
Promise 可以简单的理解为一个数据的容器,这个容器中保存着某个未来才会结束的事件的结果。
Promise 一共有三种状态,进行中( pending )、已成功( fulfilled )、已失败( rejected )。
只有内部执行的代码可以决定当前是哪一种状态,其他的任何操作都无法改变这个状态。
Promise 状态的变化只能有两种,从 pending 状态变为 fulfilled 或者从 pending 状态变为 rejected 。一旦成功或失败,状态就凝固了,不会再发生任何改变。
Promise 一旦被新建,就一定会执行,不能中途取消。
Promise 对象的 then 方法总是等待 resolve 语句或者 reject 语句的执行,并取得传入的数据。如下
let p = new Promise(function (resolve, reject) { let a = 5; resolve(a); }); p.then((data) => { console.log(data); });上面代码中,p 对象的 then() 方法,等待 resolve(a) 语句的执行,并取得 a ,所以打印 5 。 对代码稍作改动。
let p = new Promise(function (resolve, reject) { setTimeout(() => { let a = 5; resolve(a); }, 2000); }); p.then((data) => { console.log(data); }); console.log(10);在上面的代码中,then 方法会等待两秒后 resolve 语句执行,再取出数据打印。 关于等待功能的原理,下文会详述。
then() 方法的返回值是一个新的 Promise 对象,如下
let p = new Promise(function (resolve, reject) { let a = 5; resolve(a); }); let p1 = p.then((data) => {}); console.log(p1); console.log(p1 == p);结果如下
如果返回的不是一个 Promise 对象,那么会自动将它包装成一个 Promise 对象。所以你可以继续调用 then 方法。
let p = new Promise(function (resolve, reject) { let a = 5; resolve(a); }); p.then((data1) => { console.log("data1 ", data1); return 10; }).then((data2) => { console.log("data2 ", data2); });运行结果如下
then() 方法一共有两个参数且都是函数,当执行 resolve 语句时,触发第一个参数,当执行 reject 语句时,触发第二个参数,第二个参数是可选的。
let p = new Promise(function (resolve, reject) { let a = 5; reject(a); }); p.then( (data) => { console.log("param1 ", data); }, (fail) => { console.log("param2 ", fail); } );结果如下
如果执行了 reject 语句,但 then 方法没有第二个参数,那么失败的信息会向外抛出。如下所示
let p = new Promise(function (resolve, reject) { let a = 5; reject(a); }); let p1 = p.then((data) => { console.log(data); });结果如下
抛出的失败信息,可以使用 catch() 方法来接收它,例如:
let p = new Promise(function (resolve, reject) { let a = 5; reject(a); }); let p1 = p .then((data) => { console.log(data); }) .catch((fail) => { console.log("fail ", fail); });Promise 对象的实例化属于宏任务,而 then() 方法中的代码则是微任务。来看下面的代码
console.log(1); let p = new Promise((resolve, reject) => { for (var i = 0; i < 10000; i++) {} console.log(2); resolve(3); }); console.log(4); p.then((data) => { console.log(data); }); console.log(5);执行上面的代码,我们通过任务队列来分析整个过程。首先,整个 script 进入宏任务队列,如下图所示
然后执行栈加载代码入口 script ,并执行第一句代码。
执行结束后,执行实例化 Promise 的代码
实例化时,开始执行内部的 for 循环语句,for 循环是宏任务,所以会直接执行。
执行完毕后,for 循环语句出栈,执行下一个打印语句
打印语句结束,执行 resolve 语句,返回成功信息 3 。
返回信息后,resolve 语句出栈。
这里有一个误区,并不是 reslove 语句执行后,一定立刻执行 then 方法,而是执行至 then 语句的时候,才会取产生的信息。
就好比去餐馆吃饭,点菜等价于返回了信息,但是点了菜并不一定立刻开始做你的菜,因为你的前面可能还有客人在等待。当做完你前面所有客人的菜后,才开始做你的菜,就就相当于执行到 then 方法了,这时候,才会取你的订单信息。
然后 Promise 实例化完成,实例化语句出栈。继续向下执行
打印完成,打印语句出栈。向下执行 then 语句。
因为 then 语句属于微任务,所以不会立即执行,而是先放入微任务队列等待,然后继续向下执行。
打印结束,出栈。至此,script 所有内容执行结束,出栈。
出栈后,整个宏任务执行结束了,执行栈为空。于是,优先清空微任务队列,开始执行 then 语句。
执行结束后,逐个出栈。运行代码,来检验整个流程
所以,promise 对象的 then 方法就出现了“等待”的效果。
绝大部分的面试题,就是在上面的基础上,添加一个异步任务,典型的就是计时器。这里我们举一个例子,完整分析整个流程。
let p = new Promise((resovle, reject) => { setTimeout(() => { console.log(1); }, 0); console.log(2); resovle(3); }); console.log(4); p.then((data) => { console.log(data); }); console.log(5);首先执行栈加载 script 整个宏任务,也就是代码入口。然后执行第一条语句,如下图
然后执行 Promise 内部代码,如下图
由于定时器是异步代码,所以转移至异步模块,等待触发条件,如下图
转移后,继续向下执行,打印 2 。如下图
打印完成后,向下执行 resovle 语句,我们假设此时定时器触发,异步处理模块将其加入宏任务队列。如下图所示
resovle 语句执行完毕,出栈。此时 Promise 实例化完成,出栈。
继续向下执行,打印 4 。
打印完成,出栈。然后执行 p.then() 语句,由于 then 语句属于微任务,所以先放入微任务等待。如下图所示
继续向下执行,打印 5 。
打印结束,出栈。此时 script 整宏任务执行结束,出栈。执行栈为空,于是优先执行微任务队列所有微任务,如下图所示
执行 then 语句,然后执行 then 内部的打印语句。resolve 语句返回了成功信息 3 ,所以形参 data 为 3 。
打印完毕,出栈。then 语句也执行完毕,出栈。此时执行栈为空,微任务队列也为空,所以从宏任务队列加入一个新的宏任务,开始执行。如下图所示
执行定时器,然后执行定时器内部的打印语句,打印 1 。
打印结束,打印语句出栈。然后定时器执行完了,定时器出栈。整个流程就结束了,我们运行代码,验证一下。