Promise-整体说明
它是一个ES6提出一个新语法,用来优化异步代码的写法。
- 在前端领域是我们必须要会的一个内容
- 后续的课程中也会多次使用
promise:承诺
- 生活中,它是用来表述
对将来要发生的事情的肯定
。 例如 : 高中生说,老师,我会考上一所好大学的;销售员说,老板,我一定会签下大单的;程序员说,老妈,我过年一定会带个女朋友回来的。 - 在ES6中,它是新增的构造器(Array, Object, Promise),用来优化实现异步操作。在没有它之前,javascript中的异步处理,大多是利用回调函数来实现的。典型的几种如下:(1)setTimeout (2)ajax (3)nodejs中的文件读取。现在有了promise,就可以对这些异步操作进行改写了。
学习内容
- 构造器
- 三种状态和值
- 经典调用格式
- then的执行逻辑和返回值
- then链式用法($().x().x())
- async await
问题导入
回调地狱问题
示例代码
后台接口
// 静态资源托管,直接访问public/api.html const express = require('express') const app = express() var cors = require('cors') // 处理跨域请求 app.use(cors()) app.use( express.static('public') ) // 接口1:获取所有的频道 app.get('/get', (req, res) => { res.send([{id:1, name:'体育频道'}, {id:2, name:'娱乐频道'}]) }) // 接口2:获取指定频道下的分类 app.get('/getCategory', (req, res) => { if(req.query.id==1){ // 体育频道 res.send([{id:12, name:'NBA新闻'}, {id:13, name:'足球新闻'}]) }else { res.send([{id:10, name:'娱乐天天笑'}]) } }) // 接口3:获取指定新闻列表 app.get('/getNews', (req, res) => { if(req.query.id==12){ // NBA新闻 res.send([{id:1, name:'谁能得到今年的总冠军'}, {id:2, name:'湖人已经不行了么?'}]) }else { res.send([]) } }) app.listen(3000)
前台代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script> function getData({url,success}) { const xhr = new XMLHttpRequest() xhr.open('get', url) xhr.onreadystatechange = function (){ if(this.readyState === 4){ // 加载完成 const d = JSON.parse(xhr.responseText); success(d) } } xhr.send() } getData({ url:'http://localhost:3000/get', success: (data)=>{ getData({ url:'http://localhost:3000/getCategory?id='+data[0].id, success: data => { getData({ url:'http://localhost:3000/getNews?id='+data[0].id, success: data => { console.log(data) } }) } }) }}) </script> </body> </html>
Promise的基本使用
Promise是承诺的意思,es6中提出一个新api。
用来优化异步代码。它的经典使用方式如下
var p1 = new Promise(function(resolve,reject){ //异步操作 resolve(obj1) 或者 reject(obj2) }); p1.then(function(rs){ // 如果p1的状态是resolved,则then中的函数 //会执行,且obj1的值会传给rs }).catch(function(rs){ // 如果p1的状态是reject,则catch中的函数 // 会执行,且obj2的值会传给rs }).finally(function(){ // 一定会执行的函数 })
构造器
// const obj = new Object() // const arr = new Array() const p1 = new Promise(function(resolve,reject){ // 执行异步代码 // 调用resolve,或者reject }); console.dir(p1)
要点:
- 构造器必须要给定一个参数,如果不给就是会报错。例如,
new Promise()
报错的信息是:Promise resolver undefined is not a function
- 构造器的实参是一个函数,这个函数的特殊之处在于它有两个形参(resolve,reject),这两个形参也是函数。在格式上,也可以采用箭头函数来改写。例如:
var p1 = new Promise((resolve,reject)=>{})
。 - 在函数体的内部, 一般会执行异步代码,然后根据情况来调用resolve()或者是reject() 。调用resolve或者是reject后会产生什么样的后果,在后面小节介绍。 当然了,再次强调一下resolve和reject只是形参名,可以改写成其它的。
三种状态和值
一个Promise对象的状态可能是如下三种之一:pending
,resolved
,rejected
。下面分别介绍。
初始态pending
pending。它的意思是 "待定的,将发生的",相当于是一个初始状态。 创建Promise对象时,且没有调用resolve或者是reject方法,相当于是初始状态。这个初始状态会随着你调用resolve,或者是reject函数而切换到另一种状态。
var p = new Promise((resolve,rejectok,err)=>{ console.info('发呆.....' )}) console.dir(p)
这个初始状态就是你对别人说出承诺的那个瞬间,此时,大家都不知道这个承诺是否能兑现。
成功态resolved
也有叫fulfilled的。
resolved。表示解决了,就是说这个承诺实现了。 要实现从pending到resolved的转变,需要在 创建Promise对象时,在函数体中调用了resolve方法(即第一个参数)。
var p = new Promise((resolve,reject)=>{ resolved();}) console.dir(p)
注意,上面的resolve和reject仅是形参名而已。
失败态rejected
rejected。拒绝,失败。表示这个承诺没有做到,失败了。要实现从pending到rejected的转换,只需要在创建Promise对象时,调用reject函数。
var p = new Promise((resolve,reject)=>{ reject()} ) console.dir(p)
三种状态小结
- 状态是可转化。
最初创建promise对象时,默认状态是pending,如果在函数体内部调用了第一个参数对应的函数,则状态变成了resolved;如果调用了第二个参数对应的函数,则状态变成了rejected。
pending ----- resolve() --> resolved; pending ----- reject() --> rejected;
- 状态转换是不可逆的。
一旦从pending ---> resolved(或者是rejected),就不可能再回到pending,也不能由resolved变成rejected。
promise的值
一个promise对象除了状态之外,还有promiseValue。在构造器中,这个值在调用resolve和reject方法时传入。
例如:
var p = new Promise( (resolve,reject) => { resolve(123); } ); // 此时,prommise对象p的状态是 resolved,值是123。 console.dir(p)
promise的兑现
在构造器的函数体中,一旦状态发生了变化,就会进行then,或者是catch中去,同时把promiseValue传入对应的函数。
具体来说:
- 状态从pending变成resolved,进入then中,调用函数,并传入此时的promiseValue(就是调用resolve时传入的实参)
- 状态从pending变成rejected,进入catch中,调用函数,并传入此时的promiseValue(就是调用reject时传入的实参)
根据实际情况的需要,也可以不加上finally()。
使用promise改造回调函数
下面我们回到具体的问题:如何对现有的,通过回调函数来实现异步操作的代码进行改写。
基本套路
第一步:建立模板。这里的模板是指固定的套路:写一个空函数,在函数体中的创建一个promise对象,并返回。
function fnName(){ var p = new Promise((resolve,reject)=>{ }) return p; }
当然, 如果使用箭头函数的话,还可以简化。
第二步:把异步功能写入构造器中,根据实际来决定是否调用resolve,reject。
function fnName(){ var p = new Promise((resolve,reject)=>{ // 这里写具体的代码,并在某个恰当的时机去调用resolve和reject函数。 }) return p; }
第三步:调用函数。
通过fnName().then().catch()
结构来调用这个函数。
示例1: ajax的改写
原回调函数的写法
function getData({url,success}) { const xhr = new XMLHttpRequest() xhr.open('get', url) xhr.onreadystatechange = function (){ if(this.readyState === 4){ // 加载完成 const d = JSON.parse(xhr.responseText); success(d) } } xhr.send() }
改写之后
function getDataPromise (url) { return new Promise((resolve,reject) => { const xhr = new XMLHttpRequest() xhr.open('get', url) xhr.onreadystatechange = function (){ if(this.readyState === 4){ // 加载完成 const d = JSON.parse(xhr.responseText); resolve(d) } } xhr.send() }) }
then的格式及执行逻辑
目标
掌握then方法的使用;
作用
then方法的作用是为Promise对象添加状态改变时的回调函数。
下面从其调用格式,执行逻辑及返回值三个方面来介绍
then的格式
它有两个参数,每个参数都是函数。 第二个参数是可选的。
如下:
// p 是一个promise对象 p.then(函数1[,函数2])
它的两个参数都是函数。
- 第一个参数是resolved状态的回调函数。当p的状态从pending变成了resolved,函数1会执行。
- 第二个参数是rejected状态的回调函数。 当p的状态从pending变成了rejected,函数2会执行。
其中第二个参数是可选的,如果只写一个参数的话就是如下:
promise对象.then(函数1)
执行逻辑
以如下代码为例
var p = new Promise((resolve,reject)=>{ // resolve(val1); reject(val2) }) p.then( okVal=>{ console.info("成功"); console.log(okValue); }, errVal=>{ console.info("失败"); console.log(errValue); } )
它的两个参数都是函数,其执行逻辑是:
- 如果promise对象的状态是resolved,则then()会执行第一个函数,并传入当前的PromiseValue(即上面的val1);
- 如果promise对象的状态是rejected,则then()会执行第二个函数,并传入当前的PromiseValue(即上面的val2);
- 特别地,如果promise对象的状态是rejected,且此时then方法并没有设置第二个参数,就会向外抛出一个错误,错误的提示大约是
Uncaught (in promise)
。
示例代码1
var p = new Promise((resolve,reject)=>{ resolve(1)//主动调用resolve,并传入 }) // 此时,P的状态是resolved,且值promiseValue 是1. p.then((res)=>{ // 因为p的状态是resolved,所以自动执行then的第一个参数,并且把promisevalue传进来。 console.log("then,ok",res) })
执行输出结果是:then,ok,1
示例代码2
var p = new Promise((resolve,reject)=>{ reject(2)//主动调用reject,并传入实参 }) // 此时,P的状态是rejected,且值promiseValue 是2. p.then((res)=>{ // 因为p的状态是resolved,所以这句代码不会执行。 console.log("then,ok",res) },(err)=>{ // 因为p的状态是rejected,所以自动执行then的第二个参数,并且把promisevalue传进来。 console.log("then,err",err) })
执行输出结果是:then,err,2
then的返回值(难点)
then()方法的返回值也是一个promise对象,所以它支持链式写法。但是要注意的是它的返回值是一个新的promise对象,与调用then方法的并不是同一个对象。
看下如下代码:
var p1 = new Promise(()=>{}); var p2 = p1.then(function f_ok(){}, function f_err(){}); // p2也是一个promise对象。 console.log(p1 === p2); // false
如上代码可以说明p1.then()的结果是一个与p1不同的promise对象。换句话说,then()会封装一个全新的promise对象p2。那既然 p2也是一个promise对象,那么,p2的状态(promiseStatus)和值(promiseValue)分别是什么?
p2的状态及promiseValue如何确定?
p2的状态及promiseValue按如下规则来确定
- 如果p1的状态是pending,则p2的状态也是pending。
- 如果p1的状态是resolved,then()会去执行f_ok,则p2的状态由f_ok的返回值决定。
-
- 如果f_ok返回值不是promise对象,则p2的状态是resolved,且p2的promiseValue就是f_ok函数的return值。
- 如果f_ok返回值是一个promise对象,则p2的状态及promiseValue以这个promise对象为准。
- 如果f_ok这个函数内部发生了错误(或者是用户主动抛出错误),则p2的状态是rejected,且p2的promiseValue就是这个错误对象。
- 如果p1的状态是rejected,then()会去执行f_err,则p2的状态由f_err的返回值决定。
-
- 如果f_err返回值不是promise对象,则p2的状态是resolved,且p2的promiseValue就是f_err函数的return值。
- 如果f_err返回值是一个promise对象,则p2的状态及promiseValue以这个promise对象为准。
- 如果f_err这个函数内部发生了错误(或者是用户主动抛出错误),则p2的状态是rejected,且p2的promiseValue就是这个错误对象。
练习
示例代码1
var p1 = new Promise(()=>{}); var p2 = p1.then(function f_ok(){}, function f_err(){}); // p2也是一个promise对象。 console.dir(p1); // pending console.dir(p2); // pending
示例代码2
var p1 = new Promise((resolve,reject)=>{ resolve()}); var p2 = p1.then(function f_ok(){ return 1 }, function f_err(){}); // p2也是一个promise对象。 console.dir(p1); // resolved, undefined console.dir(p2); // resolved, 1
特殊地,如果f_ok()中并没有return语句,则相当于是 return undefined。
示例代码3
var p1 = new Promise((resolve,reject)=>{ resolve()}); var p2 = p1.then(function f_ok(){ var temp = new Promise((resolve,reject)=>{ resolve({a:1}) }); return temp; }, function f_err(){}); console.dir(p2); // resolved, {a:1}
示例代码4
var p1 = new Promise((resolve,reject)=>{ resolve()}); var p2 = p1.then(function f_ok(){ console.log(abc);// 这里故意犯错 }, function f_err(){}); console.dir(p2);
示例代码5
var p1 = new Promise((resolve,reject)=>{ reject(100)}); var p2 = p1.then(function f_ok(){ }, function f_err(errVal){ var temp = new Promise((resolve,reject)=>{ resolve({b:errVal}) }); return temp; }); console.dir(p2);
示例代码6
var p1 = new Promise((resolve,reject)=>{ reject(100)}); var p2 = p1.then(function f_ok(){ }, function f_err(errVal){ throw new Error("aaa") }); console.dir(p2)
catch的格式及用法
catch 是 then(null, reject)的别名
catch()的格式及用法
Promise.prototype.catch 是 Promise.prototype.then(null, reject)的别名,用于指定当promise对象的状态从pending变成rejected的回调函数 。
var p1 = new Promise((resolve,reject)=>{ reject('s') }); p1.catch(function(err){ console.log(err); }) // 与下面的代码等价 p1.then(null, function(err){ console.log(err); })
单独使用catch没有什么意义,它一般与then一块使用。如下:
new Promise((resolve,reject)=>{ }).then(function(result){ // 如果promise对象的状态是resolved的,就会到这里来,并且result会接收promiseValue的值 }).catch(function(err){ // 如果promise对象的状态是rejected的,就会到这里来,并且err会接收promiseValue的值 }) // 上面的代码如何拆分来写的话,等价于: var p1 = new Promise((resolve,reject){ }); var p2 = p1.then(function(result){ }); var p3 = p2.catch(function(err){ })
catch的返回值
catch的返回值仍是一个promise对象,确定它的值的方式与then(null,(errVal)=>{ })
的方式一致。
promise的链式调用
链式调用: $().attr().val()
p.then().then().then()...
示例1
代码
function do1() { console.log("任务1"); } function do2() { console.log("任务2"); } function do3() { console.log("任务3"); } function do4() { console.log("任务4"); } var p = new Promise((resolve,reject)=>{ resolve()}) p.then(do1) .then(do2) .then(do3) .then(do4);
结果输出是:任务1,任务2,任务3,任务4
分析
var p1 = p.then(do1); var p2 = p1.then(do2) var p3 = p2.then(do3) var p4 = p3.then(do4)
第一步:由于p的状态是resolved,所以p.then(do1)中,do1函数会执行。输出任务1
。
第二步:确定p1的状态。按前面关于then的部分的介绍,p1的状态由do1()来决定。因为do1并没有明确指定返回值,则返回值就是undefined. p1的状态就是resolved。
第三步:由于p1的状态是resolved,所以p1.then(do2)会继续执行do2。输出任务2
,且p2的状态由do2来决定。与第二步的分析相同,p2的状态仍是resolved。
第四步:接下来看p3。由于p2的状态是resolved,所以p2.then(do3)会继续执行do3。输出任务2
,且p3的状态由do3来定,仍是resolved。
最后:p3.then(do4)。由于p3的状态是resolved,所以执行do4。输出任务4
。
示例2
function do1() { console.log("任务1"); } function do2() { console.log("任务2"); } function do3() { console.log("任务3"); } function do4() { console.log("任务4"); } var p = new Promise((resolve,reject)=>{ resolve()}) p.then(do1) .then(do2) .catch(do3) .then(do4);
上面的代码的执行结果是:任务1, 任务2, 任务4
。我们先把过程分解一下,多添加几个中间变量来描述它们的值:
var p1 = p.then(do1); var p2 = p1.then(do2) var p3 = p2.catch(do3) var p4 = p3.then(do4)
分析如下:
第一步:由于p的状态是resolved,所以p.then(do1)中,do1函数会执行。输出任务1
。
第二步:确定p1的状态。按前面关于then的部分的介绍,p1的状态由do1()来决定。因为do1并没有明确指定返回值,则返回值就是undefined. p1的状态就是resolved。
第三步:由于p1的状态是resolved,所以p1.then(do2)会继续执行do2。输出任务2
,且p2的状态由do2来决定。与第二步的分析相同,p2的状态仍是resolved。
第四步:接下来看p3。由于p2的状态是resolved,所以它并不会执行do3, p3的状态没有变化,仍保持p2的状态:resolved。
最后:p3.then(do4)。由于p3的状态是resolved,所以执行do4。输出任务4
。
示例3
function do1() { console.log("任务1"); console.log(abc); //故意犯错 } function do2() { console.log("任务2"); } function do3() { console.log("任务3"); } function do4() { console.log("任务4"); } var p = new Promise((resolve,reject)=>{ resolve()}) p.then(do1) .then(do2) .then(do3) .catch(do4);
上面的代码的执行结果是:任务1, 任务4
。我们先把过程分解一下,多添加几个中间变量来描述它们的值:
var p1 = p.then(do1) var p2 = p1.then(do2) var p3 = p2.then(do3) var p4 = p3.catch(do4)
分析如下:
第一步:由于p的状态是resolved,所以p.then(do1)中,do1函数会执行,输出任务1
,同时由于这里有段错误代码,所以p1的状态就是rejected。
第二步:由于p1的状态是rejected,所以p1.then(do2) 就不会执行do2,同时p2的状态也是rejected。
第三步:对p3的分析也是如此。
第四步:p3的状态是rejected,所以会执行do4。
示例4 传参
function increment(value) { return value + 1; } function doubleUp(value) { return value * 2; } function output(value) { console.log(value);// => (1 + 1) * 2 } var p = Promise.resolve(1); p.then(increment) .then(doubleUp) .then(output)
上面代码的输出结果是 4。
分析
拆分 之后,分析如下:
// 状态:resolved; promiseValue: 1 var p = Promise.resolve(1); // p的状态是resolved; // 在执行then的第一个参数,即increment时 // 把p的 promiseValue 1 传入 // increment执行后的返回值就是2 // p1的状态是resolved,promiseValue是2 var p1 = p.then(increment); // p1的状态是resolved; // 在执行then的第一个参数,即doubleUp时 // 把p1的promiseValue 2传入 // doubleUp执行后的返回值就是4 // p2的状态是resolved,promiseValue是4 var p2 = p1.then(doubleUp); // p2的状态是resolved,promiseValue是4 // 在执行then的第一个参数,即output时 // 把p2的promiseValue传入,就是4 var p3 = p2.then(output) ;
小小一道面试题
代码填空,并完成指定功能。
function sleep(time){ // 请写出你的代码 } sleep(2000).then(()=>{ console.log("后续操作") }) console.log(2);
目标 是让sleep 的功能与setTimeout一样:就是等2000毫秒之后再执行后续操作。
参考代码如下:
function sleep(time){ return new Promise(function(resolve,reject){ // 异步操作,根据执行结果,决定是否调用 resolve,reject setTimeout(function(){ resolve() }, time) }) }
回调地狱-演示
示例代码
后台接口
// 静态资源托管,直接访问public/api.html const express = require('express') const app = express() var cors = require('cors') // 处理跨域请求 app.use(cors()) app.use( express.static('public') ) // 接口1 get app.get('/get', (req, res) => { res.send([{id:1, name:'体育'}]) }) app.get('/getCategory', (req, res) => { if(req.query.id==1){ res.send([{id:12, name:'NBA新闻'}, {id:13, name:'足球新闻'}]) }else { res.send([{id:10, name:'羽毛球'}]) } }) app.get('/getNews', (req, res) => { if(req.query.id==12){ res.send([{id:1, name:'谁能得到总冠军'}, {id:2, name:'湖人已经不行了么?'}]) }else { res.send([]) } }) app.listen(3000)
前台代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script> function getData({url,success}) { const xhr = new XMLHttpRequest() xhr.open('get', url) xhr.onreadystatechange = function (){ if(this.readyState === 4){ // 加载完成 const d = JSON.parse(xhr.responseText); success(d) } } xhr.send() } function getDataPromise (url) { return new Promise((resolve,reject) => { const xhr = new XMLHttpRequest() xhr.open('get', url) xhr.onreadystatechange = function (){ if(this.readyState === 4){ // 加载完成 const d = JSON.parse(xhr.responseText); resolve(d) } } xhr.send() }) } getData({ url:'http://localhost:3000/get', success: (data)=>{ getData({ url:'http://localhost:3000/getCategory?id=1', success: data => { console.log(data) getData({ url:'http://localhost:3000/getNews?id='+data[0].id, success: data => { console.log(data) } }) } }) }}) getDataPromise('http://localhost:3000/get').then(data=>{ return getDataPromise('http://localhost:3000/getCategory?id='+data[0].id) }).then(data => { return getDataPromise('http://localhost:3000/getNews?id='+data[0].id) }).then(data => { console.log(data) }) async function get() { const res1 = await getDataPromise('http://localhost:3000/get') const res2 = await getDataPromise('http://localhost:3000/getCategory?id='+res1[0].id) const res3 = await getDataPromise('http://localhost:3000/getNews?id='+res2[0].id) console.log(res3) } get() </script> </body> </html>
回调地狱-用promise来优化
const f = (filepath) => { const p = new Promise((resolve, reject)=>{ // 这里写具体的代码,并在某个恰当的时机去调用resolve和reject函数。 fs.readFile(filepath,'utf-8',(err, data)=>{ if(err) { // console.log(err); reject(err) } else { resolve(data) // console.log(data) } }) }) return p } let str = "" f('./1.txt').then(str1 => { console.log(str1) str+=str1 return f('./2.txt') }).then((str2)=>{ str+=str2 console.log(str2) return f('./3.txt') }).then(str3=>{ str+=str3 console.log(str3) console.log(str) })
async-await语法
async,await 是es7中新增的语法,用来进一步改进异步代码的写法,是promise升级版!
async
async
函数返回一个 Promise 对象。
async
函数内部return
语句返回的值是Promise 对象的值
function f1 () { return 1 } async function f2 () { return 1 } async function f3 () {} const r1 = f1() const r2 = f2() const r3 = f3() console.log(r1) // 1 console.log(r2) // Promise, resolved(1) console.log(r3) // Promise, resolved(undefined) r2.then(res => { console.log(res) }) r3.then(res => { console.log(res) })
await 命令
await的外层函数必须有一个async.
正常情况下,await
命令后面是一个 Promise 对象,返回该promise的值。如果不是 Promise 对象,就直接返回对应的值。
<script> async function f() { const p = new Promise((resolve,reject)=>{ resolve(100) }) const a = 1 // await 等待promise的状态变化完成(pending->resolved, pending->rejected) // 取出promise的值 const b = await p console.log(a,b) } f() </script>
async函数内部的执行流程
在执行async函数(设名为asyncF)时,进入函数内部:
- 按序执行同步代码 - 遇到await,跳出asyncF函数,
继续执行后续代码。
当await后的异步代码执行完成之后,接着执行asyncF中的后续代码。
const p1 = new Promise((resolve, reject) => { setTimeout(()=>resolve(100), 3000) }) async function f () { console.log('开始执行f') var a = await p1 console.log('p1完成了') console.log(a) return 1 } console.log('主函数运行...') const res = f() console.log('f()完成,返回值是', res) res.then(res => { console.log(res) })
执行结果是:
主函数运行... 开始执行f f()完成,返回值是 Promise { <pending> } ----此处等了3s---- p1完成了 100 1
示例
const p1 = new Promise((resolve, reject) => { setTimeout(() => resolve(100), 3000) }) const p2 = new Promise((resolve, reject) => { setTimeout(() => resolve(200), 5000) }) async function f () { console.log('开始执行f') var a = await p1 console.log('p1完成了') var b = await p2 console.log('p2完成了') return a + b } console.log('主函数运行...') const res = f() console.log('f()完成,返回值是', res) res.then(res => { console.log(res) })
输出结果
主函数运行... 开始执行f f()完成,返回值是 Promise { <pending> } ----此处等了3s---- p1完成了 ----此处等了2s---- p2完成了 300
回调地狱-用async来优化
事件循环
事件循环 浏览器本身是一个复杂的系统,它要做的事情非常多,例如: 执行js代码,请求图片资源,解析css,渲染页面,响应鼠标的点击等等。在实现层面,浏览器内部会用不同的功能模块去完成不同的事情。这些不同的模块就体现为进程。
进一步把进程进行划分:
- 主进程。用来协调控制其他子进程。
- GPU进程。用于3D绘制等。
- 渲染进程。就是我们说的浏览器内核,负责具体页面的渲染,脚本执行,事件处理等。每个tab页背后就有一个渲染进程。
进程这个单位还是比较大,它进一步拆分多个线程。可以理解为一个页面上的事还是比较多,要多找些小弟来完成。具体来说,一个渲染进程包括:
- 主线程。统一调度
- GUI渲染线程。负责渲染页面,布局和绘制。与JS引擎互斥。
- JS引擎线程。负责处理解析和执行javascript脚本程序。
- 事件触发线程。用来控制事件循环(鼠标点击、setTimeout、ajax等)。当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中。
- setInterval与setTimeout所在的线程。定时任务并不是由JS引擎计时的,是由定时触发线程来计时的。计时完毕后,通知事件触发线程
- 异步http请求线程。浏览器有一个单独的线程用于处理AJAX请求,当请求完成时,若有回调函数,通知事件触发线程。
- io线程。用来接收其他进程的消息。
每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是消息队列和事件循环。
任务有很多,人只有一个,且任意时刻只能做一件事(不是一边走路一边听课这种事哈),那怎么办,就是排队呗
eventLoop
- 主线程上要做很多事情,例如:js代码执行,页面布局计算,渲染等
- 主线程同一时刻只能做一件事,事情多了就要排队。所以主线程维护了任务队列。
- 某个事件发生时,事件触发线程 就把对应的任务添加到主线程的任务队列中。
- 主线程上的任务完成之后,就会从任务队列中取出任务来执行。
任务是以事件及其回调的方式存在的。当事件(用户的点击,图片的成功加载)发生时,将其回调添加到任务队列;主线程上的任务完成之后,就会从任务队列中取出任务来执行,此过程不断重复从而形成一个循环,称为eventLoop。
要点回顾:
- 事件循环不是js的语言层面的内容,是js的宿主环境(浏览器,nodeJS)的讨论内容。在js代码中讨论事件循环是没有意义的。
- 在更广的领域。事件循环是一个典型的生产者/消费者模型。异步I/O,网络请求是事件的生产者,源源不断提供事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。
- 在windows下,这个循环基于IOCP创建,而在*nix下基于多线程创建。
微任务和宏任务
为什么任务要分为同步任务和异步任务
试想一下,如果js的任务都是同步的,那么遇到定时器、网络请求等这类型需要延时执行的任务会发生什么?
页面可能会瘫痪,需要暂停下来等待这些需要很长时间才能执行完毕的代码
所以,又引入了异步任务。
- 同步任务:同步任务不需要进行等待可立即看到执行结果,比如console
- 异步任务:异步任务需要等待一定的时候才能看到结果,比如setTimeout、网络请求
宏任务和微任务
异步任务,又可以细分为宏任务和微任务。下面列举目前学过的宏任务和微任务。
任务(代码) | 宏/微 任务 | 环境 |
script | 宏任务 | 浏览器 |
事件 | 宏任务 | 浏览器 |
网络请求(Ajax) | 宏任务 | 浏览器 |
setTimeout() 定时器 | 宏任务 | 浏览器 / Node |
fs.readFile() 读取文件 | 宏任务 | Node |
Promise.then() | 微任务 | 浏览器 / Node |
console.log('1') new Promise((resolve, reject) => { resolve('2') }).then((res) => { console.log(res) }) setTimeout(() => { console.log('3') }) new Promise((resolve, reject) => { resolve('4') }).then((res) => { console.log(res) }) console.log('5')
1
- 先执行同步代码
- 遇到宏任务,放入队列
- 遇到微任务,放入微任务队列
- 执行栈为空
-
- 将微任务入栈执行
- 所有的微任务完成之后,取出宏任务队列来执行
面试题分析
- 1
console.log(1) setTimeout(function() { console.log(2) }, 0) const p = new Promise((resolve, reject) => { resolve(1000) }) p.then(data => { console.log(data) }) console.log(3)
1,3,1000,2
- 2
console.log(1) setTimeout(function() { console.log(2) new Promise(function(resolve) { console.log(3) resolve() }).then(function() { console.log(4) }) }) new Promise(function(resolve) { console.log(5) resolve() }).then(function() { console.log(6) }) setTimeout(function() { console.log(7) new Promise(function(resolve) { console.log(8) resolve() }).then(function() { console.log(9) }) }) console.log(10)
1,5,10,6,2,3,4,7,8,9
- 3
console.log(1) setTimeout(function() { console.log(2) }, 0) const p = new Promise((resolve, reject) => { console.log(3) resolve(1000) // 标记为成功 console.log(4) }) p.then(data => { console.log(data) }) console.log(5)
1,3,4,5,1000,2
- 4
new Promise((resolve, reject) => { resolve(1) new Promise((resolve, reject) => { resolve(2) }).then(data => { console.log(data) }) }).then(data => { console.log(data) }) console.log(3)
3,2,1
- 5
setTimeout(() => { console.log(1) }, 0) new Promise((resolve, reject) => { console.log(2) resolve('p1') new Promise((resolve, reject) => { console.log(3) setTimeout(() => { resolve('setTimeout2') console.log(4) }, 0) resolve('p2') }).then(data => { console.log(data) }) setTimeout(() => { resolve('setTimeout1') console.log(5) }, 0) }).then(data => { console.log(data) }) console.log(6)
2,3,6,p1,p2,1,4,5
- 6
<script> console.log(1); async function fnOne() { console.log(2); await fnTwo(); // 右结合先执行右侧的代码, 然后等待 console.log(3); } async function fnTwo() { console.log(4); } fnOne(); setTimeout(() => { console.log(5); }, 2000); let p = new Promise((resolve, reject) => { // new Promise()里的函数体会马上执行所有代码 console.log(6); resolve(); console.log(7); }) setTimeout(() => { console.log(8) }, 0) p.then(() => { console.log(9); }) console.log(10); </script> <script> console.log(11); setTimeout(() => { console.log(12); let p = new Promise((resolve) => { resolve(13); }) p.then(res => { console.log(res); }) console.log(15); }, 0) console.log(14); </script>