一.进程和线程
1.进程
- 是cpu资源分配的最小单位,是能拥有资源和独立运行的最小单位。
- 拥有独立的地址空间。
2.线程
- 是cpu调度的最小单位,安排CPU执行的最小单位。
- 同一个进程下的所有线程,共享进程的地址空间。
(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
cpu:“中央处理器(CPU,Central Processing Unit)是一块超大规模的集成电路,是一台计算机的运算核心和控制核心,它的功能主要是解释计算机指令以及处理计算机软件中的数据,计算机的性能在很大程度上由CPU的性能决定。”
3.关系
- 一个进程至少有一个线程(主)
- 一个进程中也可以同时运行多个线程, 我们会说程序是多线程运行的
- 一个进程内的数据可以供其中的多个线程直接共享
- 多个进程之间的数据是不能直接共享的
- 程序是在某个进程中的某个线程执行的(即真正在处理机上运行的是线程)
- 线程是在进程内部工作,而进程负责向外界输出
二.js中的单线程
1.JS为什么是单线程的
这与浏览器的用途有关,JS的主要用途是与用户互动和操作DOM。设想一段JS代码,分发到两个并行互不相关的线程上运行,一个线程在DOM上添加内容,另一个线程在删除DOM,那么会发生什么?以哪个为准?所以为了避免复杂性,JS从一开始就是单线程。
JS的语法单线程
一个语法的问题需要注意,就是JS的语法层面是单线程的,就是不能同时执行
console.log("A AT ", (new Date()).toLocaleTimeString())
alert('a')
console.log("B BT ", (new Date()).toLocaleTimeString())
js的单向数据流,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验
2.JS的同步异步
由于Js的执行机制**(只要主线程空了,就会去读取任务队列)**导致的,js中所有任务可以分成两种
-
同步任务:指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
-
异步任务:指的是,不进入主线程、而进入"任务队列"的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。
同步代码
console.log("A")
console.log("B")
console.log("C")
代码的执行结果,是 A,B,C。这样,就是同步代码,执行顺序与编写顺序保持一致。
对比下面的异步代码:
console.log("A")
// setTimeout 就是异步代码
setTimeout(()=>{
console.log("B")
}, 0)
console.log("C")
代码的执行结果,是 A,C,B。编写顺序上,我们先输出的B,但是执行结果却是先输出的C。本例中,setTimeout 函数就是异步执行,也就是当执行的到 setTimeout 时,内部的代码不会立即执行,而是将其放在异步执行队列中等待执行。同时 setTimeout 后边的代码就开始执行,也就是说输出C,没有等到输出B执行完,就开始执行了。
本例中,setTimeout 函数就是异步执行。
console.log("A AT ", (new Date()).toLocaleTimeString())
// setTimeout 异步,1000ms(1s)后执行输出B和时间
setTimeout(()=>{
console.log("B ", (new Date()).toLocaleTimeString())
}, 1000)
// 大循环,循环很多次,为了保证循环时间大于1000ms
for (let i = 0, num=999999999; i <= num; ++ i) {
if (0 == i) {
console.log("Loop first Run at ", (new Date()).toLocaleTimeString())
}
if (num == i) {
console.log("Loop last Run at ", (new Date()).toLocaleTimeString())
}
}
结果:
A AT 21:57:58
Loop first Run at 21:57:58
Loop last Run at 21:58:01
B 21:58:01
上面的例子中,我们做了一个定时器,异步执行在1000ms后,之后执行一个for循环,很多次循环,执行时间大于了1000ms。从执行结果上看,当达到了1000ms时,并没有立即执行输出B,而是要等到for循环执行完毕后,才会执行已经到达时间的输出B。
异步执行还有一个问题,就是若某些操作需要依赖于异步操作的结果。那如何保证这些操作的执行时机呢? JS提供了多种语法方案供我们使用,例如:事件驱动,Promise,Generator,async,await等
4.异步编程的解决方案
4.1 回调函数
- 把一个函数当参数,传递给另一个函数
回调函数是异步操作最基本的方法
ajax(url, () => {
// 处理逻辑
})
但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
优点:简单、容易理解和实现
缺点:不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况)
4.2 事件监听
- 异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生
下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。首先,为f1绑定一个事件(这里采用的jQuery的写法)
f1.on('done', f2);
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
上面代码中,f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2。
优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。
缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
4.3 发布订阅
- 多个线程互相协作,完成异步任务。
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)
首先,f2向信号中心jQuery订阅done信号。
jQuery.subscribe('done', f2);
然后,f1进行如下改写
function f1() {
setTimeout(function () {
// ...
jQuery.publish('done');
}, 1000);
}
上面代码中,jQuery.publish(‘done’)的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。
f2完成执行后,可以取消订阅(unsubscribe)
jQuery.unsubscribe('done', f2);
这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
4.4 Promises对象
- 是es6新增的一种新的写法,用来解决“回调地狱”的问题
两个特点:
- 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、resolve(fulfilled已成功)和rejected(已失败)
- 一旦状态设定,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了.
new Promise((resolve,reject) => {
setTimeout(() => {
resolve('hello')
}, 2000)
}).then(res => {
console.log(res)
})
new Promise((resolve,reject)=>{
var a = 1;
if(a == 1){
reject('错误')
}
}).catch(res => {
console.log(res)
})
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))
优点:让回调函数变成了规范的链式写法,程序流程可以看的很清楚。他有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到他们的状态都改变以后,在执行一个回调函数。
缺点:编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。
4.5 生成器Generators
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。
特征:
(1)function 关键字和函数之间有一个星号(*),且内部使用yield(返回数据)表达式,定义不同的内部状态。
(2)调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
function *gen() {
for(let i = 0; i < 9999; i++){console.log(yield i)};
yield 10;
yield 20;
return 30;
}
const it = gen();
console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: 30, done: true }
console.log(it.next()); // { value: undefined, done: true }
console.log(10)
console.log(it.next()); // { value: undefined, done: true }
换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段(done为false 继续执行)
普通函数的执行会形成一个调用栈,入栈和出栈是一口气完成的。而Generator必须得手动调用next()才能往下执行,相当于把执行的控制权从引擎交给了开发者。
所以Generator解决的是流程控制的问题。
它可以在执行过程暂时中断,先执行别的程序,但是它的执行上下文并没有销毁,仍然可以在需要的时候切换回来,继续往下执行。
最重要的优势在于,它看起来是同步的语法,但是却可以异步执行。
总结:从上例中我们看出虽然Generator将异步操作表示得很简洁,但是手动迭代Generator
函数很麻烦,实现逻辑有点绕,而实际开发一般会配合 co
库去使用,co库可以简化代码的写法 npm install co
4.6 async/await
- Generator 函数的语法糖。
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
var asyncReadFile = async function (){
var f1 = await readFile('./a.txt');
var f2 = await readFile('./b.txt');
console.log(f1.toString());
console.log(f2.toString());
};
该语法的目的就是定义一个异步执行的函数,内部实现是对Promise的封装
async函数的优点
(1)内置执行器:Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
(2) 语义化更好:async 和 await,比起星号和 yield,语义更清楚了。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
(3) Promise使用then函数进行链式调用,一直点点点,是一种从左向右的横向写法;async/await从上到下,顺序执行,就像写同步代码一样,更符合代码编写习惯, 是对promise的进一步优化,更强大且结构也更加清晰。
5.EventLoop 事件循环
1.所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2.主线程之外,还存在一个任务队列(event queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列(event queue),看看里面有哪些异步任务可以执行,进入执行栈,开始执行。
主线程不断重复上面的第三步。(event loop)
任务队列里面分为***宏任务***和***微任务***,执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去任务队列,遇到Task
(宏任务),先执行宏任务,将宏任务放入宏任务的event queue, 然后在执行微任务,将微任务放入微任务event queue
注意:往外拿的时候,先从微任务里拿这个回调函数,按照先入先出的规则全部执行完微任务(microTask
)后,然后再从宏任务的queue上拿宏任务的回调函数**,**如此循环
宏任务队列可以有多个,微任务队列只有一个。
遇到微任务,放在当前任务列的最底端
遇到宏任务,放在下一个新增任务列的最顶端,宏任务按照秒数先后执行·
微任务 promise async await
宏任务 setTimeout setInterval
console.log("a");
setTimeout(function(){
console.log("b")
},0);
console.log("c");
new Promise(function(resolve,reject){
console.log("d")
resolve();
}).then(function(){
console.log("e");
})//a,c,d,e,b
setTimeout(()=>{
console.log('t1');
Promise.resolve().then(()=>{
console.log('p1');
})
},10)
setTimeout(()=>{
console.log('t2')
Promise.resolve().then(()=>{
console.log('p2')
})
},0)
![image-20210401170523993](/Users/chengxuwen/Library/Application%20Support/typora-user-images/image-20210401170523993.png)
![image-20210401171146003](/Users/chengxuwen/Library/Application%20Support/typora-user-images/image-20210401171146003.png)