讲解
线程和进程
概念
进程与线程
进程:程序的一次执行,占有一次独立的内存区域
线程:CPU的基本调用单位,是程序执行的一个完整流程
关系:一个进程至少拥有一个运行的线程(主线程),一个进程可以用多个线程,多个进程数据是不共享。
JavaScript
js一门单线程的非阻塞的脚本语言
单线程:需要一个任务执行完成后才会去执行另外一个任务。
非阻塞:实现方式是通过异步的方式来实现的,比如说:发现当我们使用定时器的时候也并没有影响后面的任务。主要实现方式是通过异步来实现的。
看一下代码的执行结果:
const f2 = async () => {
console.log('f2');
};
const f1 = async () => {
await f2();
console.log('f1');
};
console.log('正常1');
f1();
setTimeout(() => {
console.log('定时器');
});
console.log('正常2');
// 正常1 f2 正常2 f1 定时器
为什么js要设计成单线程的语言呢?
我们从另外的角度考虑一下,要是js是一个多线程的语言。一个线程为一个dom添加了一个点击事件,但是另外的一个线程删除了这个dom 节点。那就会有很多莫名其妙的问题,靠人为的保证并不可靠。因此为了避免这个问题js只用一个主线程来执行代码,保证了程序的一致性。
为什么js要设计成单线程的语言呢?
执行栈与事件队列
执行栈:当我们调用一个方法的时候,js会生成一个和这个方法对应的执行环境(context),又被叫做执行上下文。
执行环境中包含这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域的变量以及这个作用域的this对象。当一系列方法被依次嗲用的时候,因为js是单线程,同一时间只能执行一个方法,于是这些方法被排列在一个地方,这个地方就是执行栈。
事件队列:当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码安在执行顺序放到执行栈中,然后从头开始执行。
同步方法执行
当执行的是一个方法的时候,那么js会向这个执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。等这个执行环境中的代码执行完毕并返回结果后,js就会退出这个执行环境并销毁,回到上一个方法的执行环境,这个过程反复进行,直到执行环境中的代码全部执行完成,一个方法执行会向执行环境添加这个方法的执行环境,这个执行环境可以调用其他方法,从结果来看就是向执行环境中添加一个执行环境。这个过程可以无限的执行下去,除非栈溢出(超过了内存的最大值)
异步方法执行
js引擎遇到一个异步时间的时候, 并不会一直等待返回结果,而是先将这个事件挂起,继续执行执行栈中的其他任务,当第一个异步返回结果的时候,js会将这个事件加入到与当前执行栈不同的另外一个队列,我们称为 事件队列。
事件循环
被放在事件队列中并不会立即执行回调,而是等待当前执行栈中的所有任务都执行完毕,主线程处于空闲的状态的情况下,主线程会查看事件队列中是否还存在任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件放在回调执行栈中,然后执行代码,如此反复,形成了一个无限循环就是我们说的事件循环(event loop)
setTimeout(() => {
console.log('3');
}, 0);
Promise.resolve(2).then((value) => console.log(value));
console.log("1")
// 1 2 3
任务
宏任务(macro-task):发起者是宿主(node、浏览器)
- script
- setTimeout
- setInterval
- UI事件
- I/O (nodejs)
- ajax 微任务(micro-task):发起者是Js引擎
- Promise
- Mutation
微队列优先级是高于宏队列优先级
setTimeout(() => {
console.log('4');
}, 0);
setTimeout(() => {
console.log('5');
}, 0);
Promise.resolve(2).then((value) => console.log(value));
Promise.resolve(3).then((value) => console.log(value));
console.log('1');
// 1 2 3 4 5
setTimeout(() => {
console.log('4');
Promise.resolve(3).then((value) => console.log(value));
}, 0);
setTimeout(() => {
console.log('5');
}, 0);
Promise.resolve(2).then((value) => console.log(value));
console.log('1');
// 1 2 4 3 5
这两端代码执行结果的差异是因为,每执行一个宏任务,都会检查微队列中是否有待执行的回调,优先执行微任务,简言之,微任务是可以插队的。
async方法执行时,遇到await会立即执行表达式,async表达式定义的函数是立即执行的,await表达式后面的代码放在微任务执行,包括赋值。
例子(实战)
一些简单的小代码验证我们的学习成果
const test2 = async () => {
console.log('test2');
};
const test1 = async () => {
console.log('test1 begin');
await test2();
console.log('test1 end');
};
console.log('script begin');
test1();
console.log('script end');
结果:
console.log(1)
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {console.log(3)});
});
new Promise((resolve,reject)=>{
console.log(4);
resolve(5)
}).then(data=>{console.log(data)})
setTimeout(() => {
console.log(6);
});
console.log(7);
// 1 4 7 5 2 3 6
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 10个 10
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 0 1 2 3 4 5 6 7 8 9
使用立即执行函数的方法来避免这个问题
for (var i = 0; i < 10; i++) {
((i)=>{setTimeout(() => {
console.log(i);
}, 0)})(i);
}
// 0 1 2 3 4 5 6 7 8 9
分析:尽管循环执行结束,i值已经变成了3。但因遇到了自执行函数,当时的i值已经被 lockedIndex锁住了。也可以理解为 自执行函数属于for循环一部分,每次遍历i,自执行函数也会立即执行。所以尽管有延时器,但依旧会保留住立即执行时的i值。
最后写一个长一点的小例子
const async2 = () => {
console.log('async2');
};
const async1 = async () => {
console.log('async1 start');
await async2();
console.log('async1 end');
};
console.log('script start');
setTimeout(function () {
console.log('settimeout');
});
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve(1);
}).then(function () {
console.log('promise2');
});
console.log('script end');