一些概念
同步和异步
连续的执行一个任务就叫做同步。不连续的执行一个任务,就叫做异步。
并发(concurrency)和并行(parallelism)
并发是宏观概念,在一段时间内通过任务间的切换完成了多个任务,这种情况就可以称之为并发。
并行是微观概念,假设 CPU 中存在两个核心,那么就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
进程与线程
本质上来说,两个名词都是 CPU 工作时间片的一个描述。
进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。
线程是进程中的更小单位,描述了执行一段指令所需的时间。
在浏览器中来说,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。
锁,假如当我读取一个数字的时候,同时有两个操作对数字进行了加减,这时候结果就出现了错误。解决这个问题只需要在操作的时候加锁,直到操作完毕之前都不能再进行写入操作。
回调
容易造成回调地狱,代码可读性及可维护性变差。
console.log(1111);
function test(cb){
setTimeout(()=>{
console.log("setTimeout");
cb && cb();
},1000)
}
test(function(){
console.log(2222);
});
自定义事件
let eventObj = new EventTarget();
(function t() {
setTimeout(() => {
console.log(1);
eventObj.dispatchEvent(new CustomEvent("myEvent"));
},1000)
})()
eventObj.addEventListener("myEvent", function () {
console.log(2);
})
promise
//Promise 新建后就会立即执行
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve(1);
console.log(2);
});
//Promise
// 2
then的返回值
会返回一个新的Promise 对象, 但是状态会有几种情况
then 的回调函数中没有返回值,返回一个新的resolved状态的Promise对象
then 的回调函数返回值非Promise的值, 返回一个新的resolved状态的Promise对象,并且把返回值,传递给下一个 then
then 的回调函数返回值是Promise对象,then 就直接返回这个Promise对象,具体的状态和值由可以由Promise对象定义
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1);
resolve(2)
}, 1000)
})
p.then((res) => {
console.log(res);
}).then(() => {
console.log('没有return,默认成功');
})
如果直接return的是非Promise的值,会直接执行回调函数
new Promise((resolve, reject) => {
resolve('success1')
}).then(res => {
console.log(res)
return Promise.resolve(res + '111')
}).then(res => {
console.log(res)
})
new Promise((resolve, reject) => {
resolve('success2')
}).then(res => {
console.log(res)
return res + '222'
}).then(res => {
console.log(res) //这个函数会立即执行不会等下次事件循环再执行
})
//success1
//success2
//success2222
//success1111
reject与catch
then 中的 rejected 函数用来捕获最近的异常。而catch捕获整个promise异常
如果rejected状态被 reject函数处理了,不会再进入catch中。如果没有 reject函数会进入catch中。
如果是代码错误,只有catch能处理。reject函数接受不到。
静态方法
resolve
Promise.resolve(2).then(res => console.log(res))
reject
Promise.reject(2).then(()=>{},err=>console.log(err))
all
Promise.all可以将多个Promise实例包装成一个新的Promise实例
接受一个Promise数组,返回一个新的Promise,
如果Promise都成功,按数组顺序返回成功状态,
若有一个失败,返回最先被reject的值。
allSettled
接受一个Promise数组,返回一个新的Promise,
不管成功和失败都会返回在res函数里,数据结构如下
//成功:
{
status: "fulfilled"
value: "success"
}
//失败
{
reason: "err"
status: "rejected"
}
race
返回最快响应的Promise
Generator
每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。
value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;
done属性是一个布尔值,表示是否遍历结束。
调用next方法传入的值表示上一个yield的值。
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let t = foo(5)
console.log(t.next()) // => {value: 6, done: false} 执行x+1=5+1=6
console.log(t.next(12)) // => {value: 8, done: false} 传入的参数12等于yield (x + 1)值,执行y=2*12=24 y/3=24/3=8 如果不传参,`yield` 会返回 `undefined`。会输出NAN.
console.log(t.next(13)) // => {value: 42, done: true} 传入的参数13等于yield (y / 3)值 z=13,之前x=5,y=24,总和42
async及await
同步形式书写代码
try及catch方法捕获错误
let fn1 = function () {
return new Promise(resolve => {
setTimeout(() => {
console.log("fn1");
resolve(111);
}, 2000)
})
}
let fn2 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("fn2");
reject("err222");
}, 1000)
})
}
// fn1().then(res=>{
// console.log(res);
// return fn2();
// }).then(res=>{
// console.log(res);
// },err=>{
// console.log(err);
// })
async function fn() {
try {
let res1 = await fn1();
console.log(res1);
let res2 = await fn2();
console.log(res2);
} catch(e) {
console.log(e);
}
}
fn()
直接return值会被包装成一个promise
async function fn(){
return "value";
}
console.log(fn().then(res=>{console.log(res);}));
循环执行await
这么写并不能实现依次执行函数,因为每个await都是在独立的async函数中。
let arrFn = [fn1,fn2];
arrFn.forEach(async fn=>{
await fn();
})
应该如下写,这样await都在fn函数中
let arrFn = [fn1,fn2];
async function fn(){
for(let i=0;i<arrFn.length;i++){
await arrFn[i]();
}
}
fn();
setTimeout,setInterval,requestAnimationFrame。
开启定时器前,记得先关闭定时器,防止多次开启定时器。
定时器第三个以后的参数会作为第一个函数的参数。
function add(x,y){
console.log(x+y)
}
setTimeout(add,1000,1,2)//3
requestAnimationFrame
自带函数节流功能,回调函数执行次数通常是每秒60次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题。
Event Loop
宏队列,macrotask,也叫tasks。
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微队列,microtask,也叫jobs.
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
浏览器的Event Loop
1.执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
2.全局Script代码执行完毕后,调用栈Stack会清空;
3.从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
4.继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
5.microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
6.取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
7.执行完毕后,调用栈Stack为空;
重复第3-7个步骤;
重复第3-7个步骤;
......
示例:
console.log('script start');
setTimeout(function () {
console.log('setTimeout---0');
}, 0);
setTimeout(function () {
console.log('setTimeout---200');
setTimeout(function () {
console.log('inner-setTimeout---0');
});
Promise.resolve().then(function () {
console.log('promise5');
});
}, 200);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('script end');
运行结果
script start
script end
promise1
promise3
promise2
setTimeout---0
setTimeout---200
promise5
inner-setTimeout---0
代码步骤
首先顺序执行完主进程上的同步任务,第一句和最后一句的console.log。
遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中。
遇到setTimeout 200,它的作用是在 200ms 后将回调函数放到宏任务队列中。
遇到两个promise放入微任务队列中。
同步任务执行完之后,首先检查微任务队列, 发现此队列不为空,执行第一个promise的then回调,输出 'promise1',将promise2加入微任务队列末尾,然后执行第二个promise的then回调,输出'promise3',执行第一个promise加入的promise2,输出 'promise2';
此时微任务队列为空,进入下一个事件循环, 检查宏任务队列,取出第一个宏任务 setTimeout的回调函数,立即执行回调函数输出 'setTimeout---0',检查微任务队列,队列为空,进入下一次事件循环.
检查宏任务队列,取出第一个宏任务 setTimeout的回调函数, 立即执行同步任务输出'setTimeout---200'。
遇到inner-setTimeout---0,放到宏任务队列中,遇到promise5放入微任务队列中。检查微任务队列,发现此队列不为空,执行promise的then回调,输出'promise5'。
此时微任务队列为空,进入下一个事件循环,检查宏任务队列,发现有 setTimeout 的回调函数,立即执行回调函数输出,输出'inner-setTimeout---0'。
代码执行结束.
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);
Promise.resolve().then(() => {
console.log(6)
}).then(() => {
console.log(7)
setTimeout(() => {
console.log(8)
}, 0);
});
})
setTimeout(() => {
console.log(9);
})
console.log(10);
//
1
4
10
5
6
7
2
3
9
8
//
NodeJS的Event Loop
- 执行全局Script的同步代码
- 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
- 开始执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,(在浏览器的Event Loop中是只取宏队列的第一个任务出来执行),每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2
- Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue ......