# 浏览器 EventLoop
# 宏任务
我们都知道 JS 是单线程的,我们按照代码顺序写入主线程,然后主线程再依次执行。但是浏览器是多个线程合作运行的,并不是执行之前统一安排好的。所以浏览器的主线程通过一个事件循环机制,可以接受并执行新的任务。我们可以通过一个 for 循环语句来监听是否有新的任务,可以在线程运行过程中,等待用户输入的数字,等待过程中线程处于暂停状态,一旦接收到用户输入的信息,那么线程会被激活,然后执行相加运算,最后输出结果。然后一直循环执行。
那么我们如何实现按序的将任务添加到 for 循环中呢,那么答案就是消息队列
消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
# 页面使用单线程的缺点
第一个问题是如何处理高优先级的任务。
因为 DOM 变化非常频繁,如果每次发生变化的时候,都直接调用相应的 JavaScript 接口,那么这个当前的任务执行时间会被拉长,从而导致执行效率的下降。
如果将这些 DOM 变化做成异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了。
这也就是说,如果 DOM 发生变化,采用同步通知的方式,会影响当前任务的执行效率;如果采用异步方式,又会影响到监控的实时性
# 第二个是如何解决单个任务执行时长过久的问题
因为所有的任务都是在单线程中执行的,所以每次只能执行一个任务,而其他任务就都处于等待状态。如果其中一个任务执行时间过久,那么下一个任务就要等待很长时间。
# 宏任务列表
- 渲染事件(如解析 DOM、计算布局、绘制);
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
- JavaScript 脚本执行事件;
- 网络请求完成、文件读写完成事件。
# setTimeout
setTimeout 返回一个 id,而clearTimeout 函数 接受需要取消的定时器的 ID
# 注意事项
- 如果当前任务执行时间过久,会影延迟到期定时器任务的执行
- 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
- 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
- 延时执行时间有最大值
Chrome
、Safari
、Firefox
都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,这导致定时器会被立即执行.
- 如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。
# requestAnimationFrame
requestAnimationFrame 提供一个原生的 API 去执行动画的效果,它会在一帧(一般是 16ms)间隔内根据选择浏览器情况去执行相关动作。
# 微任务
为了解决消息队列的种种问题,浏览器引入了微任务。微任务可以在实时性和效率之间做一个有效的权衡。
每个宏任务都有一个微任务列表,在宏任务的执行过程中产生微任务会被添加到该列表中。执行时机是在主函数执行结束之后、当前宏任务结束之前。
在现代浏览器里面,产生微任务有两种方式:
第一种方式是使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。
# MutationObserver
# Mutation Event
2000 年的时候引入了 Mutation Event, Mutation Event 采用了观察者的设计模式,当 DOM 有变动时就会立刻触发相应的事件,这种方式属于同步回调。
采用 Mutation Event 解决了实时性的问题,因为 DOM 一旦发生变化,就会立即调用 JavaScript 接口。但也正是这种实时性造成了严重的性能问题,因为每次 DOM 变动,渲染引擎都会去调用 JavaScript,这样会产生较大的性能开销。
# MutationObserverAPI
MutationObserver 的作用是监控某个 DOM 节点
MutationObserver 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响。
如果采用 setTimeout 创建宏任务来触发回调的话,那么实时性就会大打折扣,因为上面我们分析过,在两个任务之间,可能会被渲染进程插入其他的事件,从而影响到响应的实时性。
MutationObserver 采用了“异步 + 微任务”的策略。
- 通过异步操作解决了同步操作的性能问题;
- 通过微任务解决了实时性的问题。
# Pormise
# 为什么会有 promise
首先让我们来了解下,回调函数有什么缺点:
- 多重嵌套,导致回调地狱
- 代码跳跃,并非人类习惯的思维模式 ,代码逻辑不连续
- 信任问题,你不能把你的回调完全寄托与第三方库,因为你不知道第三方库到底会怎么执行回调(多次执行)
- 第三方库可能没有提供错误处理
- 不清楚回调是否都是异步调用的(可以同步调用 ajax,在收到响应前会阻塞整个线程,会陷入假死状态,非常不推荐)
为了兼容一些 promise 库,Promise 采用了一种鸭子模型(如果它看起来像只鸭子,叫起来 像只鸭子,那它一定就是只鸭子)来判断这个函数是不是一个 promise 函数,也就是判断.then()方法是否注册了 "fullfillment
" 和 / 或 "rejection
" 事件.
代码跳跃则是通过事件穿透解决的,但是也没有那么黑魔法,只不过是 then 默认参数就是把值往后传或者抛
onResolved = typeof onResolved === 'function' ? onResolved : function(value) {return value}
onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {throw reason}
# promise 方法
# 1.Promise.prototype.finally()
finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
相当于
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
(value) => P.resolve(callback()).then(() => value),
(reason) =>
P.resolve(callback()).then(() => {
throw reason;
})
);
};
# 2.Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
(1)只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
(2)只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
# 3.Promise.race()
const p = Promise.race([p1, p2, p3]);
上面代码中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为 reject,否则变为 resolve。
const p = Promise.race([
fetch("/resource-that-may-take-a-while"),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error("request timeout")), 5000);
}),
]);
p.then(console.log).catch(console.error);
上面代码中,如果 5 秒之内 fetch
方法无法返回结果,变量 p 的状态就会变为 rejected
,从而触发 catch
方法指定的回调函数。
这种可以使用 axios
的超时拦截,别的什么作用还没想到
# 4.Promise.allSettled()
Promise.allSettled()
方法接受一组 Promise
实例作为参数,包装成一个新的 Promise
实例。只有等到所有这些参数实例都返回结果,不管是 fulfilled
还是 rejected
,包装实例才会结束。该方法由 ES2020 引入。
有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()
方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。
# 5.Promise.any()
与 promise.all()相反
Promise.any() 方法接受一组 Promise
实例作为参数,包装成一个新的 Promise
实例。只要参数实例有一个变成 fulfilled
状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected
状态,包装实例就会变成 rejected
状态。该方法目前是一个第三阶段的提案 。
# 6.Promise.resolve()
有时需要将现有对象转为 Promise
对象,Promise.resolve() 方法就起到这个作用。
Promise.resolve()等价于下面的写法
Promise.resolve('foo') // 等价于 new Promise(resolve => resolve('foo'))
Promise.resolve 方法的参数分成四种情况: (1)参数是一个 Promise 实例 如果参数是 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动地返回这个实例。 (2)参数是一个 thenable 对象 thenable 对象指的是具有 then 方法的对象,比如下面这个对象。
let thenable = {
then: function (resolve, reject) {
resolve(42);
},
};
(3)参数不是具有 then 方法的对象,或根本就不是对象
(4)不带有任何参数
相当于 new Promise()
需要注意的是,立即 resolve()的 Promise
对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
# 7.Promise.reject()
# 8Promise.try()
由于 Promise.try 为所有操作提供了统一的处理机制,所以如果想用 then 方法管理流程,最好都用 Promise.try 包装一下。这样有许多好处,其中一点就是可以更好地管理异常。
解决了 try{}catch{} 无法处理异步错误的问题
# 手写 promise
# 简易实现
function Promise(excutro) {
let self = this;
self.onResolved = [];
self.onReject = [];
self.stauts = "pedding";
function resolve(value) {
if (self.stauts === "pedding") {
self.stauts = "resolved";
self.value = value;
self.onResolved.forEach((fn) => fn(value));
}
}
function reject(reson) {
if (self.stauts === "pedding") {
self.stauts = "reject";
self.reson = reson;
self.onResolved.forEach((fn) => fn(value));
}
}
excutro(resolve, reject);
}
# then
Promise.prototype.then = function (resolved, rejected) {
let self = this;
let promise2;
resolved = typeof resolved === "function" ? resolved : (val) => val;
if (self.stauts === "resolved") {
return (promise2 = new Promise((resolve, reject) => {
try {
x = resolved(self.value);
if (x instanceof Promise) {
x = x.then;
}
resolve(x);
} catch (e) {
reject(e);
}
}));
}
if (self.stauts === "pedding") {
return (promise2 = new Promise((resolve, reject) => {
try {
self.onResolved.push(function () {
try {
x = resolved(self.value);
if (x instanceof Promise) x = x.then(resolve, reject);
} catch (e) {
reject(e);
}
});
} catch (e) {
reject(e);
}
}));
}
};
# all
Promise.prototype.all = function (promiseArr) {
let index = 0;
let result = [];
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p, i) => {
//Promise.resolve(p)用于处理传入值不为Promise的情况
MyPromise.resolve(p).then(
(val) => {
index++;
result[i] = val; //所有then执行后, resolve结果
if (index === promiseArr.length) {
resolve(result);
}
},
(err) => {
reject(err);
} //有一个Promise被reject时,MyPromise的状态变为reject
);
});
});
};
# race
Promise.prototype.race = function (promiseArr) {
return new MyPromise((resolve, reject) => {
//同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态
for (let p of promiseArr) {
MyPromise.resolve(p).then(
//Promise.resolve(p)用于处理传入值不为Promise的情况
(value) => {
resolve(value);
}, //注意这个resolve是上边new MyPromise的
(err) => {
reject(err);
}
);
}
});
};
# stop
Promise.cancel = Promise.stop = function () {
return new Promise(function () {});
};
# done
Promise.prototype.done = function () {
return this.catch(function (e) {
// 此处一定要确保这个函数不能再出错
console.error(e);
});
};
# 出错时
出错时,是用 throw new Error()还是用 return Promise.reject(new Error())呢?
性能方面,throw new Error()会使代码进入 catch 块里的逻辑
而使用 Promise.reject(new Error()),则需要构造一个新的 Promise 对象(里面包含 2 个数组,4 个函数:resolve/reject,onResolved/onRejected),也会花费一定的时间和内存
综上,我觉得在 Promise 里发现显式的错误后,用 throw 抛出错误会比较好,而不是显式的构造一个被 reject 的 Promise 对象。
# await
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
Generator 函数,依次读取两个文件。
const fs = require("fs");
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile("/etc/fstab");
const f2 = yield readFile("/etc/shells");
console.log(f1.toString());
console.log(f2.toString());
};
async 函数
const asyncReadFile = async function () {
const f1 = await readFile("/etc/fstab");
const f2 = await readFile("/etc/shells");
console.log(f1.toString());
console.log(f2.toString());
};
async 函数对 Generator 函数的改进,体现在以下四点
(1)内置执行器。
(2)更好的语义。
(3)更广的适用性。
(4)返回值是Promise
。
async
函数返回的 Promise
对象,必须等到内部所有 await
命令后面的 Promise
对象执行完,才会发生状态改变,除非遇到 return
语句或者抛出错误。也就是说,只有 async
函数内部的异步操作执行完,才会执行 then
方法指定的回调函数。
# 实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
spawn
function spawn(genF) {
return new Promise(function (resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
function (v) {
step(function () {
return gen.next(v);
});
},
function (e) {
step(function () {
return gen.throw(e);
});
}
);
}
step(function () {
return gen.next(undefined);
});
});
}
# 与 Node 环境的 EventLoop 区别
在 node 11 版本中,node 下 Event Loop 已经与浏览器趋于相同
# 循环阶段
- timers:执行满足条件的 setTimeout、setInterval 回调。
- I/O callbacks:是否有已完成的 I/O 操作的回调函数,来自上一轮的 poll 残留。
- idle,prepare:可忽略
- poll:等待还没完成的 I/O 事件,会因 timers 和超时时间等结束等待。
- check:执行 setImmediate 的回调。
- close callbacks:关闭所有的 closing handles,一些 onclose 事件。
┌───────────────────────┐
┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ pending callbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ idle, prepare │<————— 内部调用(可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │ - (执行几乎所有的回调,除了 close callbacks 以及 timers 调度的 回调和 setImmediate() 调度的回调,在恰当的时机将会阻塞在此阶段)
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ | | |
| | └───────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| ┌──────────┴────────────┐
│ │ check │<————— setImmediate() 的回调将会在这个阶段执行
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
└──┤ close callbacks │<————— socket.on('close', ...)
└───────────────────────┘
# setTimeout 与 setImmediate 的顺序
Node 并不能保证 timers 在预设时间到了就会立即执行,因为 Node 对 timers 的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout() 和 setImmediate() 都写在 Main 进程中,但它们的执行顺序是不确定的: