1 单线程
Javascript语言的执行环境是"单线程"(single thread),一个任务完成之后才能执行另一个任务。
- 好处:实现起来比较简单,执行环境相对单纯。
- 坏处:只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:
- 同步(Synchronous)
- 异步(Asynchronous)
2 同步与异步
2.1 同步与异步的区别
2.1.1 同步
- 同步:同步程序从上到下按顺序执行。
console.log(1); console.log(2); console.log(3); //output:1 2 3
2.1.2 异步
- 异步:先执行一部分,等拿到结果/到时间了再执行后续代码。
异步指两个或两个以上的对象或事件不同时存在或发生。
电子邮件就是一种异步通信方式;发送者发送了一封邮件,接收者会在方便时读取和回复该邮件,而不是马上这样做。双方可以继续随时发送和接收信息,而无需双方安排何时进行操作。setTimeout(() => {console.log(1)}, 1000) setTimeout(() => {console.log(1)}, 100) setTimeout(() => {console.log(1)}, 10) //output:10 100 1000
- 常见的异步程序:
- 计时器(
SetTimeout
,SetInterval
) - ajax
- 在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
- 在软件进行异步通信时,一个程序可能会向另一软件(如服务器)请求信息,并在等待回复的同时继续执行其他操作。例如,AJAX(Asynchronous JavaScript and XML)编程技术(现在的应用不常用XML,而是用JSON)。就是这样一种机制,它通过HTTP从服务器请求较少的数据,当结果可被返回时才返回结果,而非立即返回。
- 读取文件 在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。例如读取文件。
- 计时器(
2.1.3 setTimeout
setTimeout的第三个参数:定时器启动时候,第三个以后的参数作为第一个func()
的参数传进去。例:
www.runoob.com/try/try.php…
2.2 同步异步先执行哪个?
同步程序执行完成后,执行异步程序。
console.log(1);
for (let i=0; i<2000; i++) {
console.log(1);
}
setTimeout(() => {console.log(2)}, 0)
setTimeout(() => {console.log(3)}, 0)
console.log(4);
//output:[1]*2000 4 2 3
//需要等到2000个1全都输出之后,4也输出了,才能输出2,3。
3 process.nextTick与setImmediate方法
setImmdiate(() => {
console.log(1)
})
process.nextTick(() => {
console.log(2)
})
console.log(3);
setTimeout(() => {console.log(4)},0)
setTimeout(() => {console.log(5)},1000)
setTimeout(() => {console.log(6)},0)
console.log(7);
//output: 3 7 2 4 6 1 5
执行顺序:
同步
→ nextTick
→ 异步
→ setImmediate
(当前
事件循环结束,则执行)
4 浏览器的事件循环机制
JavaScript 是单线程的,但在实际开发中确实需处理一些异步的问题,那就要求 JavaScript 的运行环境来提供一套方案让我们更好的处理一些异步问题,在前端层面,浏览器是 JavaScript 唯一的运行环境,这就有了浏览器的事件循环机制。
4.1 宏任务与微任务
异步
的程序可以分为宏任务和微任务
4.1.1 宏任务
- 宏任务:JavaScript 是单线程,但浏览器是多线程的,JavaScript 执行在浏览器中,在 V8 里跑着的一直是一个一个的宏任务,就相当于排队打饭一样,一个人相当于一个宏任务。
- 宏任务包括:
script 整体代码
、setTimeout
、setInterval
、setImmediate
、Ajax
、DOM事件
- 案例:
浏览器在执行上面代码时会先执行主线程代码(宏任务 1)然后再执行
setTimeout
里面的代码。虽然setTimeout
的定时时间为 0,但是浏览器在处理的时候会把它当做下一个宏任务进行处理,定时器也是宏任务的典型代表。
- 宏任务包括:
4.1.2 微任务
- 微任务:当浏览器执行完一个宏任务后,就会看看有没有微任务执行,如果有微任务执行就会先把当前的微任务执行完,再去执行下一个宏任务。
process.nextTick
、MutationObserver
、Promise.then
catch finally
执行顺序:
同步-> process.nextTick -> 微任务 -> 宏任务 -> setImmidiate
虽然JS是单线程的,但是可以通过事件循环
和异步
,解决并发问题。
4.2 运行栈与任务队列
同步
的程序会放到运行栈里执行。异步
的程序放到任务队列里。异步的程序等时间到了的时候,放进任务队列里(不是放到队里就立刻执行,要等到运行栈里的程序执行完了,再执行任务队列里的程序)。- 事件循环不断循环检测任务队列里是否有任务,若有任务就(按照顺序)执行,没有任务,就一直循环。
主线程从 "任务队列" 中读取事件,这个过程是循环不断的,所以整个过程的这种运行机制又称为 Event Loop(事件循环)。案例:
console.log(1);
setTimeout(() => {
console.log(2);
});
new Promise((res, req) => {
console.log(3);
res();
}).then(() => {
console.log(4);
});
console.log(5);
//1 3 5 4 2
执行顺序:
代码从上到下执行➡打印 1➡遇到 setTimeout 是下一个宏任务,目前先不处理➡遇到 Promise 打印出 3 then 回调函数是微任务,先不处理➡打印 5 且第一个宏任务执行完毕➡开始执行微任务队列➡打印 4 微任务队列执行完毕➡开始执行下一个宏任务➡打印 2➡程序结束
注:只有 Promise then
或者 catch
里面的方法是微任务,Promise 里面的回调是当作主程序的宏任务进行处理的。Promise 新建后就会立即执行。
5 Promise对象、回调地狱、async、await函数
5.1 Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。
Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
- 优点:
- 有了
Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。 Promise
对象提供统一的接口,使得控制异步操作更加容易。
- 有了
- 缺点:
- 无法取消
Promise
,一旦新建它就会立即执行,无法中途取消。 - 如果不设置回调函数,
Promise
内部抛出的错误,不会反应到外部。 - 当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
- 无法取消
5.1.1 Promise用法模板
Promise
对象是一个构造函数,用来生成Promise
实例。
const promise = new Promise(function(resolve,reject) {
if(/*异步程序成功*/){
resolve(res)
} else {
reject(error)
}
})
resolve
和reject
是两个函数,由 JavaScript 引擎提供,不用自己部署。resolve
:将Promise
对象的状态从从 pending 变为 resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
:将Promise
对象的状态从从 pending 变为 rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise
实例生成以后,可用then
方法分别指定resolved
状态和rejected
状态的回调函数:
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then
方法可以接受两个回调函数作为参数:
Promise
对象的状态变为resolved
时调用第一个回调函数Promise
对象的状态变为rejected
时调用第二个回调函数 这两个函数都是可选的,不一定要提供。它们都接受Promise
对象传出的值作为参数。
5.1.2 什么时候执行then
let p = new Promise(() => {
console.log(1)
})
p.then(()=> {
console.log(2)
})
//output:1 没有2
上面的代码没有输出2,then方法没有执行。那么,什么时候执行then方法?
调用resolve的时候才会执行then。resolve传递出来的值,是then的形参。resolve可以将异步数据传递出来。通过then方法可以拿到异步数据。
以下代码执行了then方法:
let p = newe Promise((resolve) => {
resolve("1");
})
p.then((data)=> {
console.log(data)
})
//output: 1
resolve函数的作用是,将Promise对象的状态从 pending 变为 resolved,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。💞💞💞
5.1.3 Promise 的方法
Promise 有几个比较重要的方法,如下所示:
方法 | |
---|---|
Promise.prototype.then() | Promise 实例添加状态改变时的回调函数,then 方法返回的是一个新的Promise 实例 |
Promise.prototype.catch() | 发生错误时的回调函数 |
Promise.all() | Promise.all 可以将多个 Promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值 |
Promise.race() | 可以将多个 Promise 实例包装成一个新的 Promise 实例,哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态 |
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
上面代码中,第二种写法要好于第一种写法,第二种写法可以捕获前面then
方法执行中的错误,也更接近同步的写法(try/catch
)。因此,建议总是使用catch()
方法,而不使用then()
方法的第二个参数。
5.2 回调地狱与Promise对象
5.2.1 异步编程的解决方案
Promise
是异步编程的一种解决方案,在没有 Promise
之前,只能通过回调的方式实现异步编程:
function fn(name, fn) {
const nameVal = "我是" + name;
// 用定时器模拟异步执行
setTimeout(function () {
fn(nameVal);
}, 1000);
}
fn("张三", function (val) {
console.log(val); // 我是张三
});
但是如果回调函数比较多的话,就会陷入回调地狱,大大降低了代码的可读性。而 Promise
就是来解决这个问题的。
5.2.2 什么是回调地狱
//获取奶茶的方法
function getTea(fn){
setTimeout(() => {
fn("奶茶")
},1000)
}
//获取火锅的方法
function getHotpot(fn){
setTimeout(() => {
fn("火锅")
},2000)
}
//调用获取奶茶的方法
getTea(function(data)){
console.log(data)
}
//调用获取火锅的方法
getHotpot(function(data)){
console.log(data)
}
先输出奶茶还是火锅,要看settime的时间哪个更短。 如果想吃火锅→喝奶茶这种先后顺序,该怎么办?
//调用获取火锅的方法 外层吃火锅
getHotpot(function(data)){
console.log(data)
//调用获取奶茶的方法 内层喝奶茶
getTea(function(data)){
console.log(data)
}
}
获取异步程序的数据
,不能用return
,要用回调
函数取数据,而要想按照某个顺序执行,势必就要一层一层的嵌套,而如果调用的方法很多,嵌套的层数很多,代码维护起来就很困难,这就叫回调地狱。
5.2.3 Promise
解决回调地狱
通过Promise改造代码,不会出现回调地狱。先定义两个函数,返回的都是一个Promise对象。
function getTea(){
return new Promise(function(resolve){
setTimeout(() => {
resolve("奶茶")
},1000)
})
}
function getHotpot(){
return new Promise(function(resolve){
setTimeout(() => {
resolve("火锅")
},2000)
})
}
链式操作:
//先吃火锅 再喝奶茶
getHotpot().then(function(data){
console.log(data);
return getTea();
}).then(function(data){//第二个then()调的第二个then的返回值
console.log(data);
})
利用async await 更简洁:
async function getData(){
let hotPot = await getHotpot();
console.log(hotPot);
let tea = await getTea();
console.log(tea)
}
getData();
5.3 async 与 await
5.3.1 async
Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了:
async相当于一个Promise对象的简写:
async function fun() {
return 1
}
//等效于:
function fun() {
return new Promise((resolve) => {
resolve(1);
})
}
async函数的返回值是Promise对象:
async function fun() {
return 1
}
let a = fun();
console.log(a);
//output: Promise {1}
fun().then((data) => {
console.log(data);
})
//output: 1
5.3.2 await
await
后跟Promise对象,能直接获取resolve传递出来的异步数据,让异步的代码,写起来更像是同步的代码:
let p = new Promise((resolve => {
resolve(1)
})
let p = new Promise((resolve => {
resolve(2)
})
async function fun() {
let a = await p1;
let b = await p2;
console.log(a);
console.log(b);
}
fun();
//output: 1 2
5.3.3 练习
练习1:
async function fun1() {
let data = await fun2();//await等待then拿到结果后 赋值给data
console.log(data);//看作 then(微任务)中执行的代码
}
async function fun2() {
console.log(200);//同步
return 100
}
fun1();
//output:200 100
练习2:
console.log(1)
aysnc function async1(){
await async2()
console.log(2)
}
async function async2(){
console.log(3)
}
async1()
setTimeout(function () {
console.log(4)
},0)
new Promise(resolve => {
console.log(5)
resolve()
}).then(function () {
console.log(6)
})
.then(function () {
console.log(7)
})
console.log(8)
//output: 1 3 5 8 2 6 7 4
参考
developer.mozilla.org/zh-CN/docs/… www.ruanyifeng.com/blog/2012/1… es6.ruanyifeng.com/#docs/promi…