线程和异步
进程
当一个应用程序运行时,需要使用内存和 CPU 资源,这些资源需要向操作系统申请。
操作系统以进程的方式来分配这些资源,一个进程就代表着一块独立于其他进程的内存空间。
一个应用程序要运行,必须至少有一个进程启动。
进程的最大特点是独立,一个进程不能随意的访问其他进程的资源。这就保证了多个程序在操作系统上运行互不干扰。
线程
任何一个进程在启动的时候,操作系统都会给其分配一个线程,应用程序的入口函数在主线程中运行。
在应用程序的运行过程中,可能有多个任务需要同时执行,于是可以向操作系统申请分配更多的线程来执行不同的任务。
比如,浏览器启动后,会开启多个线程来处理不同的事情。
两者区别:
和进程不一样的是,线程之间的资源不是隔离的,它们可以共享数据,并且线程可以被调度。
比如浏览器中的执行线程和 GUI 线程就是被调度为互斥的,当 GUI 线程执行渲染时,执行线程会被阻塞,反之亦然。所以在下面的代码中你是看不到元素内容被改变的:
<h1 id="title">Monica</h1>
<button onclick="test()">click me</button>
<script>
function test() {
title.innerHTML = "莫妮卡";
while (true) {}
}
</script>
<!--点击之后JS执行引擎在执行,test()运行,下一步才渲染。但是进入了死循环,执行线程结束不了了-->
我们所说的「JS 中是单线程」的语言,是指在宿主环境中,执行 JS 代码的线程只有一个,不代表宿主环境是单线程
为什么要互斥?先执行完后渲染,效率高
为什么渲染的时候不能执行 JS 代码?执行混乱
异步
单线程的主要优势是不需要考虑线程调度,降低了程序的复杂性
但在单线程中如果要处理需要等待的任务时,就必须要考虑阻塞的问题。
考虑下面的伪代码:
var dom = document.getElementById("name"); // 获取某个dom元素
var name = syncConnect("http://server/getname"); // 以同步的方式向服务器获取名字
dom.innerHTML = name;
otherTask(); // 其他无关任务
因此,JS 引入异步来处理该问题
var dom = document.getElementById("name"); // 获取某个dom元素
asyncConnect("http://server/getname", function callback(result) {
//以异步的方式向服务器获取名字
dom.innerHTML = result;
});
otherTask(); // 其他无关任务
执行栈
要想执行必须有执行上下文
JS 执行引擎只会执行栈顶端的东西
实例 1
function A() {
console.log("A"); // 函数调用,新建log上下文,入栈,执行完出栈
B(); // 建立B的上下文
}
function B() {
console.log("B"); // log上下文
}
A(); // 创建A的上下文,入栈。JS执行引擎只会执行栈顶端的东西,所以不执行下一句,而是执行A
console.log("global");
// 答案:A B global
事件循环
事件循环是 JS 处理异步函数的具体方法
具体的做法是:
- 执行 执行栈 中的代码
- 遇到一些特殊代码交给浏览器的其他线程处理
- 将执行栈中的代码全部执行完毕
- 从事件队列中取出第一个任务放入执行栈,然后重复第 1 步
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:
- 宏任务(队列):macroTask,计时器结束的回调、事件回调、http 回调等等绝大部分异步函数进入宏队列
- 微任务(队列):microTask,Promise.then, MutationObserver
当执行栈清空时,JS 引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。
<div id="app"></div>
<script>
var app = document.getElementById("app");
setTimeout(function B() {
console.log(1);
app.innerHTML = "1";
});
var observer = new MutationObserver(function A() {
// 该函数将在微队列中
console.log(2);
setTimeout(function C() {
console.log(3);
});
});
observer.observe(app, { childList: true }); // 观察 app 的变化
app.innerHTML = "2";
// 2 1 2 3 3
</script>
面试题
- 怎样理解 JS 的异步?(必考)JS 是一个单线程的语言,意味着宿主仅为其分配了一个执行线程。而在实际的开发中,JS 有时需要执行一些耗时的操作,比如等待一个 DOM 事件发生、等待网络通信完成、等待计时结束等等。如果在执行线程上去等待,就浪费线程的宝贵执行时间,阻塞后续操作。更可怕的是,由于浏览器的GUI 线程和 JS 执行线程是互斥的,这就导致浏览器界面会因为 JS 的等待处于卡死状态。因此,JS 通过异步来解决这个问题,当需要等待的时候,通知宿主的其他线程去做处理,执行线程则继续后续执行。当其他线程完成处理后,会发出通知,此时执行线程转而去执行事先定义好的回调函数即可。异步的方式充分了解放了执行线程,让执行线程可以毫无阻塞的运行,也就避免了浏览器宿主因为等待操作完成出现的卡死现象。
- 下面的哪个函数执行会导致报错?如果报错,会报什么错误?为什么会出现这种情况?
function A() {
A();
}
function B() {
var n = 0;
while (n >= 0) {
n++;
}
}
// 执行上下文 A Maximum call stack size exceeded 栈溢出
// B 死循环 不报错 栈空间不变化,一直是B
下面的代码输出结果是什么?
setTimeout(function func1() {
// func1先进入事件队列
console.log(1);
a();
}, 0);
function a() {
setTimeout(function func2() {
console.log(2);
}, 0);
console.log(3);
}
a();
console.log(4);
// 3 4 1 3 2 2
!考点:GUI 线程怎么渲染的?
ES6 异步机制
面试常考:JS 基础里面的事件循环 原形原型链 作用域链
异步处理的演化
JS 实现异步的代码模型主要依托于回调
dom.addEventListener(
"click",
function (e) {
// 回调函数作为第二个参数传递,函数可接收一个参数
},
{}
);
dom.onclick = function (e) {
// 回调函数作为属性传递,函数可接收一个参数
};
fs.readFile("./txt", function (err, buffer) {
// 回调函数作为最后一个参数传递,函数可接收两个参数
});
这种设计实际上是有缺陷的
- 没有统一的标准
- 容易陷入回调地狱(callback hell)
/*
异步任务:依次发送7次网络请求,拿到服务器数据
*/
asyncConnect("地址1", (resp1) => {
// to do something
asyncConnect("地址2", (resp2) => {
// to do something
asyncConnect("地址3", (resp3) => {
// to do something
asyncConnect("地址4", (resp4) => {
// to do something
asyncConnect("地址5", (resp5) => {
// to do something
asyncConnect("地址6", (resp6) => {
// to do something
asyncConnect("地址7", (resp7) => {
// to do something
});
});
});
});
});
});
});
后来,JS 社区提出了Promise A+
规范,希望把异步规范化,并消除回调地狱
再后来,ES6 官方标准中提出了 Promise API
来处理异步,它满足Promise A+
规范
由于异步处理变得标准了,就给 ES 官方提供了进一步改进的空间,于是在 ES7 中出现了新的语法async await
,它更加完美的解决了异步处理问题
Promise 的概念
一个 promise 就是一个对象,它表示一个异步任务
异步任务内部保存了它的进展状态,规范中约定有三种状态,不同的状态属于不同的阶段
状态的转换
任务开始时,始终处于未决阶段的挂起状态
任务在未决阶段的时候,有能力将其推向已决。比如,当从服务器拿到数据后,我们就从未决阶段推向已决的 resolved 状态,如果网络不好,导致出错了,我们就从未决阶段推向已决的 rejected 状态
我们把从未决推向已决的 resolved 状态的过程,叫做 resolve,从未决推向已决的 rejected 状态的过程,叫做 reject
这种状态和阶段的变化是不可逆的,也就是说,一旦推向了已决,就无法重新改变状态
任务完成后附带的数据
任务从未决到已决时,可能附带一些数据,比如:跑步完成后的用时、网络请求后从服务器拿到的数据
任务已决后(有了结果),可能需要进一步做后续处理
针对 resolved 的后续处理,称之为 thenable,针对 rejected 的后续处理,称之为 catchable
Promise 的基本使用
ES6 提供了一套 API 来适配上面提到的异步模型,这个 API 即Promise
var pro = new Promise((resolve, reject) => {
//未决阶段的代码,这些代码将立即同步执行,表示任务启动后要做的事情
//...
//在合适的时候,将任务推向已决
//resolve(数据):将任务推向resovled状态,并附加一些数据
//reject(数据):将任务推向rejected状态,并附加一些数据
});
应用:Promise A+规范:必须有 then,可以有多个参数
function delay(duration) {
return new Promise((resolve, reject) => {
console.log("开始计时");
setTimeout(() => {
console.log("结束计时");
if (Math.random() < 0.5) {
resolve(123);
} else {
reject(233);
}
}, duration);
});
}
var pro = delay(1000);
pro.then(
(data) => {
console.log("任务完成了", data); // undefined
},
(reason) => {
console.log("任务失败了", reason);
}
);
Promise A+规范没有规定
pro.then((data) => {
console.log("任务完成了", data); // undefined
});
pro.catch((reason) => {
console.log("任务失败了", reason);
});
注意
- 任务一旦进入已决后,所有企图改变任务状态的代码都将失效
- 以下代码可以让任务到达 rejected 状态
- 调用 reject
- 代码执行报错
- 抛出错误
拿到 Promise 对象后,可以通过 then 方法指定后续处理
pro.then(thenable, catchable);
//或
pro.then(thenable);
pro.catch(catchable);
无论是 thenable 还是 catchable,均是下面格式的函数
function (data){
//data为状态数据
}
注意:后续处理函数一定是异步函数,并且放到微队列中
面试题
- 下面代码输出什么?
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve("a");
resolve("b");
reject("c");
console.log(2);
});
promise.then(
(data) => {
console.log(data);
},
(resean) => {
console.log(reason);
}
);
console.log(4);
// 1 2 4 a
- 下面的代码输出什么?
setTimeout(() => {
console.log(1);
});
var pro = new Promise((resolve, reject) => {
console.log(2);
resolve(3);
console.log(4);
throw 5; // 状态不能变,但是依旧执行此代码,即报错,导致后面代码不走了
console.log(6); // 不走了
});
console.log(7);
pro.then(
(data) => {
console.log(data);
},
(reason) => {
console.log(reason);
}
);
// 2 4 7 3 1
更多知识
Promise 是可以链式调用的
var pro1 = ...; // pro1 是一个异步任务,它完成后会得到一个数字3
pro1
.then(n=>{
console.log(n); // 输出:3
return n * 2;
})
.then(n=>{
console.log(n); // 输出:6
return n * 2;
})
.then(n=>{
console.log(n); // 输出:12
})
demo
var pro = new Promise((resolve, reject) => {
resolve(1);
});
pro
.then((d) => {
console.log(d);
return 2;
})
.then((d) => {
//这个d取决于上一个的返回结果
console.log(d);
return 3;
})
.then((d) => {
console.log(d);
});
如果上一个返回的是一个 Promise 呢
var pro = new Promise((resolve, reject) => {
resolve(1);
});
pro
.then((d) => {
console.log(d);
return new Promise((resolve, reject) => {
resolve(5);
}); //相当于返回的此Promise
})
.then((d) => {
console.log(d);
return 3;
})
.then((d) => {
console.log(d);
});
/*
异步任务:依次发送7次网络请求,拿到服务器数据
*/
asyncConnect("地址1", (resp1) => {
// to do something
asyncConnect("地址2", (resp2) => {
// to do something
asyncConnect("地址3", (resp3) => {
// to do something
asyncConnect("地址4", (resp4) => {
// to do something
asyncConnect("地址5", (resp5) => {
// to do something
asyncConnect("地址6", (resp6) => {
// to do something
asyncConnect("地址7", (resp7) => {
// to do something
});
});
});
});
});
});
});
/*
异步任务:依次发送7次网络请求,拿到服务器数据
*/
asyncConnect("地址1")
.then((resp) => {
// to do something
return asyncConnect("地址2"); // 返回新的Promise
})
.then((resp) => {
// to do something
return asyncConnect("地址3");
})
.then((resp) => {
// to do something
return asyncConnect("地址4");
})
.then((resp) => {
// to do something
return asyncConnect("地址5");
})
.then((resp) => {
// to do something
return asyncConnect("地址6");
})
.then((resp) => {
// to do something
return asyncConnect("地址7");
})
.then((resp) => {
// to do something
});
如果使用ES7
的async
和await
,代码会更加优雅,消除了回调
/*
异步任务:依次发送7次网络请求,拿到服务器数据
*/
async function doRequest() {
var resp1 = await asyncConnect("地址1");
// to do something
var resp2 = await asyncConnect("地址2");
// to do something
var resp3 = await asyncConnect("地址3");
// to do something
var resp4 = await asyncConnect("地址4");
// to do something
var resp5 = await asyncConnect("地址5");
// to do something
var resp6 = await asyncConnect("地址6");
// to do something
var resp7 = await asyncConnect("地址7");
// to do something
}
简化
function asyncConnect(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(Math.random());
}, Math.floor(Math.random() * 3000));
});
}
async function doRequest() {
var urls = ["地址1", "地址2", "地址3"];
for (var url of urls) {
var resp = await asyncConnect(url);
console.log(resp);
}
}
快手试题
- 下面的代码输出什么
var a;
var b = new Promise((resolve, reject) => {
console.log("promise1");
setTimeout(() => {
resolve(); //1000ms后推向成功
}, 1000);
})
.then(() => {
console.log("promise2");
})
.then(() => {
console.log("promise3");
})
.then(() => {
console.log("promise4");
});
a = new Promise(async (resolve, reject) => {
console.log(a);
await b; //等待b,a的赋值不会被卡住 这个代码相当于b.then(()=>{下面一驼}),后面变成异步了
console.log(a); //pending等待着
console.log("after1");
await a;
resolve(true);
console.log("after2");
});
console.log("end");
/*
a undefined
b 是所有then调用完返回的Promise .promise234放入微队列,b是pending
开始a赋值,因为赋值给a的Promise还没运行,输出a:undefined,等待b,a运行结束.此时a变成Promise:pending
输出end, 输出微队列内容,promise234, b完成,a里面的await结束了,输出a,a还是pending,后after1, a永远完不成了(因为要等a完成后才能完成的悖论)
*/
- 下面的代码输出什么?
async function async1() {
console.log('async1 start');
await async2();//等待
console.log('async1 end');//进入微队列
}
async function async2() {//有async 返回的一定是promsie
console.log('async2');
}
// 两个函数没有执行先不看
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
Script start
Async1 start
Async2
Promise1
Script end
宏队列:setTimeout
微队列:async1end promise2
高仿 setTimeout
function delay(duration) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
}
async function test() {
console.log(1);
await delay(1000);
console.log(2);
await delay(1000);
console.log(3);
}
test();
高仿 setInterval
function delay(duration) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
}
async function test() {
var count = 0;
while (true) {
await delay(1000);
console.log(count++);
}
}
test();
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve("a");
resolve("b");
reject("c");
console.log(2);
});
promise.then(
(data) => {
console.log(data);
},
(resean) => {
console.log(reason);
}
);
console.log(4);
setTimeout(() => {
console.log(1);
});
var pro = new Promise((resolve, reject) => {
console.log(2);
resolve(3);
console.log(4);
throw 5; // 相当于 reject(5), 但是throw会中断当前函数的执行
console.log(6);
});
console.log(7);
pro.then(
(data) => {
console.log(data);
},
(reason) => {
console.log(reason);
}
);