js异步编程的那些事
主要是关于js运行顺序,宏任务,微任务,任务队列,Event loop,同步事件异步事件
异步编程
背景:js诞生之际是一门单线程语言,功能较少。后随着计算机的发展,ajax等的兴起。当计算机执行一个需要较长时间的进程时候,为了提高计算机效率,不让其空闲,就有了异步。
什么是异步js
- 在js中,任务分为两种,同步任务和异步任务。同步任务在主线程中,一开始就执行,而异步任务在需要时或者特点条件下才会执行。
异步js主要有:
- DOM操作
- 定时器
- AJAX请求
在程序执行过程中,所以的同步任务都会在主线程上执行,形成一个执行栈
主线程之外还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件,当同步任务执行完,或者说执行栈一被清空。主线程就开始执行任务队列里面的异步事件,即当前异步事件的回调函数。再任务队列中,事件遵循先进后出规则。
异步事件注意点
- 异步事件的发生先后顺序是不确定的,这取决于当前事件在子进程中完成的先后顺序,先完成的先进入任务队列,先执行。
- 定时器由于任务队列的先进后出,可能会延时发生。当定时器时间到达时,才会在任务队列中添加事件(在这之前,挂载在子进程上),而此时定时器需要等到先排队的事件执行完才能执行。(前提是执行栈也已经被清空)
- Event loop 主线程在任务队列中执行事件是循环不断的,整个这个的运行机制叫做Event loop。
- html5 规定定时器最短时间间隔为4ms,足的会变为4ms。老版本的规定为10ms,DOM时间触发后不会立即执行,会延误16ms。
Promise
Promise是异步编程的一种解决方案。比传统的解决方案 ——回调函数和事件——更合理强大。
可以说是一个容器,里面保存着某个未来才会结束的事件,(通常是一个异步操作)的结果。可以获取异步操作的消息
特点
对象的状态不受外界影响,~代表一个异步操作,有三种状态
- pending
- fulfilled
- rejected
只有异步的操作结果,可以决定当前是哪一种状态,任何其他的操作都无法改变这个状态。
一旦状态改变就不会再变,只有两种可能
- pending -> fulfilled
- pending -> rejected
只要是事件发生了,就不会再改变,叫做定型resolved。
而事件的特点是,如果你错过了他,再去监听,是得不到结果的。
1 | //实例化Promise对象 |
- 如果后面嵌套then。里面返回promise对象
- .then 可以链式不断调用
1 | p.then((info) => { |
async await
- 用await替代then,await和async需要配合使用
1 | async function move(){ |
async 和 await 执行顺序*
1 | async function async1() { |
1 | async function queryData() { |
ajax 配合async
Promise.prototype.finally()
用于指定不管 Promise 对象最后状态如何,都会执行的操作。
不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
1 | promise |
Promise.all()
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
1 | const p = Promise.all([p1, p2, p3]); |
上面代码中,Promise.all()
方法接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()
方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
Promise.race()
Promise.race()
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
1 | const p = Promise.race([p1, p2, p3]); |
只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
Promise.race()
方法的参数与Promise.all()
方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()
方法,将参数转为 Promise 实例,再进一步处理。
Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是Promise.any()
不会因为某个 Promise 变成rejected
状态而结束,必须等到所有参数 Promise 变成rejected
状态才会结束
宏任务微任务
1.任务分类
- 同步任务
- 异步任务
- 宏任务
- 微任务
2.任务详情
同步任务
定义:立即执行的任务
异步任务
定义:调用后不会立即执行的任务,例如定时器,promise,requestAnimationFrame
- 宏任务
- 定时器
- requestAnimationFrame
- 微任务
- promise
- 宏任务
3.任务执行顺序
1..执行顺序
- 先执行同步任务,再执行异步任务
- 先执行微任务,再执行宏任务
1 | console.log(1); //同步执行1 |
2.注意:
- 当执行promise内的代码时,语句遵循同步执行的规则
4.任务执行的原理*
众所周知,js语言在刚研发出来的时候,请求量很少,不像想在有那么多进程,即被设计为一个单线程语言。
当浏览器在执行一个js代码的时候,首先他会从上往下执行代码,当碰到同步任务的时候,它就会让他进入到主线程之中立即执行。但是,当它解析到异步代码的时候,它就会将该任务放到一个子进程中去跑,然后继续向下执行,当再遇到同步代码时依旧立即执行,再遇到异步任务同样将其挂载到子进程,直到执行到最后一句代码。
此时,主进程空了以后,浏览器就会做一件事情–Event-loop–,什么是eventloop呢,可以理解为,浏览器问任务队列,你的异步任务有执行完成的吗?如果没有我等一下再来问。好,如果此时有异步任务那么就让这个异步事件立即执行。这时就得讲一讲异步任务从子进程到任务队列的过程,当异步任务在子进程运行(跑)的时候,….然后有一个运行结束,就比如一个定时器,定时3000,3秒后他跑完,然后就将他的回调加载到任务队列里面去。在任务队列里任务执行的先后,遵循先进先出。
到这同步异步就这些,然后宏任务和微任务是怎么执行的呢?这时候就把任务队列划分宏任务队列和微任务队列,宏任务执行完了进宏任务队列,微任务执行完了进微任务队列,当执行Event-loop的时候,先执行宏任务队列里面的任务,再执行微任务队列里面的任务。
4..requestAnimationFrame 请求动画帧
requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机
具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
let box = document.querySelector('.box'); let len = 0; function fn() { len += 5; box.style.left = len + 'px'; if (len < 1280) { window.requestAnimationFrame(fn); } } window.requestAnimationFrame(fn);
(1)requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回 流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
(2)在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然 就意味着更少的 CPU、GPU 和内存使用量
(3)requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动 优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销