资讯专栏INFORMATION COLUMN

异步的JavaScript

tangr206 / 2859人阅读

摘要:如果在浏览器中线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。中也有可能会产生新的,会进入尾部,并在本次前执行。这就是所谓的,而把回调函数的嵌套逻辑替换成了符合正常人思维习惯的线性逻辑。

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
参考文章 && 拓展阅读

JavaScript Event Loop Explained

How to escape async/await hell

Tasks, microtasks, queues and schedules

requestAnimationFrame

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109131.html

相关文章

  • 浅析JavaScript异步

    摘要:回调函数,一般在同步情境下是最后执行的,而在异步情境下有可能不执行,因为事件没有被触发或者条件不满足。同步方式请求异步同步请求当请求开始发送时,浏览器事件线程通知主线程,让线程发送数据请求,主线程收到 一直以来都知道JavaScript是一门单线程语言,在笔试过程中不断的遇到一些输出结果的问题,考量的是对异步编程掌握情况。一般被问到异步的时候脑子里第一反应就是Ajax,setTimse...

    Tangpj 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • javascript异步与promise

    摘要:到这里,我已经发出了一个请求买汉堡,启动了一次交易。但是做汉堡需要时间,我不能马上得到这个汉堡,收银员给我一个收据来代替汉堡。到这里,收据就是一个承诺保证我最后能得到汉堡。 同期异步系列文章推荐谈一谈javascript异步javascript异步中的回调javascript异步之Promise.all()、Promise.race()、Promise.finally()javascr...

    rollback 评论0 收藏0
  • 夯实基础-JavaScript异步编程

    摘要:调用栈被清空,消息队列中并无任务,线程停止,事件循环结束。不确定的时间点请求返回,将设定好的回调函数放入消息队列。调用栈执行完毕执行消息队列任务。请求并发回调函数执行顺序无法确定。 异步编程 JavaScript中异步编程问题可以说是基础中的重点,也是比较难理解的地方。首先要弄懂的是什么叫异步? 我们的代码在执行的时候是从上到下按顺序执行,一段代码执行了之后才会执行下一段代码,这种方式...

    shadowbook 评论0 收藏0
  • 谈一谈javascript异步

    摘要:从今天开始研究一下的异步相关内容,感兴趣的请关注同期异步系列文章推荐异步中的回调异步与异步之异步之异步之和异步之一异步之二异步实战异步总结归档什么是异步我们知道的单线程的,这与它的用途有关。 从今天开始研究一下javascript的异步相关内容,感兴趣的请关注 同期异步系列文章推荐javascript异步中的回调javascript异步与promisejavascript异步之Prom...

    Sourcelink 评论0 收藏0
  • 异步

    摘要:在异步机制中,任务队列就是用来维护异步任务回调函数的队列。四对象对象是工作组提出的一种规范,目的是为异步编程提供统一接口。 异步 1.JavaScript单线程的理解 Javascript语言的执行环境是单线程(single thread)。所谓单线程,就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。 2.JavaScript单线...

    goji 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<