平常在工作中,我们经常与异步打交道,无论是函数节流、防抖,异步请求,都是异步操作。那么我们会经常使用setTimeout,Promise,Async/Await这三个东西。那么我们是真的了解这些api和语法糖他们的原理以及知识吗?本篇文章将从尽可能的说明白个中的原理和知识。
JavaScript的运行机制本质上就是Event loop,这个知识点主要是要搞清楚宏任务与微任务之间的区别。这个知识点不在这里一一说明,想了解可以看看我之前的文章。
我们平常经常使用Promise来进行各种异步操作,无论是单独使用Promise,或者搭配Async/Await。但是我们要搞清楚里面的一些知识点,才能更好的去使用Promise这个api。如果你还没有用过Promise,那么请先去看文档,MDN。
深入了解Promise,我们要从以下几个方面去了解Promise。
Promise的运行机制
const p = new Promise(function(resolve, reject) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
Promise本身是同步的立即执行函数, 但是当我们调用resolve或者reject的时候,.then内的回调函数是异步执行,并且.then内的函数会被存放到微任务中,等主栈完成后,才会去运行微任务中的.then的回调函数。
输入结果:promise1 -> promise1 end -> promise2
Promise.all的使用
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});
Promise.all就是必须等待传入的Promise数组的所有Promise都执行完毕,才会触发then的api。
那么Promise.all我们应该在什么场景下使用呢?如果当前你的异步操作必须依赖另外几个异步操作,并且都需要这几个前置异步操作都要成功的情况下才进行下一步行为,那么就可以使用Promise.all了。
打个比方说,当前页面中,我们需要依赖几个不同的接口来完成当前页面中的渲染,那么我们就可以使用Promise.all来实现对这几个不同的接口都必须返回数据后,我们才开始渲染页面。
Promise.all的实现原理
我们来尝试自己实现一个Promise.all,来了解它的工作原理。
const promise1 = Promise.resolve( 3 );
const promise2 = 42;
const promise3 = new Promise( function ( resolve, reject ) {
setTimeout( resolve, 100, 'foo' );
} );
function myAll(promiseList) {
return new Promise((resolve, reject) => {
let count = 0;
const promiseCount = promiseList.length;
const resultList = Array(promiseCount);
promiseList.forEach((promise, key) => {
Promise.resolve(promise).then((data) => {
count += 1;
resultList[key] = data;
if ( count == promiseCount ) {
resolve(resultList);
}
}, (reason) => {
return reject(reason)
});
});
});
}
myAll( [promise1, promise2, promise3] ).then( function ( values ) {
console.log( values );
}, function ( err ) {
console.log( err );
} );
本质上Promise.all的原理就是将传入的数组全部执行,并且将所有传入的Promise的resolve结果保存在一个与之传入顺序对应的数组当中,并每次有Promise触发resolve检查是否已经是最后一个,当检查到最后一个时候,触发resolve将返回结果数组返回。
Promise.race的使用
Promise.race实际上就是一个变异版的Promise.all,Promise.all是必须等待所有传入的Promise执行完毕才会触发resolve,但是Promise.race不是,Promise.race是传入的Promise中,只要有一个执行完毕,那么将立即返回,其余的Promise的返回结果将会抛弃。
const promise1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise1, promise2]).then(function(value) {
console.log(value);
// Both resolve, but promise2 is faster
});
// expected output: "two"
Promise.race的实现原理
我们可以利用之前实现的Promise.all的实现方式,做一些修改。
const promise1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'two');
});
function myRace(promiseList) {
return new Promise((resolve, reject) => {
promiseList.forEach(promise => {
promise.then(resolve, reject)
})
});
}
myRace( [promise1, promise2] ).then( function ( values ) {
console.log( values );
}, function ( err ) {
console.log( err );
} );
实际上就是通过对传入的每个promise执行一个then,将myRace的resolve传递给每个promise的then中的回调函数,从而实现那个promise先执行完毕,就返回那个promise的运行结果。
Promise.finally的使用
Promise.finally代表的是一连串的promise和then的操作都执行完毕后,无论是否报错,都会执行的函数。
const p = new Promise( ( resolve, reject ) => {
console.info( 'starting...' );
setTimeout( () => {
resolve( 'success' );
}, 1000 );
} );
p.then( ( data ) => {
console.log( `%c resolve: ${data}`, 'color: green' )
} )
.catch( ( err ) => {
console.log( `%c catch: ${err}`, 'color: red' )
} )
.finally( () => {
console.info( 'finally: completed' )
} );
Promise.finally的实现原理
实现finally的原理,我们首先要清楚,finally后面,其实是可以继续带有.then的,而且无论是否触发catch,都会执行finally的。
其实实际上finally和then没有太大的区别,只是finally不会接收任何参数,但是可以return回一个promise,可以让后续继续执行then操作。
Promise.prototype.finally = function(callback) {
const constructor = this.constructor;
return this.then(
(data) => {
return constructor.resolve(callback()).then((callbackData) => {
return data;
// 如果扩展,可以将finally的回调函数返回的promise的resolve传递到之后的then中
// return callbackData
})
},
(err) => {
return constructor.resolve(callback()).then(() => {
throw err
})
}
)
}
实际上就是调用Promise的then,注册多一个then的函数,并且返回一个Promise对象,在Promise的执行体中执行finally的回调函数,最后通过将上一个then或者catch中resolve返回的值转入到一下个then中。
小结
通过这几个源码的实现原理,我们大概就知道了Promise中的这些api的运行原理,那么我们将可以更好的在不同场景下,合理利用Promise的特性来处理异步逻辑了。如果说对于Promise的实现原理有兴趣,我之后有时间会单独对Promise的实现原理做文章,这里先不细说Promise的内部实现原理。
首先我们要知道一些概念,async/await实际上是Generator封装的一套异步处理方案,实际上就是Generator的语法糖,而Generator又依赖一个Iterator(迭代器)。所以要搞清楚async,就要先搞清楚Iterator和Generator。
Iterator的思想来源于一种数据结构,单向链表。下面简单说一说单向链表是什么东西。
单向链表是一种基本的数据结构,其中包含着两个重要的参数,一个是当前节点的值,一个是当前节点的一下个节点的指向。
单向链表有以下优点:
缺点:
Iterator的思想也是借鉴了单向链表的设计,每个节点都有一个next函数,用于返回当前节点的信息,并且内部指针+1。next函数必须返回一个对象,对象包含value和done属性。
// 迭代生成器
const makeIterator = arr => {
let nextIndex = 0;
return {
next: () =>
nextIndex < arr.length
? { value: arr[nextIndex++], done: false }
: { value: undefined, done: true },
};
};
const it = makeIterator(['Hello', 'world']);
console.log(it.next()); // { value: "Hello", done: false }
console.log(it.next()); // { value: "world", done: false }
console.log(it.next()); // {value: undefined, done: true }
根据规范,每个对象如果要变成一个可迭代对象,那么必须拥有[Symbol.iterator]参数,Iterator 接口主要供for...of消费。
const array = [1,2];
console.log(array[Symbol.iterator])
for ( let item of array ) {
console.log(item);
}
const set = new Set([1,2]);
console.log(set[Symbol.iterator])
for ( let item of set ) {
console.log(item);
}
// 报错
const map = new Map({a:1, b:2});
console.log(map[Symbol.iterator])
for ( let item of map ) {
console.log(item);
}
// 报错
const object = {a: 1, b: 2};
console.log(object[Symbol.iterator])
for ( let item of object ) {
console.log(item);
}
默认Array和Set数据格式都内置了[Symbol.iterator]接口,但是Map和Object是没有的,所以调用for...of的时候将会报错。但是我们可以实现自定义的迭代器。
const object = {
data: ['hello', 'world'],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return {
value: this.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
}
}
};
console.log(object[Symbol.iterator])
for ( let item of object ) {
console.log(item);
}
有什么时候会调用Iterator迭代器呢?
大概说明了一下Iterator迭代器到底是一个怎样的东西。接下来开始学习一下Generator,以及Generator依赖Iterator迭代器做了什么?
上面已经了解Iterator迭代器的原理,那么其实Generator实际上就是生成迭代器的语法。具体语法就是声明一个function*的函数,例如:
function* gen() {
console.log('运行gen')
yield 1;
console.log('运行第一次')
yield 2;
yield 3;
}
const g = gen();
console.log(g.next());
// 运行gen
// { value: 1, done: false }
console.log(g.next());
// 运行第一次
// { value: 2, done: false }
console.log(g.next());
// { value: 3, done: false }
console.log(g.next());
// { value: undefined, done: true }
在gen函数中,首次调用并不会执行函数中的任何代码,每次执行next的时候,程序会运行至相应的yield就暂停等待第二次的next调用。
下面我用代码模拟使用Generator来实现异步。
function ajax1 () {
return new Promise( ( resolve ) => {
setTimeout( resolve, 500, 'ajax1' )
} );
}
function ajax2 () {
return new Promise( ( resolve ) => {
setTimeout( resolve, 500, 'ajax2' )
} );
}
function* gen () {
yield ajax1();
yield ajax2();
}
const g = gen();
const g1 = g.next();
g1.value
.then( ( data ) => {
console.log( data );
const g2 = g.next();
g2.value
.then( ( data2 ) => {
console.log( data2 );
} )
} );
从例子中可以看到,每次yield执行一个函数,返回的是Promise,所以每次调用next返回的value都是一个Promise对象。所以就可以实现类似async/await的执行方式。
但是有一个问题,同样的代码,如果使用async/await来实现,俾比较简单:
const data1 = await ajax1();
const data2 = await ajax2();
但是现在我们要每次触发next都需要对next的value手动调用then,这样非常麻烦,所以我们需要一个自动迭代器,帮我们自动完成迭代的过程。
function ajax1 () {
return new Promise( ( resolve ) => {
setTimeout( resolve, 500, 'ajax1' )
} );
}
function ajax2 () {
return new Promise( ( resolve ) => {
setTimeout( () => {resolve('ajax2')}, 500 )
} );
}
function run(gen) {
const g = gen();
function _next(data) {
const res = g.next(data);
if (res.done) return res.value;
res.value.then((data) => {
_next(data);
});
}
_next();
}
function* gen () {
const res1 = yield ajax1();
console.log(res1);
const res2 = yield ajax2();
console.log(res2);
}
run(gen);
到这里是否开始已经有一点像async/await的语法了。
async/await其实实际上就是Generator的语法糖,本质上就是使用Generator来实现的,我们可以看看对比
// Generator
function* gen () {
const res1 = yield ajax1();
console.log(res1);
const res2 = yield ajax2();
console.log(res2);
}
// async/await
async function gen2() {
const res1 = await ajax1();
console.log(res1);
const res2 = await ajax2();
console.log(res2);
}
async就等于function*,await就等于yield,而且使用async/await无需自己写手动迭代器,它会自动帮你完成。
async/await还有一些不一样的点,例如await如果调用的Promise,才会异步执行,否则将会同步执行,gen2是一个Promise对象,如果在gen2最后执行return,那么将会触发gen2的then。
async function gen2() {
const res1 = await ajax1();
console.log(res1);
const res2 = await ajax2();
console.log(res2);
return 'done'
}
gen2()
.then((data) => {
console.log(data); // done
})
JavaScript的异步主要分为setTimeout,Promise,aysnc/await这三个技术。setTimeout的异步操作更多是作为对一些渲染操作以及函数节流/防抖的时候进行使用,随着ES6的成熟,Promise和async/await越来越多使用,而async/await一般都是搭配Promise一起使用的,而Promise还可以解决回调地狱的问题。
async/await实际上是Generator的语法糖,让开发者更方便的进行异步处理,无需手动迭代,带来更好的开发体验。而Generator依赖了Iterator迭代器来实现迭代,Iterator的思想是利用单向链表的设计。