异步的JavaScript
栏目: JavaScript · 发布时间: 5年前
内容简介:JS本身是一门单线程的语言,所以在执行一些需要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其他代码。如果在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。幸运的是JS语言本身和其运行的环境(浏览器,Node)都提供了一些解决方案让JS可以“异步”起来,在此梳理一下相关的知识点,如果你读完之后有所收获,那更是极好的。JS中每个函数都伴有一个自身的作用域(execution context),这个作用域包含函数的一些信息(eg.参数,局部变量等),在函数被调用时,函数的作用
JS本身是一门单线程的语言,所以在执行一些需要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其他代码。如果在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。幸运的是JS语言本身和其运行的环境(浏览器,Node)都提供了一些解决方案让JS可以“异步”起来,在此梳理一下相关的知识点,如果你读完之后有所收获,那更是极好的。
Event Loop
JS中每个函数都伴有一个自身的作用域(execution context),这个作用域包含函数的一些信息(eg.参数,局部变量等),在函数被调用时,函数的作用域对象被推入执行栈(execution context stack),执行完毕后出栈。当执行一些异步任务时,JS仅调用相应的API并不去等待任务结果而是继续执行后续代码,这些异步任务被浏览器或者Node交由其他线程执行(eg.定时器线程、http请求线程、DOM事件线程等),完成之后这些异步任务的回调函数会被推入相应的队列中, 直到执行栈为空时,这些回调函数才会被依次执行 。
举个例子:
function main() { console.log('A) setTimeout(function display() { console.log('B') }, 0) console.log('C') } main()
以上代码在Event Loop中的执行过程如下:
类似于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中的任务同样也需要等待执行栈为空时依次执行。
一个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执行时间过长就会阻塞页面的渲染。
setTimeout、setInterval、requestAnimationFrame
上文提到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动画做了一些优化:
- 大多数屏幕的最高帧率是60fps,requestAnimationFrame默认会尽可能地达到这一帧率
- 元素不在当前viewport时,requestAnimationFrame会极大地限制动画的帧率以节约系统资源
- 使用requestAnimationFrame定义多个同时段的动画,页面只会产生一次重绘。
当然requestAnimationFrame存在一定的兼容性问题,具体可参考 can i use。
Promise
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
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
参考文章 && 拓展阅读
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- SpringBoot | :异步开发之异步调用
- 改进异步封装:处理带返回值的异步调用
- 异步发展流程 —— Generators + co 让异步更优雅
- 文件系统与异步操作——异步IO那些破事
- js异步从入门到放弃(四)- Generator 封装异步任务
- netty的Future异步回调难理解?手写个带回调异步框架就懂了
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
游戏编程算法与技巧
【美】Sanjay Madhav / 刘瀚阳 / 电子工业出版社 / 2016-10 / 89
《游戏编程算法与技巧》介绍了大量今天在游戏行业中用到的算法与技术。《游戏编程算法与技巧》是为广大熟悉面向对象编程以及基础数据结构的游戏开发者所设计的。作者采用了一种独立于平台框架的方法来展示开发,包括2D 和3D 图形学、物理、人工智能、摄像机等多个方面的技术。《游戏编程算法与技巧》中内容几乎兼容所有游戏,无论这些游戏采用何种风格、开发语言和框架。 《游戏编程算法与技巧》的每个概念都是用C#......一起来看看 《游戏编程算法与技巧》 这本书的介绍吧!