# Event Loop详解
点击关注本[公众号](https://www.kancloud.cn/book/dsh225/javascript_vue_css/edit#_118)获取文档最新更新,并可以领取配套于本指南的《**前端面试手册**》以及**最标准的简历模板**.
> 本文是[弄懂Event Loop](https://juejin.im/post/5c3d8956e51d4511dc72c200?utm_source=gold_browser_extension#comment)的删改版,去除了原文中一些容易引起歧义的部分,对一些内容进行了扩充
[TOC]
## 前言
`Event Loop`即事件循环,是指浏览器或`Node`的一种解决`javaScript`单线程运行时不会阻塞的一种机制,也就是我们经常使用**异步**的原理。
## 为啥要弄懂Event Loop
* 是要增加自己技术的深度,也就是懂得`JavaScript`的运行机制。
* 现在在前端领域各种技术层出不穷,掌握底层原理,可以让自己以不变,应万变。
* 应对各大互联网公司的面试,懂其原理,题目任其发挥。
## 栈、队列的基本概念
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995437042-9d683636-9bf5-45fb-8cf3-e482b94a707d.webp#align=left&display=inline&height=271&originHeight=271&originWidth=294&size=0&status=done&width=294)
### 栈(Stack)
**栈**在计算机科学中是限定仅在**表尾**进行**插入**或**删除**操作的线性表。 **栈**是一种数据结构,它按照**后进先出**的原则存储数据,**先进入**的数据被压入**栈底**,**最后的数据**在**栈顶**,需要读数据的时候从**栈顶**开始**弹出数据**。
**栈**是只能在**某一端插入**和**删除**的**特殊线性表**。
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995436902-6dcf8420-be5b-4dd9-9fb6-43e567e53c86.webp#align=left&display=inline&height=282&originHeight=282&originWidth=616&size=0&status=done&width=616)
### 队列(Queue)
特殊之处在于它只允许在表的前端(`front`)进行**删除**操作,而在表的后端(`rear`)进行**插入**操作,和**栈**一样,**队列**是一种操作受限制的线性表。
进行**插入**操作的端称为**队尾**,进行**删除**操作的端称为**队头**。 队列中没有元素时,称为**空队列**。
**队列**的数据元素又称为**队列元素**。在队列中插入一个队列元素称为**入队**,从**队列**中**删除**一个队列元素称为**出队**。因为队列**只允许**在一端**插入**,在另一端**删除**,所以只有**最早**进入**队列**的元素**才能最先从队列中**删除,故队列又称为**先进先出**(`FIFO—first in first out`)
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995436932-57cb6ee5-763a-47b2-a0be-4174c4fd1c66.webp#align=left&display=inline&height=270&originHeight=270&originWidth=554&size=0&status=done&width=554)
## Event Loop
在`JavaScript`中,任务被分为两种,一种宏任务(`MacroTask`)也叫`Task`,一种叫微任务(`MicroTask`)。
### (宏任务)
* `script`全部代码、`setTimeout`、`setInterval`、`setImmediate`(浏览器暂时不支持,只有IE10支持,具体可见[`MDN`](https://link.juejin.im/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FWindow%2FsetImmediate))、`I/O`、`UI Rendering`。
### (微任务)
* `Process.nextTick(Node独有)`、`Promise`、`Object.observe(废弃)`、`MutationObserver`(具体使用方式查看[这里](https://link.juejin.im/?target=http%3A%2F%2Fjavascript.ruanyifeng.com%2Fdom%2Fmutationobserver.html))
## 浏览器中的Event Loop
`Javascript` 有一个 `main thread` 主线程和 `call-stack` 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
### JS调用栈
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
### 同步任务和异步任务
`Javascript`单线程任务被分为**同步任务**和**异步任务**,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995436925-9ac8fb38-4dfd-4d12-b5fe-c564ede43f80.webp#align=left&display=inline&height=518&originHeight=518&originWidth=636&size=0&status=done&width=636)任务队列`Task Queue`,即队列,是一种先进先出的一种数据结构。![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995437021-43545b5b-0a48-475a-a8a6-cce80433fe1a.webp#align=left&display=inline&height=669&originHeight=669&originWidth=800&size=0&status=done&width=800)
### 事件循环的进程模型
* 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即`null`,则执行跳转到微任务(`MicroTask`)的执行步骤。
* 将事件循环中的任务设置为已选择任务。
* 执行任务。
* 将事件循环中当前运行任务设置为null。
* 将已经运行完成的任务从任务队列中删除。
* microtasks步骤:进入microtask检查点。
* 更新界面渲染。
* 返回第一步。
### 执行进入microtask检查点时,用户代理会执行以下步骤:
* 设置microtask检查点标志为true。
* 当事件循环`microtask`执行不为空时:选择一个最先进入的`microtask`队列的`microtask`,将事件循环的`microtask`设置为已选择的`microtask`,运行`microtask`,将已经执行完成的`microtask`为`null`,移出`microtask`中的`microtask`。
* 清理IndexDB事务
* 设置进入microtask检查点的标志为false。
上述可能不太好理解,下图是我做的一张图片。
![](https://cdn.nlark.com/yuque/0/2019/gif/128853/1560995436931-71f56a41-54d3-49f3-a382-c1e6acbf301e.gif#align=left&display=inline&height=589&originHeight=589&originWidth=1011&size=0&status=done&width=1011)
执行栈在执行完**同步任务**后,查看**执行栈**是否为空,如果执行栈为空,就会去检查**微任务**(`microTask`)队列是否为空,如果为空的话,就执行`Task`(宏任务),否则就一次性执行完所有微任务。
每次单个**宏任务**执行完毕后,检查**微任务**(`microTask`)队列是否为空,如果不为空的话,会按照**先入先**出的规则全部执行完**微任务**(`microTask`)后,设置**微任务**(`microTask`)队列为`null`,然后再执行**宏任务**,如此循环。
## 举个例子
~~~
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
~~~
首先我们划分几个分类:
### 第一次执行:
~~~
Tasks:run script、 setTimeout callback
Microtasks:Promise then
JS stack: script
Log: script start、script end。
~~~
执行同步代码,将宏任务(`Tasks`)和微任务(`Microtasks`)划分到各自队列中。
### 第二次执行:
~~~
Tasks:run script、 setTimeout callback
Microtasks:Promise2 then
JS stack: Promise2 callback
Log: script start、script end、promise1、promise2
~~~
执行宏任务后,检测到微任务(`Microtasks`)队列中不为空,执行`Promise1`,执行完成`Promise1`后,调用`Promise2.then`,放入微任务(`Microtasks`)队列中,再执行`Promise2.then`。
### 第三次执行:
~~~
Tasks:setTimeout callback
Microtasks:
JS stack: setTimeout callback
Log: script start、script end、promise1、promise2、setTimeout
~~~
当微任务(`Microtasks`)队列中为空时,执行宏任务(`Tasks`),执行`setTimeout callback`,打印日志。
### 第四次执行:
~~~
Tasks:setTimeout callback
Microtasks:
JS stack:
Log: script start、script end、promise1、promise2、setTimeout
~~~
清空**Tasks**队列和`JS stack`。
以上执行帧动画可以查看[Tasks, microtasks, queues and schedules](https://link.juejin.im/?target=https%3A%2F%2Fjakearchibald.com%2F2015%2Ftasks-microtasks-queues-and-schedules%2F)
或许这张图也更好理解些。
![](https://cdn.nlark.com/yuque/0/2019/gif/128853/1560995436968-c6ff2732-4b20-49d4-852f-8c298eeb0d2e.gif#align=left&display=inline&height=341&originHeight=341&originWidth=611&size=0&status=done&width=611)
## 再举个例子
~~~
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
~~~
这里需要先理解`async/await`。
`async/await` 在底层转换成了 `promise` 和 `then` 回调函数。
也就是说,这是 `promise` 的语法糖。
每次我们使用 `await`, 解释器都创建一个 `promise` 对象,然后把剩下的 `async` 函数中的操作放到 `then` 回调函数中。
`async/await` 的实现,离不开 `Promise`。从字面意思来理解,`async` 是“异步”的简写,而 `await` 是 `async wait` 的简写可以认为是等待异步方法执行完成。
### **关于73以下版本和73版本的区别**
* 在老版本版本以下,先执行`promise1`和`promise2`,再执行`async1`。
* 在73版本,先执行`async1`再执行`promise1`和`promise2`。
**主要原因是因为在谷歌(金丝雀)73版本中更改了规范,如下图所示**:
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995436973-49daab42-3959-4cb2-9417-30a651fedf80.webp#align=left&display=inline&height=243&originHeight=243&originWidth=668&size=0&status=done&width=668)
* 区别在于`RESOLVE(thenable)`和之间的区别`Promise.resolve(thenable)`。
### **在老版本中**
* 首先,传递给 `await` 的值被包裹在一个 `Promise` 中。然后,处理程序附加到这个包装的 `Promise`,以便在 `Promise` 变为 `fulfilled` 后恢复该函数,并且暂停执行异步函数,一旦 `promise` 变为 `fulfilled`,恢复异步函数的执行。
* 每个 `await` 引擎必须创建两个额外的 Promise(即使右侧已经是一个 `Promise`)并且它需要至少三个 `microtask` 队列 `ticks`(`tick`为系统的相对时间单位,也被称为系统的时基,来源于定时器的周期性中断(输出脉冲),一次中断表示一个`tick`,也被称做一个“时钟滴答”、时标。)。
### **引用贺老师知乎上的一个例子**
~~~
async function f() {
await p
console.log('ok')
}
~~~
简化理解为:
~~~
function f() {
return RESOLVE(p).then(() => {
console.log('ok')
})
}
~~~
* 如果 `RESOLVE(p)` 对于 `p` 为 `promise` 直接返回 `p` 的话,那么 `p`的 `then` 方法就会被马上调用,其回调就立即进入 `job` 队列。
* 而如果 `RESOLVE(p)` 严格按照标准,应该是产生一个新的 `promise`,尽管该 `promise`确定会 `resolve` 为 `p`,但这个过程本身是异步的,也就是现在进入 `job` 队列的是新 `promise`的 `resolve`过程,所以该 `promise` 的 `then` 不会被立即调用,而要等到当前 `job` 队列执行到前述 `resolve` 过程才会被调用,然后其回调(也就是继续 `await` 之后的语句)才加入 `job` 队列,所以时序上就晚了。
### **谷歌(金丝雀)73版本中**
* 使用对`PromiseResolve`的调用来更改`await`的语义,以减少在公共`awaitPromise`情况下的转换次数。
* 如果传递给 `await` 的值已经是一个 `Promise`,那么这种优化避免了再次创建 `Promise` 包装器,在这种情况下,我们从最少三个 `microtick` 到只有一个 `microtick`。
### **详细过程:**
**73以下版本**
* 首先,打印`script start`,调用`async1()`时,返回一个`Promise`,所以打印出来`async2 end`。
* 每个 `await`,会新产生一个`promise`,但这个过程本身是异步的,所以该`await`后面不会立即调用。
* 继续执行同步代码,打印`Promise`和`script end`,将`then`函数放入**微任务**队列中等待执行。
* 同步执行完成之后,检查**微任务**队列是否为`null`,然后按照先入先出规则,依次执行。
* 然后先执行打印`promise1`,此时`then`的回调函数返回`undefinde`,此时又有`then`的链式调用,又放入**微任务**队列中,再次打印`promise2`。
* 再回到`await`的位置执行返回的 `Promise` 的 `resolve` 函数,这又会把 `resolve` 丢到微任务队列中,打印`async1 end`。
* 当**微任务**队列为空时,执行宏任务,打印`setTimeout`。
**谷歌(金丝雀73版本)**
* 如果传递给 `await` 的值已经是一个 `Promise`,那么这种优化避免了再次创建 `Promise` 包装器,在这种情况下,我们从最少三个 `microtick` 到只有一个 `microtick`。
* 引擎不再需要为 `await` 创造 `throwaway Promise` - 在绝大部分时间。
* 现在 `promise` 指向了同一个 `Promise`,所以这个步骤什么也不需要做。然后引擎继续像以前一样,创建 `throwaway Promise`,安排 `PromiseReactionJob` 在 `microtask` 队列的下一个 `tick` 上恢复异步函数,暂停执行该函数,然后返回给调用者。
具体详情查看([这里](https://link.juejin.im/?target=https%3A%2F%2Fv8.js.cn%2Fblog%2Ffast-async%2F))。
## NodeJS的Event Loop
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995436966-ae34b24c-83d0-4472-8854-1552abd9fcdb.webp#align=left&display=inline&height=223&originHeight=223&originWidth=543&size=0&status=done&width=543)
`Node`中的`Event Loop`是基于`libuv`实现的,而`libuv`是 `Node` 的新跨平台抽象层,libuv使用异步,事件驱动的编程方式,核心是提供`i/o`的事件循环和异步回调。libuv的`API`包含有时间,非阻塞的网络,异步文件操作,子进程等等。 `Event Loop`就是在`libuv`中实现的。
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995436946-e5bcfbd1-340e-4c68-a14e-cd000081eef4.webp#align=left&display=inline&height=442&originHeight=442&originWidth=745&size=0&status=done&width=745)
### `Node`的`Event loop`一共分为6个阶段,每个细节具体如下:
* `timers`: 执行`setTimeout`和`setInterval`中到期的`callback`。
* `pending callback`: 上一轮循环中少数的`callback`会放在这一阶段执行。
* `idle, prepare`: 仅在内部使用。
* `poll`: 最重要的阶段,执行`pending callback`,在适当的情况下回阻塞在这个阶段。
* `check`: 执行`setImmediate`(`setImmediate()`是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行`setImmediate`指定的回调函数)的`callback`。
* `close callbacks`: 执行`close`事件的`callback`,例如`socket.on('close'[,fn])`或者`http.server.on('close, fn)`。
具体细节如下:
### timers
执行`setTimeout`和`setInterval`中到期的`callback`,执行这两者回调需要设置一个毫秒数,理论上来说,应该是时间一到就立即执行callback回调,但是由于`system`的调度可能会延时,达不到预期时间。
以下是官网文档解释的例子:
~~~
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
~~~
当进入事件循环时,它有一个空队列(`fs.readFile()`尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,`fs.readFile()`完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。
当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的**阈值**,然后回到**timers阶段**以执行定时器的回调。
在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。
**以下是我测试时间:**
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1560995436982-2f817b92-3c4a-4d4c-8f95-92f63efa336c.webp#align=left&display=inline&height=430&originHeight=430&originWidth=724&size=0&status=done&width=724)
### pending callbacks
此阶段执行某些系统操作(例如TCP错误类型)的回调。 例如,如果`TCP socket ECONNREFUSED`在尝试connect时receives,则某些\* nix系统希望等待报告错误。 这将在`pending callbacks`阶段执行。
### poll
**该poll阶段有两个主要功能:**
* 执行`I/O`回调。
* 处理轮询队列中的事件。
**当事件循环进入`poll`阶段并且在`timers`中没有可以执行定时器时,将发生以下两种情况之一**
* 如果`poll`队列不为空,则事件循环将遍历其同步执行它们的`callback`队列,直到队列为空,或者达到`system-dependent`(系统相关限制)。
**如果`poll`队列为空,则会发生以下两种情况之一**
* 如果有`setImmediate()`回调需要执行,则会立即停止执行`poll`阶段并进入执行`check`阶段以执行回调。
* 如果没有`setImmediate()`回到需要执行,poll阶段将等待`callback`被添加到队列中,然后立即执行。
**当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。**
### check
**此阶段允许人员在poll阶段完成后立即执行回调。**
如果`poll`阶段闲置并且`script`已排队`setImmediate()`,则事件循环到达check阶段执行而不是继续等待。
`setImmediate()`实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用`libuv API`来调度在`poll`阶段完成后执行的回调。
通常,当代码被执行时,事件循环最终将达到`poll`阶段,它将等待传入连接,请求等。
但是,如果已经调度了回调`setImmediate()`,并且轮询阶段变为空闲,则它将结束并且到达`check`阶段,而不是等待`poll`事件。
~~~
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
~~~
如果`node`版本为`v11.x`, 其结果与浏览器一致。
~~~
start
end
promise3
timer1
promise1
timer2
promise2
~~~
具体详情可以查看《[又被node的eventloop坑了,这次是node的锅](https://juejin.im/post/5c3e8d90f265da614274218a)》。
如果v10版本上述结果存在两种情况:
* 如果time2定时器已经在执行队列中了
~~~
start
end
promise3
timer1
timer2
promise1
promise2
~~~
* 如果time2定时器没有在执行对列中,执行结果为
~~~
start
end
promise3
timer1
promise1
timer2
promise2
~~~
具体情况可以参考`poll`阶段的两种情况。
从下图可能更好理解:
![](https://cdn.nlark.com/yuque/0/2019/gif/128853/1560995436960-165cb65c-477f-4b4c-8a0a-79d3136f342e.gif#align=left&display=inline&height=333&originHeight=333&originWidth=598&size=0&status=done&width=598)
## setImmediate() 的setTimeout()的区别
**`setImmediate`和`setTimeout()`是相似的,但根据它们被调用的时间以不同的方式表现。**
* `setImmediate()`设计用于在当前`poll`阶段完成后check阶段执行脚本 。
* `setTimeout()` 安排在经过最小(ms)后运行的脚本,在`timers`阶段执行。
### 举个例子
~~~
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
~~~
**执行定时器的顺序将根据调用它们的上下文而有所不同。 如果从主模块中调用两者,那么时间将受到进程性能的限制。**
**其结果也不一致**
**如果在`I / O`周期内移动两个调用,则始终首先执行立即回调:**
~~~
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
~~~
其结果可以确定一定是`immediate => timeout`。
主要原因是在`I/O阶段`读取文件后,事件循环会先进入`poll`阶段,发现有`setImmediate`需要执行,会立即进入`check`阶段执行`setImmediate`的回调。
然后再进入`timers`阶段,执行`setTimeout`,打印`timeout`。
~~~
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
~~~
## Process.nextTick()
**`process.nextTick()`虽然它是异步API的一部分,但未在图中显示。这是因为`process.nextTick()`从技术上讲,它不是事件循环的一部分。**
* `process.nextTick()`方法将 `callback` 添加到`next tick`队列。 一旦当前事件轮询队列的任务全部完成,在`next tick`队列中的所有`callbacks`会被依次调用。
**换种理解方式:**
* 当每个阶段完成后,如果存在 `nextTick` 队列,就会清空队列中的所有回调函数,并且优先于其他 `microtask` 执行。
### 例子
~~~
let bar;
setTimeout(() => {
console.log('setTimeout');
}, 0)
setImmediate(() => {
console.log('setImmediate');
})
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
~~~
在NodeV10中上述代码执行可能有两种答案,一种为:
~~~
bar 1
setTimeout
setImmediate
~~~
另一种为:
~~~
bar 1
setImmediate
setTimeout
~~~
无论哪种,始终都是先执行`process.nextTick(callback)`,打印`bar 1`。
* * *
## 公众号
想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新.
**简历模板**:关注公众号回复「模板」获取
《**前端面试手册**》:配套于本指南的突击手册,关注公众号回复「fed」获取
![2019-08-12-03-18-41](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png)
- 前言
- 指南使用手册
- 为什么会有这个项目
- 面试技巧
- 面试官到底想看什么样的简历?
- 面试回答问题的技巧
- 如何通过HR面
- 推荐
- 书籍/课程推荐
- 前端基础
- HTML基础
- CSS基础
- JavaScript基础
- 浏览器与新技术
- DOM
- 前端基础笔试
- HTTP笔试部分
- JavaScript笔试部分
- 前端原理详解
- JavaScript的『预解释』与『变量提升』
- Event Loop详解
- 实现不可变数据
- JavaScript内存管理
- 实现深克隆
- 如何实现一个Event
- JavaScript的运行机制
- 计算机基础
- HTTP协议
- TCP面试题
- 进程与线程
- 数据结构与算法
- 算法面试题
- 字符串类面试题
- 前端框架
- 关于前端框架的面试须知
- Vue面试题
- React面试题
- 框架原理详解
- 虚拟DOM原理
- Proxy比defineproperty优劣对比?
- setState到底是异步的还是同步的?
- 前端路由的实现
- redux原理全解
- React Fiber 架构解析
- React组件复用指南
- React-hooks 抽象组件
- 框架实战技巧
- 如何搭建一个组件库的开发环境
- 组件设计原则
- 实现轮播图组件
- 性能优化
- 前端性能优化-加载篇
- 前端性能优化-执行篇
- 工程化
- webpack面试题
- 前端工程化
- Vite
- 安全
- 前端安全面试题
- npm
- 工程化原理
- 如何写一个babel
- Webpack HMR 原理解析
- webpack插件编写
- webpack 插件化设计
- Webpack 模块机制
- webpack loader实现
- 如何开发Babel插件
- git
- 比较
- 查看远程仓库地址
- git flow
- 比较分支的不同并保存压缩文件
- Tag
- 回退
- 前端项目经验
- 确定用户是否在当前页面
- 前端下载文件
- 只能在微信中访问
- 打开新页面-被浏览器拦截
- textarea高度随内容变化 vue版
- 去掉ios原始播放大按钮
- nginx在MAC上的安装、启动、重启和关闭
- 解析latex格式的数学公式
- 正则-格式化a链接
- 封装的JQ插件库
- 打包问题总结
- NPM UI插件
- 带你入门前端工程
- webWorker+indexedDB性能优化
- 多个相邻元素切换效果出现边框重叠问题的解决方法
- 监听前端storage变化