在实例中重学JavaScript事件循环


Posted in Javascript onDecember 03, 2020

单线程的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 事件循环的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
jquery 双色表格实现代码
Dec 08 Javascript
如何让页面在打开时自动刷新一次让图片全部显示
Dec 17 Javascript
解决css和js的{}与smarty定界符冲突问题的两种方法
Sep 10 Javascript
javascript函数作用域学习示例(js作用域)
Jan 13 Javascript
jQuery子属性过滤选择器用法分析
Feb 10 Javascript
vue.js如何更改默认端口号8080为指定端口的方法
Jul 14 Javascript
javaScript中"=="和"==="的区别详解
Mar 16 Javascript
妙用缓存调用链实现JS方法的重载
Apr 30 Javascript
vue微信分享出来的链接点开是首页问题的解决方法
Nov 28 Javascript
微信公众平台获取access_token的方法步骤
Mar 29 Javascript
使用Vue实现移动端左滑删除效果附源码
May 16 Javascript
微信小程序接入腾讯云验证码的方法步骤
Jan 07 Javascript
js 数据类型判断的方法
Dec 03 #Javascript
用vue设计一个日历表
Dec 03 #Vue.js
JS闭包原理及其使用场景解析
Dec 03 #Javascript
实用的 vue tags 创建缓存导航的过程实现
Dec 03 #Vue.js
Javascript节流函数throttle和防抖函数debounce
Dec 03 #Javascript
如何实现vue的tree组件
Dec 03 #Vue.js
Vue实现图书管理小案例
Dec 03 #Vue.js
You might like
php mysql 判断update之后是否更新了的方法
2012/01/10 PHP
ThinkPHP验证码和分页实例教程
2014/08/22 PHP
thinkPHP基于ajax实现的菜单与分页示例
2016/07/12 PHP
PHP实现的激活用户注册验证邮箱功能示例
2017/06/06 PHP
JS文本框默认值处理详解
2013/07/10 Javascript
Nodejs+express+html5 实现拖拽上传
2014/08/08 NodeJs
纯JavaScript实现的分页插件实例
2015/07/14 Javascript
jQuery获取父元素节点、子元素节点及兄弟元素节点的方法
2016/04/14 Javascript
JavaScript的ExtJS框架中表格的编写教程
2016/05/21 Javascript
jQuery实现日期联动效果实例
2016/07/26 Javascript
jQuery弹出遮罩层效果完整示例
2016/09/13 Javascript
JavaScript中访问id对象 属性的方式访问属性(实例代码)
2016/10/28 Javascript
深入理解Node.js 事件循环和回调函数
2016/11/02 Javascript
ionic+AngularJs实现获取验证码倒计时按钮
2017/04/22 Javascript
jQuery Autocomplete简介_动力节点Java学院整理
2017/07/17 jQuery
nodejs图片处理工具gm用法小结
2018/12/12 NodeJs
javascript Canvas动态粒子连线
2020/01/01 Javascript
解决Can't find variable: SockJS vue项目的问题
2020/09/22 Javascript
Python版的文曲星猜数字游戏代码
2013/09/02 Python
在Django中进行用户注册和邮箱验证的方法
2016/05/09 Python
python多维数组切片方法
2018/04/13 Python
python实现图书馆研习室自动预约功能
2018/04/27 Python
python解析含有重复key的json方法
2019/01/22 Python
详解Python解决抓取内容乱码问题(decode和encode解码)
2019/03/29 Python
NumPy排序的实现
2020/01/21 Python
python GUI库图形界面开发之PyQt5图片显示控件QPixmap详细使用方法与实例
2020/02/27 Python
Django跨域资源共享问题(推荐)
2020/03/09 Python
秋季婚礼证婚词
2014/01/11 职场文书
平民服装店创业计划书
2014/01/17 职场文书
合作协议书范本
2014/10/25 职场文书
2014年妇幼保健工作总结
2014/12/08 职场文书
市场总监岗位职责
2015/02/11 职场文书
丧事主持词
2015/07/02 职场文书
大学入学感言
2015/08/01 职场文书
护士旷工检讨书
2015/08/15 职场文书
SpringBoot整合JWT的入门指南
2021/06/29 Java/Android