202303--同事分享的前端面试题收录
浏览器和node的事件循环有什么区别?
先说一下异步任务中的宏任务与微任务概念
异步任务被分为两类:宏任务(macrotask)与微任务(microtask),两者的执行优先级也有所区别。
宏任务主要包含:script(整体代码)
、setTimeout
、setInterval
、setImmediate(Node独有)
,I/O
、DOM Events
、requestAnimationFrame
。
微任务主要包含:Promise.then
、Promise.catch
、Promise.finally
、MutationObserver
,queueMicrotask
,process.nextTick(Node独有)
等。
浏览器事件循环,执行顺序如下:
- 一开始执行栈空,micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)
- 执行一个宏任务script 脚本(执行栈中没有就从任务队列中获取)。
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中。
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务,每个微任务是依次执行的。
- 当前宏任务执行完毕,开始检查渲染,然后渲染线程接管进行渲染。
- 渲染完毕后,JavaScript 线程继续接管,开始下一个循环。
Node的事件循环,执行顺序如下:
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(周而复始运行)...
(1) poll:检索新的 I/O 事件;执行与 I/O 相关的回调,除了关闭的回调函数socket.on('close', callback),setTimeout,setInterval和 setImmediate() 回调函数之外,其余情况 node 将在适当的时候阻塞等待。 poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情 i> 进入该阶段时如果没有设定 timer 的话,会发生以下两件事情
- 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
- 如果 poll 队列为空时,会有两件事发生
- 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
- 如,果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
ii> 当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。 (2) check:setImmediate() 回调函数在这里执行。 (3) close callbacks:一些关闭的回调函数,如:socket.on('close', ...)。 (4) timers:timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。 同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。 (5) pending callbacks:执行延迟到下一个循环迭代的 I/O 回调。 (6) idle、prepare:仅系统内部使用。

浏览器和node事件循环的区别: 浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
下面用一个例子,说明浏览器和node事件循环的区别:
console.log('script开始');
setTimeout(() => {
console.log('宏任务1');
Promise.resolve().then(function () {
console.log('微任务2')
})
},0);
setTimeout(() => {
console.log('宏任务2');
Promise.resolve().then(function() {
console.log('微任务3')
})
},0)
Promise.resolve().then(function () {
console.log('微任务1');
})
console.log('script结束');
在浏览器中的执行顺序为:
script开始
script结束
微任务1
宏任务1
微任务2
宏任务2
微任务3
宏任务与微任务的执行顺序在 Node v10前后版本中表现也有所不同。用上面的例子来分析:
- 在 Node v11+ 一旦执行一个阶段里的一个宏任务(setTimeout,setInterval 和 setImmediate),会立刻执行微任务队列,所以输出顺序为
script开始
script结束
微任务1
宏任务1
微任务2
宏任务2
微任务3
- 在 Node v10 及以下版本,要看第一个定时器执行完成时,第二个定时器是否在完成队列中。
如果第二个定时器还未在完成队列中,输出顺序为
script开始
script结束
微任务1
宏任务1
微任务2
宏任务2
微任务3
如果是第二个定时器已经在完成队列中,输出顺序为
script开始
script结束
微任务1
宏任务1
宏任务2
微任务2
微任务3
什么是AST?有什么用?
抽象语法树AST是Abstract Syntax Tree的缩写,它以抽象的树状形式表现编程语言的语法结构。
先说两个概念:
词法分析:一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元。每个关键字,标识符,操作符,标点符号都是一个 Token。词法分析器会过滤掉源程序中的注释和空白字符(如换行符、空格、制表符等)。最终,整个代码被分割成一个个tokens,形成一个一维的token数组。
语法分析:将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。常见的 JS Parser(语法解析器)有 acorn、esprima、traceur、shift 等。
抽象语法树的用途:
(1)编辑器的错误提示、代码格式化、代码高亮、代码自动补全;
(2)对代码错误进行检查或风格,比如eslint
、pretiier
;
(3)将高版本的JS语法转换成兼容低版本浏览器的语法。如 babel
;
JS执行的过程是读出JS文件中的字符流,接着通过词法分析生成 token
,之后通过语法分析( Parser )生成 AST,最后生成机器码执行。
webpack5是怎么做缓存的?
一图胜千言, 搞清楚缓存是什么时候生成和读取的,这个问题差不多就讲清楚了。先说一下缓存是怎么生成的。缓存主要产生在两个阶段,第一个阶段是模块解析,第二个阶段是模块编译完之后。缓存并不是直接写入硬盘的, 先写入到内存中的缓存队列, 等编译完成之后,才从内存缓存队列写入到硬盘。
写入的是什么内容呢?是一个map类型,key是identifier(缓存资源的唯一标识),value是模块或文件的解析数据(resolveData)+快照(snapshot)。解析数据很好理解。快照的生成规则得说一下,每个文件的快照是依据resolveTime
, fileDependencies
、contextDependencies
、missingDependencies
以及在 webpack.config 的 snapshotOptions
配置来生成快照的内容。
何时读缓存? 项目二次构建解析和编译文件或模块时,会去读缓存。从硬盘读取到内存,再加以利用。读取的时候要判断缓存是否还能使用,若是强制构建,设置了不进行缓存,没有可以检查的快照,缓存失效的话,该解析编译还得重新解析编译。
webpack缓存设计的核心思路是复用已经编译的模块,省去重新执行编译的流程与时间。babel-loader
、eslint-loader
自身内置的缓存功能,DLL
,cache-loader
都是遵循这种思路。顺便说一下webpack4和webpack5缓存的区别。
webpack4缓存方案的不足:
(1)cache-loader
的能力圈仅是经由 loader
处理后的文件内容,缓存内容的范围比较有限;
(2)cache-loader
缓存数据的过程也有一些性能开销,会影响整个项目编译构建速度,一般多用于编译耗时较长的 loader 上。
(3)cache-loader
是通过对比文件 metadata 的 timestamps,这种缓存失效策略不是非常的安全。
webpack5与webpack4相比: (1) webpack5 不仅在module的解析和编译阶段,而且在代码生成、sourceMap 阶段都使用到了持久化缓存; (2)内置了更加安全的缓存对比策略(timestamp + content hash); (3)compile 流程和持久化缓存解耦,在compile阶段持久化缓存数据的动作不会阻碍整个流程,而是先放置到一个缓存队列中,当 compile 结束后才会从内存中写入到硬盘中。
webpack5的cache属性添加了很多配置选项:
属性 | 说明 |
---|---|
cache.type | 缓存类型,支持 'memory' | 'filesystem' ,需要设置为 filesystem 才能开启持久缓存。 |
cache.cacheDirectory | 缓存文件路径,默认为 node_modules/.cache/webpack。 |
cache.buildDependencies | 额外的依赖文件,当这些文件内容发生变化时,缓存会完全失效而执行完整的编译构建。 |
cache.managedPaths | 受控目录,Webpack 构建时会跳过新旧代码哈希值与时间戳的对比,直接使用缓存副本,默认值为 ['./node_modules']。 |
cache.profile | 是否输出缓存处理过程的详细日志,默认为 false。 |
cache.maxAge | 缓存失效时间,默认值为 5184000000秒 。 |
module.exports = {
cache: {
type: 'filesystem', // 可选值 memory | filesystem
cacheDirectory: './.cache/webpack', // 缓存文件生成的地址
buildDependencies: { // 那些文件发现改变就让缓存失效,一般为 webpack 的配置文件
config: [
'./webpack.config.js'
]
},
managedPaths: ['./node_modules', './libs'], // 受控目录,指的就是那些目录文件会生成缓存
profile: true, // 是否输出缓存处理过程的详细日志,默认为 false
maxAge: 1000 * 60 * 60 * 24, // 缓存失效时间,默认值为 5184000000
}
}
async/await的实现原理?
实现思路:
- generator + promise, generator的用法与async/await比较形似,直观的让人想到, async应该是generator的语法糖。async的返回值是Promise,所以最终也要返回一个Promise。
- generator与async的差别是async里面的await 函数可以自动串行执行,所以要写一个递归函数,让generator自动执行
- generator的value和done状态是迭代器协议的返回值。value是yield的返回值, done是false时,继续执行迭代器, done为true时,resolve结果。
/**
* async模拟实现
* @param {*} genFn - 生成器函数
*/
function asyncFn(genFn) {
const g = genFn();
return new Promise((resolve, reject) => {
function autoRunNext(g, nextVal) {
const { value, done } = g.next(nextVal);
// 迭代器未执行完
if (!done) {
value.then((res) => {
autoRunNext(g, res);
})
} else {
// 迭代器执行完
resolve(value);
}
}
// 第一次执行autoRunNext是用来启动遍历器,不用传参数
autoRunNext(g);
})
}
// 测试
const getData = (i) => new Promise((resolve) => setTimeout(() => resolve(`data${i}`), 500))
function* testG() {
const data1 = yield getData(1);
console.log('data1: ', data1);
const data2 = yield getData(2);
console.log('data2: ', data2);
return 'success';
}
asyncFn(testG).then((res) => { console.log(res)})
上传大文件中断了怎么恢复?
大文件一般都是采用分片上传,将大文件转换成二进制文件流,利用文件流可以切割的属性,将整个文件切分成多个chunk,每个chunk都要有chunkIndex,chunkHash, 此外还要传chunkTotal,fileHash给服务器,以便服务端校验文件的正确性和完整性。以并行或串行的方式传输,服务器接收分片并存储,收到合并请求后使用流将切片合并成最终文件。
在分片上传的过程中,如果因为由于意外因素(如网络中断或系统崩溃等异常因素)导致上传中断,网络恢复后,为了避免重新开始从头上传, 需要在上传切片的时候记录上传的进度。再次上传时,可以继续从上次中断的地方进行继续上传。可以在客户端记录,服务端也可以提供已上传分片查询接口,让客户端查询已上传的分片数据,下一次从未上传的分片数据开始继续上传。
用css画一个扇形?
画扇形的方法比较多, 大多方法使用多个div,需要设置多个层级,相对复杂。有一种简单的画法,就是使用css裁剪clip-path的多边形polygon属性,思路是:
- 画一个圆,对这个圆进行裁剪。
- 设置裁剪的第一个点是圆心x=50%, y=50%; 第二个点是x=0%, y=0%;
- 第三个裁剪点是x=100%, y=0%
- 连点成面, 后面若还有裁剪点,裁剪都是基于前面点连出的轮廓。
- 逆时针翻转90度,与我们的直觉就会趋于一致。
<div class="sector"></div>
<style>
.sector{
width: 100px;
height: 100px;
border-radius: 100%;
background-color: green;
// 第三个点x3在0-100%的变化,对应着0-90deg之间的扇形
clip-path: polygon(50% 50%, 0% 0%, 100% 0);
// 在其它点都不变化的情况下,第四个点y4在0-100%之间变化,对应着90-180deg之间的扇形
// clip-path: polygon(50% 50%, 0% 0%, 100% 0,100% 10%);
// 在其它点都不变化的情况下,第五个点x5在100-0%之间变化,对应着180-270deg之间的扇形
// clip-path: polygon(50% 50%, 0% 0%, 100% 0,100% 100%,10% 100%);
// 在其它点都不变化的情况下,第六个点y6在100-0%之间变化,对应着270-360deg之间的扇形
// clip-path: polygon(50% 50%, 0% 0%, 100% 0,100% 100%,0% 100%, 0 10%);
transform: rotate(-45deg);
}
</style>
参考链接
转载自:https://juejin.cn/post/7212016466774048805