线程是cpu最小的工作单元,一个线程一个时间点只能做一件事。
js引擎是通过一个线程在工作,也就是说JS语言是单线程,JS编译器每次执行都只能执行一句代码。
以下内容都是针对JS而言。
Synchronize 同步
按照我们写的代码的顺序依次执行。
Asynchronous 异步
简单的理解 让函数(代码)在将来某个特定顺序或者时机执行。
定时器
setTimeout 延迟多少毫秒执行
setInterval 每间隔多少毫秒执行
返回一个timeID,也就是一个整数。
都可以有三个参数,第一个将来要执行的函数,第二个毫秒数,第三个及更多传入要执行的函数中的数据(作为参数传入)
定时器清理
clearTimeout
clearInterval
将timeID传入到相应的清理方法就可以。
在定时器中要执行的函数、DOM事件处理函数、从服务器获取数据的代码都是异步代码,而所有要异步执行的函数,都会在所有同步执行的代码执行完之后再执行。
回调地狱
回调的方式的异步代码在某些场景下会有回调地狱的问题。出现回调地狱代码不好调试、可读性也很差。
实验
从服务器获取数据的伪代码如下:
function getDataFromUrl(url, data, callback) {
// 模拟不定时间从服务器获取了数据, 然乎调用回调函数。
const randomDelay = Math.random().toFixed(3) * 10000;
const data = Math.random();
setTimeout(() => {
callback(data)
},randomDelay)
}
有如下需求:
有三个网址:
https://aaa, https://bbb, https://ccc
a网站数据可以直接获取,但b网站数据需要先获取到a网站数据,c网站数据需要先获取到b网站数据。
这符合异步的定义,即代码在将来某个特定时候或者时机执行。
可以用回调函数进行实现:
getDataFromUrl('http://aaaaaaaa','a',(resData)=>{
console.log('received a data ')
// 获取B数据
getDataFromUrl('http://bbbbbbbb',resData,(resData)=>{
console.log('received b data ')
getDataFromUrl('http://cccccccc',resData,(resData)=>{
console.log('received c data ')
})
})
再极端点情况:
getDataFromUrl('http://aaaaaaaa','a',(resData)=>{
console.log('received a data ')
// 获取B数据
getDataFromUrl('http://bbbbbbbb',resData,(resData)=>{
console.log('received b data ')
getDataFromUrl('http://cccccccc',resData,(resData)=>{
console.log('received c data ')
getDataFromUrl('http://cccccccc',resData,(resData)=>{
console.log('received c data ')
getDataFromUrl('http://cccccccc',resData,(resData)=>{
console.log('received c data ')
getDataFromUrl('http://cccccccc',resData,(resData)=>{
console.log('received c data ')
})
})
})
})
})
这简直就是地狱!
为什么需要异步?
- 我们不希望代码在类似获取服务器数据的时候,就卡在那,而不去执行下面的同步代码。
- 我们有时候会希望一部分代码执行完后,另一部分代码再执行。
- 也就是说,异步编程这种方式确保了程序在异步操作完成之前不会执行后续的代码,同时不会阻塞整个程序的其他部分(小逻辑阻塞,大方向不阻塞)。
Promise
是ES6标准中才出现的一个机制 (类型),对某个行为产生的结果约定好后续处理方式的一种机制。
基本用法
function getDataFromUrl(url, data, callback) {
const randomDelay = Math.random().toFixed(3) * 10000;
setTimeout(() => {
callback()
},randomDelay)
}
console.log(1)
const p1 = new Promise((resolve,reject)=>{
// 真正你想要执行的代码 写在这个函数里面
console.log(2)
getDataFromUrl('http://localhost:8080/','a',()=>{
console.log('aaaa')
resolve(22222)
// reject(222)
})
console.log(3)
注意:传入promise中的这个函数(一般被称为执行函数 )他是同步执行的 !!!也就是说上面的代码中控制台输出依次为 1, 2, 3。
promise有两个重要属性,[[PromiseState]] 实例状态和[[PromiseResult]] 实例数据
-
[[PromiseState]] promise实例有三个状态,pending 待定状态,fullfilled 成功状态、解决状态,rejected 失败状态、拒绝状态。resolve执行会让promise实例变成 fullfilled状态,reject执行 会让priomise实例变成 rejected状态。
-
注意: 每个promise的实例的状态只能改变一次,要么 从pending变成fullfilled,要么从pending变成rejected。
-
resolve 和 reject 能改变相应promise实例的[[PromiseResult]]。执行时传入什么,值就会称为 [[PromiseResult]] 的值。
那么实例状态与结果改变有什么意义呢?见下一小节。
原型上的then方法
Promise.prototype.then 原型方法,每一个Promise类型的实例都可以调用。
基本实验
const p1 = new Promise((resolve, reject) =>{
setTimeout(() => {
resolve(1);
})
})
const p2 = p1.then(
()=>{
// 这里是要写的代码
},
()=>{}
)
过程如下所示:
一开始的时候,p1这个pormise实例创建成功,其中PromiseResult是undefine, PromiseState是pending,而其中的setTimeout函数是异步函数,会在后面才执行。
p1创建后,p1调用了then方法,then方法也会返回一个promise实例,也就是p2这个promise实例。p2创建后,与p1一样中PromiseResult是undefine, PromiseState是pending。
then 方法执行要传两个函数做为参数(可以不传)。第一个函数,其触发放入任务队列准备执行的时机,就是前一个promise的pfillromiseState变为fullfilled的时候。第二个函数,其触发放入任务队列准备执行的时机,就是前一个promise的promiseState变为reject的时候。
也就是说,当p1执行resolve(1)后,p1中的promiseState会变为fullfilled,promiseResult变为1,并且发出一个信号,使得p2执行handleResolve()函数。同理当p1执行reject(2)函数后p1中的promiseState会变为rejected,promiseResult变为2,并且发出一个信号,使得p2执行handleReject()函数。但是!不论p2执行的是handleResolve()函数还是handleReject()函数,只要这两个函数能够成功运行,p2的promiseState都是fulfilled!
注意:任何一个方法 如果执行过程中报错 那么实例就会变成rejected状态实例数据就是报错信息
p2的promiseResult值取决于,handleResolve()函数或者handleReject()函数返回的值,当然如果这两个函数不返回值的话,promiseResult就是undefined,其实也可以看成,不返回值其实就是返回了undefine。
注意:如果前一个promise执行reject,但后面的then中没传handleReject()函数或者前一个promise执行resolve,但后面的then中没传handleResolve()函数,那前一个promise的状态和数据会原样后传。
如下所示:
const p1 = new Promise((resolve, reject) =>{
setTimeout(() => {
//resolve(1);
reject(22222)
})
})
const p2 = p1.then(
undefine,
()=>{}
)
p1执行了reject()函数,但是then中没有handleReject()函数,那p1的promiseResult与promiseState就会原样后传给p2。对于resolve()和handleResolve()函数也是一样的。
注意:链式then调用,最终如果只要有Rejected状态的实例,那么就会报Promise的错误。
传递数据的方式
handleReject()和handleResolve()中可以带参数,如下所示:
const p1 = new Promise((resolve, reject) =>{
getDataFromUrl('http://aaaa','a',(res)=>{
resolve(res)
})
// throw new Error('11')
})
const p2 = p1.then(
(res)=>{
return new Promise((resolve, reject) =>{
getDataFromUrl('http://bbbb',res,(response)=>{
resolve(response)
})
})
},
(err)=>{}
)
p2.then(
(res)=>{
getDataFromUrl('http://ccc',res,(response)=>{
})
},
(err)=>{}
)
其中res与err的值就是前promise的[[promiseResult]],这就是promise中传递数据的方式。比如说,p1中的任务执行完产生的数据,需要给p2,供p2的下一个任务使用,就可以通过这种方式实现。
但有一个问题,如果直接在p2的handleResolve()函数中调用访问数据的函数,如下:
const p1 = new Promise((resolve, reject) =>{
getDataFromUrl('http://aaaa','a',(res)=>{
resolve(res)
})
// throw new Error('11')
})
const p2 = p1.then(
(res)=>{
getDataFromUrl('http://bbbb',res,(response)=>{
resolve(response)
})
},
(err)=>{}
)
p2.then(
(res)=>{
getDataFromUrl('http://ccc',res,(response)=>{
})
},
(err)=>{}
)
会有一个问题,那就是 p2中的handleResolve()函数,一执行完getDataFromUrl()函数之后,p2的promiseState马上就会变成fullfiled,p3的handleResolve()函数马上就会执行,这不符合我们的预期,即p2中任务执行完得到数据后,再执行p3!所以我们要在手动返回一个Pormise。如下图示意:
手动返回的Promise的promiseResult和promiseState会在response收到后,再传入resolve()函数中,使promiseResult和promiseState变化,再复制到p2中,实现我们的预期效果。
最后,p3再得到p2中得到的数据后,再发送数据请求。
这种then方式的调用,其实就有点像 一步 下一步 接着下一步的执行顺序。就可以把回调地狱的代码 变成了看起来像同步代码的书写模式,但是其实依然是异步的代码。
promise的其他方法
原型方法
catch方法
Promise.prototype.catch, 相当于调用then(null, handleRejected),其中函数也有参数,可以获取到实例数据。该方法也会返回一个新的promise实例。
finally方法
Promise.prototype.finally,finally执行也会返回一个promise实例,父实例的状态改变后, 传入的函数都会执行。同时父实例的数据和状态会原样后传,且数据不会被传入函数的返回值改变。
静态方法
Promise.resolve()
产生一个fullfilled状态实例, 可以传入一个参数,作为Promise的数据
Promise.reject()
产生一个reject状态实例,可以传入一个参数,作为Promise的数据。
Promise.all()
- 要传入一个可迭代对象,比如说数组。这里要求数组里面都是Promise,执行返回一个promise。
- 如果数组中所有promise实例状态都是fullfiled,那返回的实例就是fullfiled;反之,只要有一个pormise实例时reject,那返回的实例就是reject。而返回的promise实例的数据只取决于数组中第一个promise实例的数据。
Promise.race()
- 要传入一个可迭代对象,比如说数组。这里要求数组里面都是Promise,执行返回一个promise。
- 只要数组中第一个(时间上的第一个)promise实例变为reject,返回的promise实例就reject;只要数组中第一个(时间上的第一个)promise实例变为fullfiled,返回的promise实例就fullfiled。
Promise.any()
- 要传入一个可迭代对象,比如说数组。这里要求数组里面都是Promise,执行返回一个promise。
- 如果数组中所有promise实例状态都是reject,那返回的实例就是reject;反之,只要有一个pormise实例时fullfiled,那返回的实例就是fullfield。而返回的promise实例的数据只取决于数组中第一个promise实例的数据。
好用的async和await
async的使用方式
async function foo() {}
let bar = async function() {}
let baz = async () => {} // 箭头函数也可以
class Qux {
async qux(){}
}
async关键字可以让函数具有异步特征,要配合await这个关键字。
实验
console.log(111)
async function test(){
console.log('async')
const result = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(66666)
},1000)
})
console.log(result)
}
test()
console.log(222)
await 左侧及下方代码会是异步执行,await 右侧及上方代码会是同步执行!
以实验为例,最后输出结果时111, async, 222, 66666。为什么呢?
首先这段代码同步执行,首先输出111。
然后定义test,执行test。test执行过程中,它并不是整个test函数都是异步执行的。
await的上方还是同步执行的,打印async。
然后await右边的Promise实例创建是同步的,但是promise中的setTimeout()函数是异步的。
await左边的result是异步的,它需要等await右边的Promise的状态变为fullfiled,await会将Promise的数据解析返回过来。
await下边的console.log(result)也是异步的,它需要等result收到值。
最后的console.log(222)是同步的,直接执行输出222.
当222输出完后,就会执行异步函数setTimeout,调用resolve(222),使promise状态变为fullfield,并且将222作为promise数据,并且返回给result。然后console.log(result),就会输出66666。
这样做到了test()函数不会阻塞后部代码(比如console.log(222)),又让test()函数中按照特定的顺序执行代码了。
多个await使用
async function test(){
console.log('async')
const resultA = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(66666)
},1000)
})
const resultB = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(55555)
},1000)
})
const resultC = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(44444)
},1000)
})
console.log(resultA, resultB, resultC);
}
test()
await后跟数据要求
await右边也不是一定要是promise, 如果不是,这个值就被当做已解决的promise实例中包含的数据。await关键字期待(但实际上并不要求)一个实现thenable接口的对象,但常规值也可以。如果是实现了thenable接口的对象,则这个对象可以由await来解包。promise实例 都是thenable对象。
async function test(){
console.log('async')
const result = await 333
console.log(result)
}
test()
时间循环机制
浏览器是多进程的,其中有一个进程被称为 Render进程 即 渲染进程。这个进程非常重要!!
页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,所以我们要重点了解渲染进程。
渲染进程是多线程的,我们来看渲染进程的一些常用较为主要的线程,列举的仅为该进程中部分线程
渲染进程
GUI渲染线程
-
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制
- 当我们改变页面中元素的样式时,GUI线程执行,重新绘制页面
- GUI渲染线程与JS引擎线程是互斥的
- 当JS引擎执行时GUI线程会被挂起(相当于被冻结了)
- GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
JS引擎线程
-
JS引擎线程就是JS内核,负责处理Javascript脚本程序(例如chrome的V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码
- JS引擎一直等待着任务队列中任务的到来,然后加以处理
- 浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的
- 一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
事件触发线程
event queue
event table
当js执行碰到事件绑定和一些异步操作(如setTimeOut,也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等)
负责把那些需要异步执行的函数 在相应的时机 添加到任务队列中,等待JS引擎线程空闲时来处理。DOM等事件也是事件触发机制在起作用,像事件的回调,你也可以认为就是事件触发线程在管理。(回调、异步事件都是它在管理)
定时触发器线程
个人认为,只负责计时,时间到了之后,会发出信号给事件触发线程,让事件触发线程进行处理。
宏任务与微任务
宏任务
浏览器中常见的宏任务:
主代码块(同步代码)
setTimeout
setInterval
requestAnimationFrame()
微任务
常见的微任务:
promise.then
catch
finally
MutationObserver
注意:async、await,await左边和下边的代码相当于promise.then,是微任务; await右边和上边的代码是同步代码,是宏任务
当一个宏任务执行完,会在渲染前,将执行期间产生的所有微任务都执行完!
宏任务->微任务-> GUI渲染->宏任务->....