面试复习计划JavaScript篇四

261 阅读11分钟

引言

这篇我们来复习promise

Ⅰ抛出问题

在讲promise之前,我们需要了解以下JavaScript的一大特点——单线程

——单线程

JavaScript最大的特点就是单线程,即同一时刻只能执行一件事。所有添加的任务都需要排队,只有前面一个任务完成,后面的任务才能得到执行,否者就得一直等着。

因此,如果有IO设备正在等待执行,比如Ajax,若未能读取到数据就会一直等待,出了结果才能继续执行。

——同步与异步

我们之前讲过执行上下文栈的概念,它依靠后进先出的行事风格为人处世。

同步任务指的是,在主线程中,排队压入执行栈的任务,当前一个任务执行完毕,下面的任务才能继续执行。

在主线程之外还存在一个“任务队列”,而“任务队列”的行事风格则是先进先出。如果在程序中碰到某些有“便秘”的任务时,这种任务会进入任务队列。当任务队列中的一个任务”擦完屁股“,准备完毕可以执行时,该任务才会转到执行栈中,这种任务叫做异步任务。

虽然js是单线程的,但是浏览器的渲染进程是多线程的,js也是其中的一条,浏览器一般包括以下几种线程:

  • GUI渲染线程,负责浏览器页面的渲染,与JS引擎线程互斥
  • JS引擎,我们的主角,它的职责当然是执行js代码,它跟楼上过不去,JS引擎执行时,GUI就不会执行。如果JS计算时间过长,将会导致页面渲染阻塞,让我们更直观的感受到页面卡顿。
  • 事件触发线程,用来控制事件循环,刚刚我们提到的任务队列就是由它管理的
  • 定时触发器线程,我们很熟悉的的setInterval和setTimeout就在这个线程中,没错,它们归浏览器管,并不是js引擎
  • 异步http请求线程,我们知道Ajax的核心就是XMLHttpRequest,XMLHttpRequest对象就属于这一线程。

知道了这些,我们就可以更好的来了解JS的运行机制了,JavaScript采用的是Event Loop运行机制,它的整体执行流程大致如下:

  1. 主线程中,js引擎往执行栈中压入一个同步任务开始执行
  2. 当碰到异步任务的时候,js引擎会将其放入事件列表(event table)并注册函数,直到该事件符合触发条件时,事件触发线程会把事件推入事件队列(event queue)中,等待JS引擎的处理
  3. 随后,当前执行栈中的一个任务被处理完毕,js引擎去查看任务队列中的事件,开始执行队列中的事件
  4. 执行队列中的事件被执行完毕后,js引擎往执行栈中压入下一个同步任务开始执行...

此后不断循环这几步,如此看来,每当执行栈中的一个同步任务执行完毕,无论任务队列中是否为空,js引擎都会去查看。没有就直接开启下一轮循环,有则全部执行。

看完这些,我们举个例子:

function double(value){
    setTimeout(()=>setTimeout(console.log,0,value*2),1000);
}
double(3);

在这个异步函数中,我们需要理解的是,函数中的setTimout并不是在1000毫秒之后直接执行里面的回调,而是在1000毫秒之后的某个时间执行。当js引擎初遇setTimout语句时,它就把回调函数放入事件列表中注册事件,1000毫秒之后将其推入事件队列等待主线程完成当前执行栈中的任务之后调用。而double函数在setTimeout成功调度异步操作之后会立即退出。

——异步操作

先别急着看promise出场,我们首先来了解下在promise之前,以往的异步编程方式。

首先,异步行为可能会产生一个值,而这个值也许会在其他地方需要用到,那我们怎么样使异步行为返回这样一个值呢?答案是为异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调参数),看下例:

function double(value,callback){
    setTimeout(() => callback(value*2),1000);
}
double(3,(x) => console.log(`I was given: ${x}`));		//I was given: 6

然后,异步行为还应该有失败处理方法,那么就为异步函数添加成功处理回调和失败处理回调:

function double(value,success,failure){
    setTimeout(() => {
        try{
            if(typeof value !== 'number'){
                throw 'Must provide number as first argument';
            }
            success(2 * value);
        }
        catch(e){
            failure(e);
        }
    },1000)
}
const successCallback = (x) => console.log(`success: ${x}`);
const failureCallback = (e) => console.log(`failure: ${e}`);
double(3,successCallback,failureCallback);				//success: 6
double('b',successCallback,failureCallback);			//failure: Must provide number as first argument

如果,异步返回值又依赖另一个异步返回值,那么回调的情况还会进一步变得复杂。在实际的代码中,这就要求嵌套回调:

function double(value,success,failure){
    setTimeout(() => {
        try{
            if(typeof value !== 'number'){
                throw 'Must provide number as first argument';
            }
            success(2 * value);
        }
        catch(e){
            failure(e);
        }
    },1000)
}
const successCallback = (x) => {
    double(x,(y) => console.log(`success: ${y}`));
}
const failureCallback = (e) => console.log(`failure: ${e}`);
couble(3,successCallback,failureCallback);				//success: 12

显然,随着代码越来越复杂,回调策略是不具有扩展性的。嵌套回调的代码维护起来极为困难,因此以往的异步编程方式又被称为“回调地狱”。

Ⅱ他来了,他来了,promise来了

——介绍大哥

promise是ECMAScript 6新增的引入类型,它通过new操作符来实例化。

promise存在三种状态,且必然处于其中一种状态,分别是:

  • pending——待定状态,promise初始状态。
  • fulfilled——兑现状态,由pending状态落定,且一经落定无法改变。代表成功的兑现状态。
  • rejected——拒绝状态,由pending状态落定,且一经落定无法改变。代表着失败的拒绝状态。

promise能做什么:

  • 解决“回调地狱”,是代码变得易于维护且富有诗意~
  • 支持并发请求,获取并发请求的数据
  • 它可以解决异步问题

——实例化promise

实例化过程中需要传入一个回调函数调用执行器函数,回调函数中包括两个参数,分别为resolve和reject,用于控制状态转换,调用前者会转换为fulfilled状态,后者会转换为rejected状态。

let p1 = new Promise((resolve,reject)=>{})
let p2 = new Promise((resolve,reject)=>{
    resolve();
})
let p3 = new Promise((resolve,reject)=>{
    reject();
})
console.log(p1);			//Promise{<pending>}
console.log(p2);			//Promise{<fulfilled>:undefined}
console.log(p3);			//Promise{<rejected>:undefined}

另外上例的执行器函数还可以简写成如下,使得promise一经初始化就处于一种非待定状态。

let p1 = Promise.resolve('Yes');
let p2 = Promise.reject('No');
console.log(p1);			//Promise{<fulfilled>:Yes}
console.log(p2);			//Promise{<rejected>:No}

如果调用resolve函数或reject函数时带有参数,那么它们的参数会被传递给回调函数。它们的参数可以是非promise的值,也可以是promise。

如果传入的值为promise的话,如下例所示

let p1 = new Promise((resolve,reject)=>{});
let p2 = new Promise((resolve,reject)=>{
    resolve(p1);
})

p1和p2都是promise的实例,但p2的resolve函数却将p1作为参数,即一个异步操作的结果是返回另一个异步操作。此时,p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如上例,由于p1的状态一直处于pending,所以p2也无法继续往下执行。只有p1的状态由pending 改变为fulfilled或rejected,P2才会继续往下执行。

let p1 = new Promise((resolve,reject)=>{
    reject('No');
});
let p2 = new Promise((resolve,reject)=>{
    resolve(p1);
}).then(value=>console.log(value),reason=>console.log(reason))
setTimeout(()=>{
    console.log(p1);		//Promise{<rejected>:No}
	console.log(p2);		//Promise{<rejected>:No}
},0)

当p1的状态变为rejected的时候,即使p2调用的是resolve函数,也不可避免地被p1"同化",另外p2的处理方法也受其影响,直接执行拒绝操作。

——then方法

then方法也就是Promise.prototype.then()方法,用于为promise实例添加处理程序的主要方法。

这个then()方法接收两个参数,分别是onResolved和onRejected,前者是操作成功的处理程序,后者是操作拒绝的处理程序。这两个参数都是可选的,如果只想使用其中一种处理程序,另一种就得设为null。

let p1 = Promise.resolve("It works").then(value=>console.log(value),null);
//It works
let p2 = Promise.reject("It doesn't work").then(null,reason=>console.log(reason));
//It doesn't work

then方法是具有返回值的,它返回的是一个新的promise实例,这个实例是由前一个promise的resolve函数或reject函数的返回值构建的,它的状态也是由前者传递的

let p1 = Promise.resolve()
let p2 = p1.then(value=>console.log('A'),reason=>console.log('Wrong'));		//A
let p3 = p2.then(value=>console.log('B'),reason=>console.log('Wrong'));		//B
setTimeout(()=>{
    console.log(p1);		//Promise {<fulfilled>: 'Yes'}
    console.log(p2);		//Promise {<fulfilled>: undefined}
    console.log(p3);		//Promise {<fulfilled>: undefined}
},1000)

我们也可以利用链式结构,返回不同的状态并向下传递

let p1 = new Promise((resolve,rejct)=>{
    resolve("Yes")
}).then(value=>{
    console.log(value);							//Yes
    return new Promise((resolve,reject)=>{
        reject("No")
    })
}).then(null,reason=>{
    console.log(reason);						//No
})

——catch方法

你可能会想到try/catch语句,而实际上这个catch是Promise原型上的方法即Promise.prototype.catch,该方法用于给promise添加拒绝处理程序.。事实上,catch方法就是语法糖,相当于.then(null,reason=>{})

let p = new Promsie((resolve,reject)=>{
    reject("Error");
}).catch(err=>console.log(err));				//Error

——finally方法

在promise中,finally语句无论调用的是resolve函数还是reject函数,皆会执行,但必须调用处理方法的其中之一。通常,finally语句被用来做删除加载动画,加载时触发动画,加载完毕无论加载成功或失败,该动画都会被删除。

let p1 = new Promise((ressolve,reject)=>{
    console.log("动画开始");					//动画开始
    resolve();
}).finally(()=>console.log("动画停止/"));		//动画停止

——all方法

all方法可以对多个promise包装成一个新的promise实例进行处理,如果参与处理的promise都是fulfilled状态,这个新的promise实例将会变为fulfilled状态,并返回包含每个promise处理结果的数组

let p1 = new Promise((resolve,reject)=>{
    resolve("第一个异步")
})
let p2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve("第二个异步")
    },1000)
})
let p = Promise.all([p1,p2]).then(value=>console.log(value));	//['YEAH', 'yeah']

如上例,如果p中包含的promise中有一个处于待定状态,则p也会跟着处于待定状态,直到p包含的promise的状态发生改变。

若其中有一个是rejected状态,则这个新的promise实例将会变为rejected状态。并将第一个promise的拒绝理由作为这个新promise实例的拒绝理由。

let p1 = Promise.reject('Wrong One');
let p2 = Promise.reject('Wrong Two');
let p = Promise.all([p1,p2]).then(null,reason=>console.log(reason));	//Wrong One

——allSettled方法

跟all方法一样,但返回的结果是一个包含所有传入的promise的状态和其结果组成的数组

let p1 = new Promise((resolve,reject)=>{
    resolve("第一个异步")
})
let p2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve("第二个异步")
    },1000)
})
let p = Promise.allSettled([p1,p2]).then(value=>console.log(value));
//[{status:"fulfilled",value:"第一个异步"},{status:"fulfilled",value:"第一个异步"}]

——race方法

race这个单词翻译过来就“赛跑”、”比速度“的意思,而这个方法正恰如其意。它也可以将多个promise包装成一个新的promise实例进行处理,然而,如果参与处理的promise中,哪个率先改变状态(无论是fulfilled还是rejected),哪个就会作为新promise实例的处理对象。

let p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        reject("出错了!");
    },1000)
})
let p2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve("俺是第一!")
    },500)
})
let p =                       Promise.race([p1,p2]).then(value=>console.log(value),reason=>console.log(reason));
//俺是第一

Ⅲ总结

本篇首先介绍了JavaScript的单线程特性,以及JavaScript之于浏览器的关系,随后又介绍了Event Loop的运作机制。在这过程中,主要需要理解的观念是同步、异步,另外又讲了传统处理异步任务的方法及所谓回调地狱的难题,这都为下面引入promise做了铺垫。

  • Promise分为三个状态,实例化时调用传入回调函数的参数resolve或reject进行落定,一旦落定就恒定不变,resolve会使状态转化为fulfilled,reject会使状态转化为rejected。promise的状态。
  • 一个promise中的resolve或reject可以传入一般值,也可以传入另外一个promise,如果传入后者的话,则另外传入的promise的状态会收到前一个promise的影响。它会等待前一个promise的状态发生改变,随后继承前面的状态。
  • promise的状态改变时会有相应的方法根据不同状态进行后续处理,这个方法就是——then,它接收两个参数,分别用于处理fulfilled状态和rejected状态。如果仅对一个状态进行处理,则另一个参数需要设为null。
  • 状态处理方法还有:
    • catch方法,对拒绝或运行中的错误进行处理;
    • finally方法,不管啥状态都会处理;
    • all方法,可对多个promise进行处理,当所有传入的promise为fulfilled时才会进行处理;
    • allSettled方法,同all方法,却比all方法返回的信息更全,前者返回每个promise处理结果的数组,而该方法则返回一个包含所有传入的promise的状态和其结果组成的数组;
    • race方法,同样可以传入多个promise,但谁跑得快就处理谁;

最后,我们结合本篇讲的内容,思考一下下面代码的运行结果

let fun;
let p = new Promise((resolve)=>{
    fun = function(){
        console.log('1');
        resolve();
        console.log('2');
    };
});
p.then(()=>console.log('4'));
fun();
console.log('3');

运行结果的输出顺序是:1 2 3 4

虽然,fun函数的执行看似实在then处理程序后面,但真正执行时,该程序作为异步任务被推入任务队列,因此最后才会被执行。

结语

这就是本篇所有内容了,下一篇将会复习深浅拷贝