在实例中重学JavaScript事件循环

单线程的JS

众所周知js是一门单线程语言,即同一时间只能做一件事。为什么js是单线程的呢,主要与它的用途有关。

作为浏览器脚本语言,js的主要用途是和用户互动&操作DOM,我们并不想并行的操作DOM。如果不是单线程的话,我们一个线程在给DOM节点上添加内容,另一个线程却删除了这个节点,到底该以哪个为准呢?

所以为了避免复杂性,从一诞生,JavaScript 就是单线程。

事件循环(event loop)

JS是一门单线程语言,意味着代码要一行一行的执行。所有任务都要排队,前一个任务结束,才会执行后一个任务。

但平时大家开发时常用到的ajax,setTimeOut,promise之类的并没有阻塞进程。如果浏览器只有一个js引擎构成,遇到上面这些比较耗时的请求或操作时,浏览器就会阻塞住,这肯定不是我们想要的。

其实js单线程是指浏览器在解释和执行js代码时只有一个线程,即js引擎线程。但浏览器还包括一些其他的线程来处理这些异步的方法,比如Web APIs线程,GUI渲染线程等。

事件循环的处理流程:

JS线程依靠调用栈来处理执行js代码,当遇到一些异步的操作时,则将其移交给Web APIs,自己继续往下进行。
Web APIs线程则将收到的事件按一定的规则和顺序添加到任务队列里去。
JS线程处理完当前所有任务(即执行栈为空),则去检查任务队列里是否有等待被处理的事件,若有,则取出一个事件回调放入执行栈中执行。
然后不断循环第三步。

宏任务与微任务

任务队列又分为宏任务队列和微任务队列:

  • 宏任务队列(macrotask queue):存放的是setTimeout, setInterval, setImmediate, I/O, UI rendering等。
  • 微任务队列(microtask queue):存放的是Promises, Object.observe, MutationObserver,process.nextTick等。

所以我们细化一下事件循环的处理流程(浏览器环境):

JS线程依靠调用栈来处理执行js代码,当遇到一些异步的操作时,则将其移交给Web APIs,自己继续往下进行。
Web APIs线程则将收到的事件按一定的规则和顺序添加到任务队列里去。宏任务事件则添加到宏任务队列,微任务事件则添加到微任务队列。
JS线程处理完当前所有任务(即执行栈为空),会先去微任务队列检查是否有待处理的事件,若有,会将微任务队列里的所有事件一件件执行完直到微任务队列为空,再去宏任务队列取出最前面的一个事件执行,执行完这一个宏任务事件后再去检查微任务队列是否有事件待处理。
然后不断循环第三步。

实际需求中重学JavaScript事件循环

什么是JS事件循环?

在秋招的时候经常会被问到这个问题,但自己的理解仅限于以上,然后刷过几道输出值顺序的题目,没有过业务中的实际应用场景。后来拿到offer后就忘的一干二净了,直到毕业入职后开始写代码重新遇到了这才有了更深一步的理解。

背景
用户上传多张图片,前端拿到每张图片的url和宽高发送给后端。

解决
首先是拿到用户上传的文件,做一些校验和限制

// 调用系统弹框添加图片的方法
addFile(e) {
 let uploadFiles = e.target.files,self = this;

 self.getListData = []; // 要传给后端的对象数组

 self.testFiles(uploadFiles) // 对用户上传的文件做一些校验和限制

 self.loadAll(uploadiles) // 调用loadAll方法
},

然后让我们看一下loadAll,主要是遍历上传的这些图片文件,然后每一个图片文件都调用了loadImg

loadAll(files) {
 let promises = [],self = this

 // 遍历文件流
 files.forEach((file,i) => {
  // 创建对象,push到数组里
  (self.getListData).push({
   imageUrl: '',
  });

  let eachPromise = self.loadImg(file,i)
  // 存储当前promise对象
  promises.push(eachPromise)
 })

 Promise.all(promises).then(() => {
  //全部完成,向后端发送请求
 }).catch(err => {
  console.log(err)
 })
},

然后让我们看一下loadImg,这个方法返回一个Promise对象,主要是为了保证拿到图片的URL以及在img.onload里拿到图片的宽高,因为这两个事件都是异步事件。

实际上js主线程是不会等待这两个结果,就会继续往下执行的。但因为我们在img.onload里才会把Promise给resolve出去,而loadAll方法里用了一个Promise.all来等待所有promise都完成,这样就可以保证向后端发送请求时所有的图片的url和宽高都能拿到。

loadImg(file,i) {
 return new Promise(async (resolve,reject) => {
  let self = this
  // 调用公司wos服务,拿图片文件的url
  let successRes = await _fileUpload.uploadFile(item)
  if(successRes && successRes !== 'error'){
    self.getListData[i]['imageUrl'] = successRes.url
  }
  let img = new Image()
  img.src = successRes.url
  img.onload = () => {
   self.getListData[i]['width'] = img.width
   self.getListData[i]['height'] = img.height
   resolve()
  }
  img.onerror = (e) => {
   reject(e)
  }
 })
}

让我们想一下如果把loadImg里拿图片的url这个操作放到loadAll里呢?

loadAll(files) {
 let promises = [],self = this

 // 遍历文件流
 files.forEach(async(file,i) => {
  // 创建对象,push到数组里
  (self.getListData).push({
   imageUrl: '',
  });

  // 调用公司wos服务,拿图片文件的url
  let successRes = await _fileUpload.uploadFile(item)
  if(successRes && successRes !== 'error'){
    self.getListData[i]['imageUrl'] = successRes.url
  }

  let eachPromise = self.loadImg(file,i)
  // 存储当前promise对象
  promises.push(eachPromise)
 })

 Promise.all(promises).then(() => {
  //全部完成,向后端发送请求
 }).catch(err => {
  console.log(err)
 })
},

如果变成这种写法,因为js的主线程执行栈不会等待await返回结果,循环里await _fileUpload.uploadFile(item)这行代码后面的内容会被交给Web APIs然后跳出async函数。继续执行主线程,而现在Promise.all的参数是一个空数组,然后就直接发了请求。但现在并没有拿到图片的URL和宽高。

关键字await只能使async函数一直等待,执行栈当然不可能停下来等待的,await将其后面的内容包装成Promise交给Web APIs后,执行栈会跳出async函数继续执行,直到Promise执行完并返回结果。await只在async函数里面奏效。

总结

从上面这个需求的实现中,好像对事件循环的理解更深刻了!像Promise.then里和await后面的代码都会等待返回结果后再被放入对应事件的任务队列中等待执行,JS线程会继续向下执行调用栈。包括vue中的watch handler也是被先放入了任务队列里等待。

所以可知事件循环在实际工作中对写代码和优化代码都非常重要~如理解有误请在评论区多多指教。

以上就是在实例中重学JavaScript事件循环的详细内容,更多关于JavaScript 事件循环的资料请关注我们其它相关文章!

(0)

