异步
两个或多个事件不同时存在或发生,区别于同步,同步是顺序执行从上到下,而异步不需要顺序执行,且不依赖于前面的事情是否已完成。
举例:
//异步执行
let count = 1;
let timer = setTimeout(function () {
count++;
console.log('in', count)
}, 1000)
console.log('out', count)
执行代码会发现,out必in先输出。
再举个例子:
//循环执行
let count = 1
let timer = setInterval(function () {
count++
console.log('in', count)
}, 1000)
setTimeout(function () {
clearInterval(timer)
console.log('clear')
}, 5000)
上面代码,每秒执行count++,且输出一次count的值,5秒后清空定时器。
在异步中最常见的方法setTimeout和setInterval
在第一个例子中,我们发现,代码并不是从上往下执行的,setTimeout方法打印的内容,是在out输出后打印的,这是为什么呢?
这是因为定时器方法被放到一个看不见的队列中,等待执行指令。
进程 & 线程
概念 & 区别
在计算机组成原理中,进程是CPU资源分配的最小单位,线程是CPU调度的最小单位。
- 进程内部有独立资源,进程与进程之间的资源是互不干扰的。
- 线程是共用一个资源
可以用下面这个图理解:
思考:浏览器chrome新开一个窗口,是进程还是线程?
答案:从上面我们了解到,进程与进程之间的资源是互不干扰的,而线程会共用一个资源。
假如我们在页面上执行了死循环,页面卡死。此时我们打开一个新窗口,还可以继续执行。如果窗口是线程,共用资源,那我们页面卡死之后,我们每打开一个页面都会卡死,因为资源是共用的,占用内存过多,导致所有页面都会卡死,这就很可怕。
所以窗口是进程,每个页面之间资源互不干扰,哪怕我们其中一个页面卡死,也可以打开新窗口,把卡死的窗口关掉。
浏览器原理
刚刚提到浏览器窗口是一个进程,那么进程下面就会有多个线程。在浏览器中,有以下这些线程,支持页面的渲染:
1、GUI渲染线程
GUI渲染线程用来解析HTML、CSS、构建DOM树。
注意:与JS引擎线程互斥,当执行JS的时候GUI渲染会被挂起
2、JS引擎线程
JS引擎线程用来处理JS,解析执行脚本。会阻塞GUI线程。
为什么GUI渲染线程和JS引擎线程互斥呢?是为了防止先后顺序错乱,导致页面渲染不一致。
3、定时器线程
用来处理和执行异步定时器,接收JS引擎分配的定时任务并执行,处理完成后交给事件触发线程。
4、异步HTTP请求线程
异步执行网络操作,接收JS引擎分配的网络请求任务并执行,处理完成后交给事件触发线程。
5、事件触发线程
接收所有的时间,把接收到的事件按顺序排序,并交给JS引擎线程
总结:GUI渲染线程和JS引擎线程互斥,为了防止先后顺序错乱。当JS引擎线程执行到定时器任务时,会把任务分配给定时器线程。当执行到网络请求会把任务分配给异步HTTP请求线程。当定时器线程、异步HTTP请求线程处理完后,会交给事件触发线程。事件触发线程会把接收到的定时器任务、异步HTTP请求任务进行排队,然后给到JS引擎执行。
事件循环机制
执行栈中存放同步任务,事件队列中存放异步任务(把线程处理好的异步任务放到队列中等待执行)
当执行栈中的任务执行完后,会到事件队列中查看是否有需要执行的任务。事件队列任务执行完成后,又会回到执行栈继续执行。这就是事件循环机制。
PS:如果执行栈中任务没执行完,需要等待执行栈中的任务执行完毕,才会去执行事件队列中的内容,导致定时任务执行时间并不准确,最小执行时间为4ms。
事件队列中又分为宏任务和微任务
- 宏任务:script、setTimeout、setInterval、I/O
- 微任务:promise、defineProperty、proxy
PS:微任务紧迫性更高。微任务执行完后,或者没有微任务,才会去执行下一个宏任务。
总结:
JS单线程 => 同步 + 异步 => 执行栈 + 任务队列(任务优先级)
举例:
setTimeout(() =>{
console.log('Timeout')
}, 0)
new Promise(resolve => {
console.log('new Promise')
resolve();
}).then(() =>{
console.log('promise then')
}).then(() =>{
console.log('promise then then')
})
console.log('hi')
可以暂停思考一下打印顺序。
new Promise中的内容属于同步,then执行的回调才是异步的,因此new Promise是第一个执行的任务,然后是最后一行hi是同步任务。then中的内容属于Promise宏任务中的微任务,顺序执行,因此,微任务1和2执行。最后执行setTimeout的宏任务。
关于Promise我们之前在ES6语法一文中已经做了初步的介绍。
在异步模块中,我们对Promise做了基本介绍。这里就不再赘述了。
这里主要是对Promise的规范以及应用的展开。
Promise的整体语法结构
这部分看不懂的,可以看ES6语法文章中,对Promise用法的拆解,这里是作为一个整体角度,把Promise中可以使用的方法以及回调函数,做了一个概述。
new Promise((resolve, reject)=>{
// 这里执行异步操作,然后将异步操作的结果,以及参数value/reason,通过resolve或者reject传入then的回调函数中
resolve(value)
// reject(reason)
}).then(res => {
//此处res就是上面resolve传入的value,在then中可以返回一个新的promise对象用来处理下一次异步操作
//此处可以对res进行处理
return new Promise((resolve, reject)=>{
//将异步操作的结果放入resolve/reject,传入then
resolve(value)
// reject(reason)
})
}).then(res => {
//如果不用进行异步操作,只是对数据进行简单处理,可以使用这种方式进行下一次then
return Promise.resolve(res)
}).then(res => {
//第三个promise的处理结果
}, err => {
//此处err是reject传入的reason
//其实then中可以传入两个回调函数参数,第一个参数是promise中使用的resolve后进入成功回调函数
//第二个参数是promise中使用reject后进入的失败(或者说是错误)回调函数,在then中失败回调函数可以忽略,然后在最后通过下面的catch方法接收失败的结果
}).catch(err => {
//此处err是reject传入的reason
//此处可对err信息进行一些打印操作
})
- Promise.all(iterable): 等待所有 Promise 完成,如果所有 Promise 都成功,则返回一个包含所有结果的数组;如果有任何一个 Promise 失败,则返回第一个失败的 Promise 的原因。
- Promise.race(iterable):等待第一个完成的 Promise,并返回其结果或原因。
当需要多个异步操作处理完成,才能对数据进行处理可使用promise.all方法。里面传入一个promise数组,then中的result是一个数组。举例:
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({name: 'why', age: 18})
}, 2000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({name: 'kobe', age: 19})
}, 1000)
})
]).then(results => {
console.log(results);
})
总结:then中的第二个失败回调,和catch方法一般是使用其中一个就可以了。如果多个回调,且每个回调需要处理不同的失败结果,可以使用then中的第二个回调。catch方法用来处理异步操作中其他不合理的代码逻辑。(catch可以处理的错误更多,then中第二个回调是专门用来处理回调失败的结果)
promise/A+规范
这个规范大家有兴趣可以看看:Promises/A+中文网
术语
- promise是一个有then方法的对象或者函数,行为遵循本规范
- thenable是一个有then方法的对象或者函数
- value是promise状态成功的值,也就是resolve的参数,包括各种数据类型,也包括undefined/thenable或者promise
- reason是promise状态失败时的值,也就是reject的参数,表示拒绝的原因
- exception是一个使用throw抛出的异常值
规范
promise有三种状态:pending、fulfilled、rejected
pending
- 初始化的状态,可改变
- 一个promise在resolve或者reject之前都处于这个状态
- 可以通过resolve改变promise状态为fulfilled
- 可以通过reject改变promise状态为rejected
fulfilled
- 最终态,不可变
- 一个promise被resolve后会变成这个状态
- 必须拥有一个value值
rejected
- 最终态,不可变
- 一个promise被reject后会变成这个状态
- 必须拥有一个reason值
总结:
pending → resolve(value) → fulfilled
pending → reject(reason) → rejected
then
Promise中应该提供一个then方法,用来访问最终的结果,无论是value还是reason。
Promise.then(onFulfilled, onRejected)
PS:一般以on开头的都是回调函数,比如说onClick、onChange
1、参数要求
(1)onFulfilled必须是函数类型,如果不是函数,应该被忽略
(2)onRejected必须是函数类型,如果不是函数,应该被忽略
说明then中两个参数都必须是函数,而把函数当做参数传递的,就叫回调函数。
2、onFulfilled特性
(1)在promise变成fulfilled时,应该调用onFulfilled,参数是value
(2)在promise变成fulfilled之前,不应该被调用【状态为fulfilled再调用then的第一个回调函数】
(3)只能被调用一次【所以在实现的时候需要一个变量来限制执行次数】
3、onRejected特性
(1)在promise变成rejected时,应该调用onRejected,参数是reason
(2)在promise变成rejected之前,不应该被调用【状态为rejected调用then的第二个回调函数】
(3)只能被调用一次【所以在实现的时候需要一个变量来限制执行次数】
4、onFulfilled和onRejected应该是微任务
这里用queueMicrotask来实现微任务的调用
5、then方法可以被调用多次
(1)promise状态变成fulfilled后,所有的onFulfilled回调都需要按照then的顺序执行,也就是按照注册顺序执行【所以在实现的时候需要一个数组来存放多个onFulfilled的回调】
(2)promise状态变成rejected后,所有的onRejected回调都需要按照then的顺序执行,也就是按照注册顺序执行【所以在实现的时候需要一个数组来存放多个onRejected的回调】
6、返回值
then应该返回一个promise
promise2 = promise1.then(onFulfilled, onRejected)
(1)onFullfiled或onRejected执行的结果为x,调用resolvePromise
(2)如果onFulfilled或者onRejected执行时抛出异常e,promise2需要被reject
(3)如果onFulfilled不是一个函数,promise2以promise1的value触发fulfilled
(4)如果onRejected不是一个函数,promise2以promise1的reason触发rejected
7、resolvePromise
resolvePromise(promise2, x, resolve, reject)
(1)如果promise2和x相等,那么reject TypeError
(2)如果x是一个promise
- 如果x是pending状态,那么promise必须要在pending,直到x变成fulfilled or rejected
- 如果x被fulfilled,fulfill promise with the same value
- 如果x被rejected,reject promise with the same reason
(3)如果x是一个object或者是一个function
let then = x.then
- 如果x.then这步出错,那么reject promise with e as the reason.【抛出异常】
- 如果then是一个函数,then.call(x, resolvePromiseFn, rejectPromise)
resolvePromiseFn的入参是y,指向resolvePromise(promise2, y, resolve, reject);
rejectPromise的入参是r,reject promise with r.
- 如果resolvePromise和rejectPromise都调用了,那么第一个调用优先,后面的调用忽略。
- 如果调用then抛出异常e,reject promise with e as the reason.
- 如果resolvePromise或rejectPromise已经被调用,那么忽略
- 如果then不是一个function,fulfill promise with x
实现一个Promise
基本步骤
先来看看Promise的基础版完整写法
new Promise((resolve, reject) => {
//Promise大多用来处理异步操作,这里使用setTimeout模拟一个异步操作
setTimeout(() => {
resolve(value)
// reject(reason)
}, 1000)
}).then(value => {
//第一个then
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(value)
// reject(reason)
}, 1000)
})
}, reason => {
}).then(value => {
//第二个then
console.log(value)
}, reason => {
}).catch(reason => {})
从上面代码看出,一个完整的Promise链式调用大概可以分为以上形式,由此可以看出Promise中至少有resolve、reject、then、catch等函数,还有创建Promise实例的构造函数。这条链式调用的过程是:
- new Promise:调用Promise的构造函数,创建Promise实例,然后执行传入的(resolve, reject) => {}回调函数,执行回调里面的同步操作,setTimeout里面异步操作需要后面执行
- 通过1中的Promise实例调用then方法,then方法会返回一个Promise实例(不管你有没有与上面代码那样返回一个Promise),该方法需要传入成功和失败的回调函数,如果传入的不是函数,Promise会另外处理
- 整条链上的then是同步执行的,但传入then的成功回调和失败回调,只会在前一个resolve/reject执行之后才能执行(resolve和reject每次只能执行一个),如果执行then的时候前一个resolve/reject没有执行,其成功和失败的回调函数会先存起来,等到resolve/reject之后才会执行
实现过程
1、采用class实现Promise,首先定义三个常量用来表示Promise的状态(pending、fulfilled、rejected),然后实现一下构造函数,value和reason首先设置为null。
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MPromise {
constructor() {
//初始状态为pending
this.status = PENDING;
this.value = null;
this.reason = null;
}
}
2、接下来实现一下Promise中的resolve和reject方法,当我们调用resolve方法需要将Promise的状态改为fulfilled(成功),当调用reject时需要将Promise的状态改为rejected(失败)
resolve(value) {
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
}
}
reject(reason) {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
}
}
3、当我们new Promise((resolve, reject) => {})的时候我们是有传入一个回调函数的,所以我们的构造函数需要接收这个回调函数并执行这个回调函数,所以构造函数需要改为如下:
constructor(fn) {
//初始状态为pending
this.status = PENDING;
this.value = null;
this.reason = null;
try {
//在此执行传入的回调函数,该回调函数的参数就是我们先前在类中定义的改变Promise状态的resolve和reject函数
fn(this.resolve.bind(this), this.reject.bind(this));
}catch (e) {
this.reject(e)
}
}
4、所以目前实现Promise的代码应该是这样
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MPromise {
constructor(fn) {
// 初始状态为pending
this.status = PENDING;
this.value = null;
this.reason = null;
try {
// 在此执行传入的回调函数,该回调函数的参数就是我们先前我们在类中定义的改变Promise状态的resolve和reject函数
fn(this.resolve.bind(this), this.reject.bind(this));
} catch (e) {
this.reject(e);
}
}
resolve(value) {
// 当Promse为fulfilled和rejected状态时状态不再可变,只有pending状态时才能改变状态
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
}
}
reject(reason) {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
}
}
}
5、接下来需要实现Promise的另一个方法then,then((value) => {}, (reason) => {})中需要传入当Promise变为fulfilled状态或者rejected状态之后的成功回调函数和失败回调函数,成功回调函数将在构造函数中调用resolve(value)的时候执行,失败回调函数将在构造函数中调用reject(value)的时候执行。
//储存成功和失败回调函数的数组
FULFILLED_CALLBACK_LIST = [];
REJECTED_CALLBACK_LIST = [];
then(onFulfilled, onRejected) {
//当传入的onFulfilled和onRejected不是函数的时候,我们需要将其转换成函数
const realOnFulfilled = this.isFunction(onFulfilled) ? onFulfilled : value => {
return value;
}
const realOnRejected = this.isFunction(onRejected) ? onRejected : reason => {
return reason;
}
//then方法默认会返回一个Promise实例,使得我们能够一直执行链式调用
const promise2 = new MPromise((resolve, reject) => {
switch (this.status) {
//当状态变为fulfilled表明已经执行resolve,this.value就是resolve(value)中传入的值
case FULFILLED: {
realOnFulfilled(this.value)
break;
}
//当状态变为rejected表明已经执行reject,this.reason就是reject(reason)中传入的值
case REJECTED: {
realOnRejected(this.reason)
break;
}
//当状态变为pending还未执行resolve或reject,此时暂时用数组将成功和失败的回调方法存储起来,所以需要在前面分别定义一个存储成功和失败的数组
case PENDING: {
this.FULFILLED_CALLBACK_LIST.push(realOnFulfilled)
this.REJECTED_CALLBACK_LIST.push(realOnRejected)
}
}
})
return promise2
}
6、现在有一个问题,如果我们将成功和失败的回调函数存储在数组中,我们什么时候来调用这个回调呢?肯定是要等到Promise的状态改变了才能调用这个储存在数组中的回调函数,这时候我们就需要监听Promise状态的编号,我们在此可以用到ES6的getter和setter,注意getter和setter必须同时存在。(可以在ES6语法的数组和对象的扩展中查看)
//此处使用另一个变量来存储状态是为了防止套娃,每当我们this.status的时候都会调用get status(),假设get status方法里面return this.status的话,就会陷入无限的死循环
_status = PENDING;
get status() {
return this._status;
}
set status(newStatus) {
this._status = newStatus;
switch (newStatus) {
//成功取出成功回调函数执行,this.value是执行了resolve之后的value
case FULFILLED: {
this.FULFILLED_CALLBACK_LIST.forEach(callback => {
callback(this.value);
});
break;
}
//失败取出失败回调函数执行,this.reason是执行了reject之后的reason
case REJECTED: {
this.REJECTED_CALLBACK_LIST.forEach(callback => {
callback(this.reason);
});
break;
}
}
}
7、前面说到每个then都会返回一个promise实例,使得我们可以进行如下链式使用then,但现在是有一个问题。第一个then中成功回调函数会执行,这是因为我们最开始创建Promise执行了resolve,但是第二个then的成功和失败回调是不会执行的,因为并没有执行对应的resolve和reject。
这里我们要搞清楚,then返回的Promise实例和then成功回调函数返回的Promise实例并不是同一个对象,在then的成功回调函数中调用resolve或者reject,我们给这个方法取名resolvePromise。
8、需要对then进行修改一下,then的修改其实只有两处,就是switch中成功和失败状态那里,成功和失败的返回值用一个变量x接收,然后然后调用resolvePromise方法,该方法传入四个参数,分别是then将要返回的promise实例、x、promise实例的resolve和reject。
//储存成功和失败回调函数的数组
FULFILLED_CALLBACK_LIST = [];
REJECTED_CALLBACK_LIST = [];
then(onFulfilled, onRejected) {
//当传入的onFulfilled和onRejected不是函数的时候,我们需要将其转换成函数
const realOnFulfilled = this.isFunction(onFulfilled) ? onFulfilled : value => {
return value;
}
const realOnRejected = this.isFunction(onRejected) ? onRejected : reason => {
return reason;
}
//then方法默认会返回一个Promise实例,使得我们在能够一直执行链式调用
const promise2 = new MPromise((resolve, reject) => {
switch (this.status) {
//当状态变为fulfilled表明已经执行resolve,this.value就是resolve(value)中传入的值
case FULFILLED: {
const x = realOnFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
break;
}
//当状态变为rejected表明已经执行reject,this.reason就是reject(reason)中传入的值
case REJECTED: {
const x = realOnRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
break;
}
//当状态变为pending还未执行resolve或reject,此时暂时用数组将成功和失败的回调方法存储起来,所以需要在前面分别定义一个存储成功和失败的数组
case PENDING: {
this.FULFILLED_CALLBACK_LIST.push(realOnFulfilled)
this.REJECTED_CALLBACK_LIST.push(realOnRejected)
}
}
})
return promise2
}
9、resolvePromise的实现,对于成功/失败回调函数的返回值x可能有很多种情况,可能返回一个Promise,也可能返回一个对象、一个函数、设置不返回任何值,在resolvePromise中我们需要考虑到这些情况并针对不同情况作出处理
resolvePromise(promise2, x, resolve, reject) {
//如果newPromise和x指向同一对象,以TypeError为因,拒绝执行newPromise
//这是为了防止死循环
if(promise2 === x) {
return reject(
new TypeError("The promise and the return value are the same")
);
}
if(x instanceof MPromise) {
//如果x为promise,则使newPromise接收x的状态
//也就是继续执行x,如果执行的时候拿到一个y,还要继续解析y
//之前规范中提到成功和失败的回调函数应该是一个微任务,所以这里使用queueMicrotask进行包裹,先前的then函数中的成功和失败回调函数后续也会进行修改
queueMicrotask(() => {
x.then(y => {
this.resolvePromise(promise2, y, resolve, reject);
}, reject)
});
} else if (typeof x === "object" || this.isFunction(x)) {
//如果x为对象或者函数,可能对象是第三方的promise
if(x === null) {
//null也会被判断为对象
return resolve(x);
}
let then = null;
try {
//把x.then赋值给then
then = x.then;
}catch (error) {
//如果取x.then的值时抛出错误e,则以e为因,拒绝promise
return reject(error);
}
//如果then是函数
if(this.isFunction(then)) {
let called = false;
//将x作为函数的作用域this调用
//传递两个回调函数作为参数,第一个参数叫做resolvePromise,第二个参数叫做rejectPromise
try {
then.call(
x,
//如果resolvePromise以值y为参数被调用,则运行resolvePromise //此处y是上一个resolve传过来的值
y => {
//需要有一个变量called来保证只调用一次。 //为了防止第三方Promise同时调用resolve和reject
if(called) return;
called = true;
reject(r);
}
);
} catch (error) {
//如果调用then方法抛出了异常e
if(called) return;
//否则以e为因拒绝promise
reject(error);
}
} else {
// 如果then不是函数,以x为参数执行promise
resolve(x);
}
} else {
//如果x不为对象或者函数,以x为参数执行promise
resolve(x);
}
}
10、promise的使用中,我们除了在then中进行失败的处理之外,promise还可以通过catch方法对失败进行处理,其原因还是在调用then。Promise除了在创建Promise实例的回调函数中使用resolve()/reject()之外,还可以直接通过对象直接使用,如Promise.resolve()/Promise.reject(),我们接下来实现一下这几个功能。
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
if(value instanceof MPromise) {
return value;
}
return new MPromise(resolve => {
resolve(value)
});
}
static reject(reason) {
return new MPromise((resolve, reject) => {
reject(reason)
});
}
Promise竞速的写法:
static race(promiseList) {
return new MPromise((resolve, reject) => {
const length = promiseList.length;
if(length === 0) {
return resolve();
} else {
for(let i = 0; i < length; i++) {
MPromise.resolve(promiseList[i]).then(
value => {
return resolve(value);
},
reason => {
return reject(reason);
}
);
}
}
});
}