JS自己是一门单线程的语言,因此在执行一些须要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其余代码。若是在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而形成很差的用户体验。幸运的是JS语言自己和其运行的环境(浏览器,Node)都提供了一些解决方案让JS能够“异步”起来,在此梳理一下相关的知识点,若是你读完以后有所收获,那更是极好的。javascript
JS中每一个函数都伴有一个自身的做用域(execution context),这个做用域包含函数的一些信息(eg.参数,局部变量等),在函数被调用时,函数的做用域对象被推入执行栈(execution context stack),执行完毕后出栈。当执行一些异步任务时,JS仅调用相应的API并不去等待任务结果而是继续执行后续代码,这些异步任务被浏览器或者Node交由其余线程执行(eg.定时器线程、http请求线程、DOM事件线程等),完成以后这些异步任务的回调函数会被推入相应的队列中,直到执行栈为空时,这些回调函数才会被依次执行。html
举个例子:前端
function main() {
console.log('A)
setTimeout(function display() {
console.log('B')
}, 0)
console.log('C')
}
main()
复制代码
以上代码在Event Loop中的执行过程以下:java
相似于setTimeout这样的任务还有:setInterval, setImmediate, 响应用户操做的事件(eg. click, input等), 响应网络请求(eg. ajax的onload,image的onload等),数据库操做等等。这些操做有一个统一的名字:task,因此上图中的message queue实际上是task queue,由于还存在一些像:Promise,process.nextTick, MutationObserver之类的任务,这些任务叫作microtask,microtask会在代码执行过程当中被推入microtask queue而不是task queue,microtask queue中的任务一样也须要等待执行栈为空时依次执行。ajax
一个task中可能会产生microtask和新的task,其中产生的microtask会在本次task结束后,即执行栈为空时执行,而新的task则会在render以后执行。microtask中也有可能会产生新的microtask,会进入microtask queue尾部,并在本次render前执行。数据库
这样的流程是有它存在缘由的,这里仅仅谈下我我的的理解,若有错误,还请指出: 浏览器中除了JS引擎线程,还存在GUI渲染线程,用以解析HTML, CSS, 构建DOM树等工做,然而这两个线程是互斥的,只有在JS引擎线程空闲时,GUI渲染线程才有可能执行。在两个task之间,JS引擎空闲,此时若是GUI渲染队列不为空,浏览器就会切换至GUI渲染线程进行render工做。而microtask会在render以前执行,旨在以相似同步的方式(尽量快地)执行异步任务,因此microtask执行时间过长就会阻塞页面的渲染。promise
上文提到setTimeout,setInterval都属于task,因此即使设置间隔为0:浏览器
setTimeout(function display() {
console.log('B')
}, 0)
复制代码
回调也会异步执行。服务器
setTimeout,setInterval常被用于编写JS动画,好比:网络
// setInterval
function draw() {
// ...some draw code
}
var intervalTimer = setInterval(draw, 500)
// setTimeout
var timeoutTimer = null
function move() {
// ...some move code
timeoutTimer = setTimeout(move, 500)
}
move()
复制代码
这实际上是存在必定的问题的:
从event loop的角度分析:setInterval的两次回调之间的间隔是不肯定的,取决于回调中的代码的执行时间;
从性能的角度分析:不管是setInterval仍是setTimeout都“没法感知浏览器当前的工做状态”,好比当前页面为隐藏tab,或者设置动画的元素不在当前viewport,setInterval & setTimeout仍会照常执行,实际是没有必要的,虽然某些浏览器像Chrome会优化这种状况,但不能保证全部的浏览器都会有优化措施。再好比多个元素同时执行不一样的动画,可能会形成没必要要的重绘,其实页面只须要重绘一次便可。
在这种背景下,Mozilla提出了requestAnimationFrame,后被Webkit优化并采用,requestAnimationFrame为编写JS动画提供了原生API。
function draw() {
// ...some draw code
requestAnimationFrame(draw)
}
draw()
复制代码
requestAnimationFrame为JS动画作了一些优化:
固然requestAnimationFrame存在必定的兼容性问题,具体可参考 can i use。
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
复制代码
假设最初学JS时我看到的是上面的代码,我必定不会想写前端。这就是所谓的“callback hell”,而Promise把回调函数的嵌套逻辑替换成了符合正常人思惟习惯的线性逻辑。
function fetchSomething() {
return new Promise(function(resolved) {
if (success) {
resolved(res);
}
});
}
fetchSomething().then(function(res) {
console.log(res);
return fetchSomething();
}).then(function(res) {
console.log('duplicate res');
return 'done';
}).then(function(tip) {
console.log(tip);
})
复制代码
async await是ES2017引入的两个关键字,旨在让开发者更方便地编写异步代码,但是每每能看到相似这样的代码:
async function orderFood() {
const pizzaData = await getPizzaData() // async call
const drinkData = await getDrinkData() // async call
const chosenPizza = choosePizza() // sync call
const chosenDrink = chooseDrink() // sync call
await addPizzaToCart(chosenPizza) // async call
await addDrinkToCart(chosenDrink) // async call
orderItems() // async call
}
复制代码
Promise的引入让咱们脱离了“callback hell”,但是对async函数的错误用法又让咱们陷入了“async hell”。
这里其实getPizzaData和getDrinkData是没有关联的,而await关键字使得必须在getPizzaData resolve以后才能执行getDrinkData的动做,这显然是冗余的,包括addPizzaToCart和addDrinkToCart也是同样,影响了系统的性能。因此在写async函数时,应该清楚哪些代码是相互依赖的,把这些代码单独抽成async函数,另外Promise在声明时就已经执行,提早执行这些抽出来的async函数,再await其结果就能避免“async hell”,或者也能够用Promise.all():
async function selectPizza() {
const pizzaData = await getPizzaData() // async call
const chosenPizza = choosePizza() // sync call
await addPizzaToCart(chosenPizza) // async call
}
async function selectDrink() {
const drinkData = await getDrinkData() // async call
const chosenDrink = chooseDrink() // sync call
await addDrinkToCart(chosenDrink) // async call
}
// return promise early
async function orderFood() {
const pizzaPromise = selectPizza()
const drinkPromise = selectDrink()
await pizzaPromise
await drinkPromise
orderItems() // async call
}
// or promise.all()
Promise.all([selectPizza(), selectDrink()]).then(orderItems) // async call
复制代码