likes
comments
collection
share

202303--同事分享的前端面试题收录

作者站长头像
站长
· 阅读数 50

浏览器和node的事件循环有什么区别?

先说一下异步任务中的宏任务与微任务概念

异步任务被分为两类:宏任务(macrotask)与微任务(microtask),两者的执行优先级也有所区别。 宏任务主要包含:script(整体代码)setTimeoutsetIntervalsetImmediate(Node独有),I/ODOM EventsrequestAnimationFrame。 微任务主要包含:Promise.thenPromise.catchPromise.finallyMutationObserver,queueMicrotask,process.nextTick(Node独有)等。

浏览器事件循环,执行顺序如下:

  1. 一开始执行栈空,micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)
  2. 执行一个宏任务script 脚本(执行栈中没有就从任务队列中获取)。
  3. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中。
  4. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务,每个微任务是依次执行的。
  5. 当前宏任务执行完毕,开始检查渲染,然后渲染线程接管进行渲染。
  6. 渲染完毕后,JavaScript 线程继续接管,开始下一个循环。

202303--同事分享的前端面试题收录

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:仅系统内部使用。

202303--同事分享的前端面试题收录

浏览器和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)对代码错误进行检查或风格,比如eslintpretiier ; (3)将高版本的JS语法转换成兼容低版本浏览器的语法。如  babel

JS执行的过程是读出JS文件中的字符流,接着通过词法分析生成 token,之后通过语法分析( Parser )生成 AST,最后生成机器码执行。

webpack5是怎么做缓存的?

一图胜千言, 搞清楚缓存是什么时候生成和读取的,这个问题差不多就讲清楚了。先说一下缓存是怎么生成的。缓存主要产生在两个阶段,第一个阶段是模块解析,第二个阶段是模块编译完之后。缓存并不是直接写入硬盘的, 先写入到内存中的缓存队列, 等编译完成之后,才从内存缓存队列写入到硬盘。

写入的是什么内容呢?是一个map类型,key是identifier(缓存资源的唯一标识),value是模块或文件的解析数据(resolveData)+快照(snapshot)。解析数据很好理解。快照的生成规则得说一下,每个文件的快照是依据resolveTimefileDependenciescontextDependenciesmissingDependencies 以及在 webpack.config 的 snapshotOptions配置来生成快照的内容。

202303--同事分享的前端面试题收录

何时读缓存? 项目二次构建解析和编译文件或模块时,会去读缓存。从硬盘读取到内存,再加以利用。读取的时候要判断缓存是否还能使用,若是强制构建,设置了不进行缓存,没有可以检查的快照,缓存失效的话,该解析编译还得重新解析编译。 202303--同事分享的前端面试题收录

webpack缓存设计的核心思路是复用已经编译的模块,省去重新执行编译的流程与时间。babel-loadereslint-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的实现原理?

实现思路:

  1. generator + promise, generator的用法与async/await比较形似,直观的让人想到, async应该是generator的语法糖。async的返回值是Promise,所以最终也要返回一个Promise。
  2. generator与async的差别是async里面的await 函数可以自动串行执行,所以要写一个递归函数,让generator自动执行
  3. 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属性,思路是:

  1. 画一个圆,对这个圆进行裁剪。
  2. 设置裁剪的第一个点是圆心x=50%, y=50%; 第二个点是x=0%, y=0%;
  3. 第三个裁剪点是x=100%, y=0%
  4. 连点成面, 后面若还有裁剪点,裁剪都是基于前面点连出的轮廓。
  5. 逆时针翻转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>

202303--同事分享的前端面试题收录

参考链接

转载自:https://juejin.cn/post/7212016466774048805
评论
请登录