在实例中重学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 相关文章推荐
js实现鼠标拖动图片并兼容IE/FF火狐/谷歌等主流浏览器
Jun 06 Javascript
利用jquery包将字符串生成二维码图片
Sep 12 Javascript
谈谈JavaScript类型系统之Math
Jan 06 Javascript
JQuery的Pager分页器实现代码
May 03 Javascript
详解Backbone.js框架中的模型Model与其集合collection
May 05 Javascript
jQueryUI Datepicker组件设置日期高亮
Oct 13 Javascript
vue-router项目实战总结篇
Feb 11 Javascript
mpvue构建小程序的方法(步骤+地址)
May 22 Javascript
Vue源码解析之数据响应系统的使用
Apr 24 Javascript
JavaScript变量作用域及内存问题实例分析
Jun 10 Javascript
element-ui 文件上传修改文件名的方法示例
Nov 05 Javascript
JavaScript事件概念详解(区分静态注册和动态注册)
Feb 05 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的责任链编程模式
2015/08/11 PHP
详解PHP文件的自动加载(autoloading)
2018/02/04 PHP
不常用但很实用的PHP预定义变量分析
2019/06/25 PHP
JavaScript 继承详解(一)
2009/07/13 Javascript
js调用浏览器打印模块实现点击按钮触发自定义函数
2014/03/21 Javascript
JQuery教学之性能优化
2014/05/14 Javascript
js限制文本框只能输入数字方法小结
2014/06/16 Javascript
JS实现屏蔽shift,Ctrl,alt等功能键的方法
2015/06/01 Javascript
BootStrap中的table实现数据填充与分页应用小结
2016/05/26 Javascript
BootStrap轻松实现微信页面开发代码分享
2016/10/21 Javascript
JS匹配日期和时间的正则表达式示例
2017/05/12 Javascript
详解nodejs通过代理(proxy)发送http请求(request)
2017/09/22 NodeJs
详解angularjs 学习之 scope作用域
2018/01/15 Javascript
Vue2.0系列之过滤器的使用
2018/03/01 Javascript
jQuery实现列表的增加和删除功能
2018/06/14 jQuery
vue填坑之webpack run build 静态资源找不到的解决方法
2018/09/03 Javascript
微信小程序实现底部导航
2018/11/05 Javascript
详解Vue.js v-for不支持IE9的解决方法
2018/12/29 Javascript
Vue实现导航栏点击当前标签变色功能
2020/08/19 Javascript
python正则表达式去掉数字中的逗号(python正则匹配逗号)
2013/12/25 Python
Python 实现链表实例代码
2017/04/07 Python
PyTorch CNN实战之MNIST手写数字识别示例
2018/05/29 Python
python字符串常用方法
2018/06/14 Python
详谈Python 窗体(tkinter)表格数据(Treeview)
2018/10/11 Python
Python反爬虫技术之防止IP地址被封杀的讲解
2019/01/09 Python
Pyqt5实现英文学习词典
2019/06/24 Python
详解django实现自定义manage命令的扩展
2019/08/13 Python
Python 元组操作总结
2019/09/18 Python
Django+uni-app实现数据通信中的请求跨域的示例代码
2019/10/12 Python
python绘制封闭多边形教程
2020/02/18 Python
PyQt5实现登录页面
2020/05/30 Python
加拿大户外探险购物网站:SAIL
2020/06/27 全球购物
护士自我鉴定
2013/10/23 职场文书
中英文自我评价常用句型
2013/12/19 职场文书
学校百日安全生产活动总结
2014/07/05 职场文书
2016优秀毕业生个人事迹材料
2016/02/29 职场文书