相关推荐

  • js重写alert事件(避免alert弹框标题出现网址)

    js代码: window.alert = function(msg, callback) { var div = document.createElement("div"); div.innerHTML = "<style type=\"text/css\">" + ".nbaMask { position: fixed; z-index: 1000; top: 0; right: 0; left: 0; bottom: 0

  • JavaScript代码模拟鼠标自动点击事件示例

    利用JavaScript代码可以帮助我们实现鼠标的自动点击事件.比如点击了按钮1以后,JavaScript代码会自动去点击下一个按钮,一直点击到按钮5才会停止(可根据实际情况进行相应的调整). 效果图如下所示: 按钮1被点击以后,通过JavaScript的代码自动点击按钮2,代码点击了按钮2之后又会去点击按钮3,以此类推,直到点击到按钮5为止. 首先先设置几个按钮,然后再设置一个盒子包裹着一些文字,方便我们在接下来js部分中更换里面的内容时,更加直观的看到它的效果. HTML代码部分: 通过cs

  • JavaScript事件循环及宏任务微任务原理解析

    首先看一段代码: 打印顺序是什么? 正确答案:script start, script end, promise1, promise2, setTimeout 其中涉及到事件循环(event loop),宏任务(macrotask),微任务(microtask) 一.事件循环 Event Loop 程序中设置两个线程:一个负责程序本身的运行,称为"主线程":另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为&quo

  • vue.js click点击事件获取当前元素对象的操作

    Vue.js可以传递$event对象 <body id="app"> <ul> <li v-on:click="say('hello!', $event)">点击当前行文本</li> <li>li2</li> <li>li3</li> </ul> <script> new Vue({ el: '#app', data: { message: 'He

  • JS addEventListener()和attachEvent()方法实现注册事件

    在 JavaScript 的 DOM 事件模型中,通过调用对象的 addEventListener() 方法注册事件.用法如下: element.addEventListener(String type, Function listener, boolean useCaptrue); 参数说明如下: type:注册事件的类型名.事件类型与事件属性不同,事件类型名没有 on 前缀.例如,对于事件属性 onclick 来说,所对应的事件类型为 click. listener:监听函数,即事件处理函数

  • 详解node.js 事件循环

    Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高. Node.js 几乎每一个 API 都是支持回调函数的. Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现. Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数. 事件驱动程序 Node.js 使用事件驱动模型,当web server接收到请

  • JavaScript事件概念详解(区分静态注册和动态注册)

    js中的事件 什么是事件?事件是电脑输入设备与页面进行交互的响应,我们称之为事件 事件类型 鼠标单击:例如单击button.选中checkbox和radio等元素:鼠标进入.悬浮或退出页面的某个热点:例如鼠标停在一个图片上方或者进入table的范围: 键盘按键:当按下按键或释放按键时: HTML事件:例如页面body被加载时:在表单中选取输入框或改变输入框中文本的内容:例如选中或修改了文本框中的内容: 突变事件:主要指文档底层元素发生改变时触发的事件,如DomSubtreeModified(DO

  • 浅谈javascript事件环微任务和宏任务队列原理

    JS 事件环 JS 程序的运行是离不开事件环机制的,这个机制保证在发生某些事情的时候我们有机会执行一个我们事先预定好的函数,事情发生的时候 JS 会将相应的函数入栈执行然后出栈,但是关于事件环我们还有一些未知的东西,例如,setTimeout 我们习惯称他为定时器,但是可能很多人没有意识到,这个东西和我们常用的一些事件没什么不同,只不过我们通常所说的事件大多需要用户触发,而 setTimeout 不用用户自己触发,而是指定时间之后触发:那么问题来了,如果我们将时间设置为 0 会发生什么?会立即执

  • 实例讲解JavaScript 计时事件

    JavaScript 计时事件 通过使用 JavaScript,我们有能力做到在一个设定的时间间隔之后来执行代码,而不是在函数被调用后立即执行.我们称之为计时事件. 在 JavaScritp 中使用计时事件是很容易的,两个关键方法是: setInterval() - 间隔指定的毫秒数不停地执行指定的代码. setTimeout() - 在指定的毫秒数后执行指定代码. 注意: setInterval() 和 setTimeout() 是 HTML DOM Window对象的两个方法. setInt

  • JS如何监听div的resize事件详解

    需求 开发过程中经常遇到的一个问题就是如何监听一个div的size变化. 比如我用canvas绘制了一个chart,当canvas的size发生变化的时候,需要重新绘制里面的内容,这个时候就需要监听resize事件做处理. window上虽然可以添加resize事件监听,但这并不能满足我们的需求,因为很多时候,div的size发生了变化,但是window的size并没有改变. 不过我们可以间接利用window的resize事件监听来实现对于某个div的resize事件监听,请看下面具体实现. 对

随机推荐