面试题小记

124 阅读19分钟

wepack5 新特性

模块联邦

内置清除输出目录 在 output 对象中配置 { clean: true }

更为优雅的处理资源模块 如 asset/resource 替代 file-loader

打包体积优化,更好的处理 tree-shaking,需要按需导入,导出

打包缓存,无需 cache-loader,默认开启缓存,缓存到内存中,也可对 cache 进行配置,如缓存到文件(fileSystem)中

tree shaking

目的: 删除无效代码,减小打包文件体积

使用 esm 方式的导入导出能更为方便的做 tree shaking, es6 模块的导入导出的的依赖关系是确定的,在代码执行之前,通过静态分析,就可得到完整的静态关系。

什么是loader

loader 我理解的就是一个模块转换函数,像管道一样,由于 webpack 只识别 js 和 json,处理其他的文件,如 css,需要预处理,loader 可以将不同的文件转换为 js 或者 data url。

什么是plugin

由于 webpack 的整个构建体系是依托于插件的,webpack 有很多钩子函数,在不同的时期触发,如 emit, 我们可以注册钩子的回调函数,改变打包结果和做到其它一些 loader 做不到的事,使用钩子的类或函数就是插件。

防抖

防抖我理解的就是如这样一个场景,搜索框输入关键词,会请求接口,渲染列表,但是用户在输入的时候,每输入一个字请求一次接口无疑事一次浪费,我们只需要在用户输完关键词,再请求一次接口,这就是防抖的场景,如果在规定的时间再次请求,就取消前一次的请求。

应用场景:邮箱验证,搜索框关键词列表渲染

function(fn, delay) {
    let timer = null;
    return function() {
        if(timer) {
            clearInterval(timer);
        }
    
        timer = setTimeout(() => {
            fn.apply(this);
        }, delay)
    }
}

节流

节流可以理解为在规定时间内只能调用一次。且是最先被触发调用的那次。

应用场景:高频点击,scroll 滚动, 拖动改变窗口大小

function(fn, delay) {
    let timer = null;
    return function() {
        if(timer) return;
    
        timer = setTimeout(() => {
            fn.apply(this);
            timer = null;
        }, delay)
    }
}

浅拷贝

Object.assign

let obj1 = {
    a: 1,
    b: {
        c: 3,
    },
}
let obj2 = Object.assign({}, obj1);

... 扩展运算符

深拷贝

JSON.parse(JSON.stringify());

递归

const weakMap = new WeakMap();

function deepClone(target) {
  if(typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {};
    if(weakMap.get(target)) {
      return weakMap.get(target);
    }

    weakMap.set(target, cloneTarget);

    for(const key in target) {
      cloneTarget[key] = deepClone(target[key]);
    }

    return cloneTarget;

  } else {
    return target;
  }
}

前端性能优化

  1. 避免回流,重绘

    css 相关:避免使用 table 布局, 尽量在 dom 树的末尾改变 class, 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上

    js 相关:避免频繁操作样式和dom

  2. 尽量减少 http 请求,可使用 iconfont 图标代替图片图标

  3. 压缩图片,如使用雪碧图等

  4. 使用打包工具,如 webpack 进行优化

webpack 性能优化

可使用 include,exclude缩小文件范围和排除文件

resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径,减少耗时的递归解析操作

使用 resolve.extensions 将高频次的文件后缀放到前面避免多余查找

使用更高版本的 webpack

在大项目时,可以将费时的 loader 处理放在 thread-loader 中另外处理

利用缓存,如将缓存结果放到文件系统中

使用 tree-shaking,摇树,去掉多余代码

输出 url 到浏览器渲染出页面全过程

  1. 输入 url
  2. 查找缓存
  3. DNS 域名解析
  4. 建立 Tcp 连接,三次握手
  5. 发起 Http 请求
  6. 响应结果并返回 html 文件
  7. 关闭 Tcp 连接,四次挥手
  8. 浏览器渲染, 构建 Dom 树,构建 Css 规则树,浏览器解析 Dom 和 Css, 构建出渲染树,布局,计算出每个节点的位置并绘制
  9. js 引擎解析

new 的过程中发生了什么

  1. 创建一个空对象
  2. 将空对象的 __proto__ 指构造函数的 prototype
  3. 执行构造函数,将构造出来的新对象绑定为构造函数的 this
  4. 如果构造函数有返回一个对象,则返回这个对象,否则返回新创建的那个对象
function myNew(Fn, ...args) {
  if(typeof Fn !== 'function') {
    throw new TypeError('This is not a constructor')
  }

  const obj = {};
  obj.__proto__ = Fn.prototype;

  //使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
  const result = Fn.call(obj, ...args);
  return typeof result === 'object' ? result : obj;
}

call,bind,apply 的区别

callapplybind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入。改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

call方法的第一个参数也是this的指向,后面传入的是一个参数列表。改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

bind 方法和 call 很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)。改变this指向后不会立即执行,而是返回一个永久改变this指向的函数

webpack rollup vite

webpack 是一个现代化的静态模块打包工具,当 webpack 处理程序时,会从一个入口或多个入口构建一个依赖图,然后将项目中所需的每一个模块合成一个或多个 bundle

rollup 也是一个 esm 模块打包器,也 webpack 很相似,但 rollup 打包的文件更小,原生支持 tree-shaking,加载其它类型的文件或支持导入 commonJs,这些额外的需求需要其它的插件

vite 快速冷启动,它会直接启动服务器,不需要进行打包工作,不需要递归的去分析模块的依赖,以及解析模块,按需编译,利用现代浏览器支持 ES Module 的特性,当浏览器请求某个模块的时候,再根据需要对模块的内容进行编译,这种方式大大缩短了编译时间

vue-router 路由钩子

全局路由钩子: beforeEach beforeResolve afterEach

路由独享的守卫:beforeEnter

组件路由钩子:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

闭包

闭包就是有权访问另一个函数作用域中内部变量或数据的函数

权限管理

webpack 是什么

webpack 是一个现代化的 js 打包程序,从一个入口或者多个入口递归构建依赖图,将项目中的每一个模块打包成一个或者多个 bundle

webpack 构建流程

强缓存,协商缓存;两种方案优势缺点;分别使用情况,使用条件

  1. 强缓存

浏览器在加载资源时,根据请求头的 expires 和 cache-control 判断是否命中强缓存

强缓存是指在缓存过期时间内,客户端通过直接从缓存中读取资源,而不去请求服务器。强缓存通常通过在响应头中添加 Cache-Control 和 Expires 字段来实现,其中:

  • Cache-Control 通常被用来指定缓存策略,可以设置 max-age 来指定缓存的最长时间;
  • Expires 字段则指定了缓存过期的时间,格式是 GMT 格式的时间字符串。

优势:

  • 从缓存中读取资源速度快,能够快速响应客户端的请求;
  • 可以在客户端和服务器之间减少数据传输,节约带宽。

缺点:

  • 缓存过期后会导致资源更新不及时,客户端可能会请求到旧的资源;
  • 由于客户端直接使用缓存,服务器无法控制客户端的缓存行为,可能会导致客户端缓存资源过期却继续使用缓存的情况。

适用情况:

  • 静态资源,如图片、字体等。
  1. 协商缓存

协商缓存是指客户端在缓存过期之后,向服务器发出请求,询问服务器该资源是否已经发生了变化,如果资源没有发生变化,则服务器返回状态码 304 Not Modified,告诉客户端可以使用缓存,否则返回新的资源内容。协商缓存通常通过在响应头中添加 Last-Modified 和 ETag 字段来实现,其中:

  • Last-Modified 指定了资源的最后修改时间;
  • ETag 则是服务器生成的一个唯一标识,用于判断资源是否发生了变化。

优势:

  • 能够避免缓存资源过期却继续使用缓存的情况,保证资源的实时性;
  • 由于只有在缓存过期时才请求服务器,减少了对服务器的请求次数,节约了网络资源。

缺点:

  • 每次请求都需要带上缓存相关的字段,增加了请求的数据量;
  • 服务器需要判断资源是否发生了变化,增加了服务器的负担。

适用情况:

  • 动态资源,如网页、接口等。

综合来看,强缓存适用于静态资源的场景,协商缓存适用于动态资源的场景。在实际应用中,可以根据具体的业务需求和性能瓶颈选择合适的缓存方案。

react fiber

react 页面更新流程

  1. 组件渲染生成一棵新的虚拟dom树;

  2. 新旧虚拟dom树对比,找出变动的部分;(也就是常说的diff算法)

  3. 为真正改变的部分创建真实dom,把他们挂载到文档,实现页面重渲染;

在数据更新时,react会生成一颗虚拟 dom 树,给 diff 带来了压力,js 占据主线程去做比较,渲染线程便无法做其他工作,用户的交互得不到响应,所以便出现了 react fiber。

react fiber 没法让比较的时间缩短,但它使得 diff 的过程被分成一小段一小段的,因为它有了“保存工作进度”的能力。

老的架构:stack 深度优先遍历

  1. 从根节点开始,依次遍历该节点的所有子节点;

  2. 当一个节点的所有子节点遍历完成,才认为该节点遍历完成;

一旦中断,无法找到父节点,断点无法恢复。

image.png

新的架构:fiber

image.png

三个指针:父节点,下一个兄弟节点,第一个子节点

  1. 从根节点开始,依次遍历该节点的子节点、兄弟节点,如果两者都遍历了,则回到它的父节点;

  2. 当一个节点的所有子节点遍历完成,才认为该节点遍历完成;

fiber 是 react 16 提出的新机制,使用链表替代了树,使组件更新的流程可以被恢复,将组件渲染分片,让出主线程。

requestIdleCallback 宏任务 16ms 调用一次

fiber 是用来让原来同步的调用颗粒化的

image.png

Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

Promise构造函数是同步执行的,而then方法是异步执行的。

当我们创建一个Promise对象时,Promise构造函数会立即执行,并且会立即调用传入的函数。这个函数会立即执行,并且可以立即返回一个值或抛出一个异常。因此,Promise构造函数是同步执行的。

但是,当我们调用then方法时,它并不会立即执行回调函数。相反,它会将回调函数添加到一个任务队列中,等待当前任务执行完毕后再执行。这个任务队列是异步执行的,因此then方法是异步执行的。

react, vue 中 key 的作用

react, vue中的 diff 算法

  • react 采用单指针从左向右进行遍历
  • vue采用双指针,从两头向中间进行遍历 双端对比算法

react

  1. tree diff:在两个树对比时,只会比较同一层级的节点,会忽略掉跨层级的操作

  2. component diff:在对比两个组件时,首先会判断它们两个的类型是否相同,如果不同则不会进一步向下比较,会直接销毁组件,创建新的组件插入。

  3. element diff:对于同一层级的一组节点,会使用具有唯一性的key来区分是否需要创建,删除,或者是移动。

谈谈你对TCP三次握手和四次挥手的理解

TCP三次握手的过程如下:

  1. 客户端向服务器发送一个SYN包,表示请求建立连接。

  2. 服务器收到SYN包后,向客户端发送一个SYN-ACK包,表示确认请求,并请求建立连接。

  3. 客户端收到SYN-ACK包后,向服务器发送一个ACK包,表示确认建立连接。

宏任务和微任务的执行顺序

事件循环的执行顺序如下:

  1. 执行当前宏任务中的同步代码。

  2. 执行当前宏任务中的所有微任务,直到微任务队列为空。

  3. 从宏任务队列中取出一个宏任务,执行其中的同步代码。

  4. 执行当前宏任务中的所有微任务,直到微任务队列为空。

  5. 重复执行步骤3和步骤4,直到宏任务队列和微任务队列都为空。

console.log('start');

setTimeout(() => {
  console.log('setTimeout1');
  Promise.resolve().then(() => {
    console.log('Promise1');
  });
}, 0);

setTimeout(() => {
  console.log('setTimeout2');
  Promise.resolve().then(() => {
    console.log('Promise2');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('Promise3');
});

console.log('end');

在这个例子中,我们使用了两个setTimeout和三个Promise,它们分别代表了两个宏任务和三个微任务。代码的执行顺序如下:

  1. 执行第一个console.log,输出"start"。

  2. 执行第一个setTimeout,将其放入宏任务队列中。

  3. 执行第二个setTimeout,将其放入宏任务队列中。

  4. 执行Promise.resolve().then(),将其放入微任务队列中。

  5. 执行第三个console.log,输出"end"。

  6. 当前宏任务执行完毕,开始执行微任务队列中的所有任务。

  7. 执行第一个Promise的回调函数,输出"Promise3"。

  8. 执行第一个setTimeout的回调函数,输出"setTimeout1"。

  9. 执行第一个Promise的回调函数,输出"Promise1"。

  10. 执行第二个setTimeout的回调函数,输出"setTimeout2"。

  11. 执行第二个Promise的回调函数,输出"Promise2"。

微任务的执行优先级高于宏任务

setTimeout1和Promise1的回调函数先于setTimeout2和Promise2的回调函数执行,这是因为宏任务和微任务的执行顺序都是按照它们被添加到队列中的顺序执行的。

为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?

Vuex 的 mutation 和 Redux 的 reducer 都是用来修改状态的函数,它们的设计初衷是为了保证状态的可预测性和可维护性。因此,它们都不应该包含异步操作,因为异步操作会破坏状态的可预测性和可维护性。

应该将异步操作放在 action 中处理,而不是在 mutation 或 reducer 中处理。action 是一个用来处理异步操作的函数,它可以触发 mutation 或 reducer 来修改状态,从而保证状态的可预测性和可维护性。

redux-thunk的作用

redux-thunk 是一个用于处理异步操作的中间件, 允许我们在 Redux 中编写异步的 action creator。它的作用是将异步操作从组件中分离出来,使得组件只需要关注数据的展示和交互,而不需要关注数据的获取和处理。

BFC极其应用

独立渲染区域, BFC 中的元素按照一定的规则进行排列和布局,不会影响到外部元素的布局

应用:

清除浮动,当一个元素包含浮动元素时,它的高度会塌陷,从而影响到外部元素的布局。可以通过将该元素设置为 BFC 来清除浮动,从而保证布局的正确性。

防止margin重叠

Vue 的响应式原理中 Object.defineProperty 有什么缺陷?

  1. 无法监听数组的变化:Object.defineProperty只能监听对象属性的变化,无法监听数组的变化。当我们修改数组的元素时,Vue 无法自动更新视图,需要手动调用数组的变异方法或使用 Vue 提供的特殊方法来更新视图。

  2. 无法监听新增属性和删除属性:Object.defineProperty只能监听已有属性的变化,无法监听新增属性和删除属性。当我们新增或删除属性时,Vue无法自动更新视图,需要手动调用Vue提供的特殊方法来更新视图。

this.$set();

  1. 深度监听问题,只能代理一层属性,监听深层属性,需要递归

  2. 性能问题,需要为每个属性添加 getter, setter 方法

React 事件和原生事件有什么区别

  1. 事件处理函数不同,一个是普通 js 函数,一个是 dom 事件处理函数

  2. 事件对象不同,一个合成事件对象,它是 React 自己实现的,而原生事件对象是浏览器提供的事件对象。

  3. 事件冒泡机制不同:React 事件使用的是合成事件机制,它是一种模拟的事件冒泡机制,而原生事件使用的是浏览器提供的事件冒泡机制。

  4. 命名不同。

介绍一下http缓存

  • 追问:哪些字段用做强缓存?哪些字段用做协商缓存?
  • 追问:cache-control、expires、etag等字段的属性值是什么样的?
  • 追问:这些字段都被存放在请求的哪个部分?
  • 追问:last-modify和expires这些字段的时间有什么区别?
  • 追问:last-modify和expires能共存吗?
  • 追问:如果不想让某个资源使用缓存,那么应该如何设计http缓存?
  • 追问:cache-control中的no-cache和no-store的区别

HTTP缓存是指在Web应用中使用缓存来存储已经获取的资源副本,以便在后续的请求中重复使用。通过使用缓存,可以减少网络请求,提高网页加载速度,节省带宽和服务器资源。

强缓存是指客户端在发送请求之前,直接从本地缓存中获取资源副本,而无需与服务器进行通信。常用的字段用于控制强缓存的有:

  • Cache-Control:用于设置缓存的行为。常见的属性值有:

    • public:可以被任何缓存(包括CDN和浏览器)缓存。
    • private:只能被单个用户的浏览器缓存,不能被CDN缓存。
    • max-age:指定资源在缓存中的最长有效时间,单位为秒。
    • s-maxage:类似于max-age,但仅适用于共享缓存,如CDN。
    • no-cache:表示缓存服务器必须先与原始服务器验证资源是否过期,然后才能使用缓存的资源。
    • no-store:表示不允许缓存服务器缓存资源,每次都要向原始服务器请求。
  • Expires:指定资源的过期时间,是一个GMT格式的日期字符串。只有当客户端的时间在过期时间之前,才会使用缓存中的资源。

协商缓存是指客户端发送请求时,先与服务器进行通信,根据服务器返回的响应判断是否可以使用缓存。常用的字段用于控制协商缓存的有:

  • Last-Modified:服务器返回的资源的最后修改时间,用于判断资源是否有更新。
  • ETag:服务器返回的资源的唯一标识符,也用于判断资源是否有更新。

这些字段都存放在请求头部中。

Last-Modified 表示资源的最后修改时间,当客户端再次请求资源时,会将该值通过请求头部的 If-Modified-Since 字段发送给服务器,服务器会比较该时间与资源的最后修改时间,如果时间相同,则返回状态码 304 Not Modified,表示资源没有变化,客户端可以使用缓存的版本。

Expires 是一个具体的过期时间,由服务器返回给客户端。当客户端再次请求资源时,会将当前的时间与 Expires 的时间进行对比,如果当前时间小于过期时间,则表示资源仍然有效,客户端可以使用缓存的版本。

Last-ModifiedExpires 是可以共存的,它们在不同的场景下起作用。Last-Modified 用于协商缓存,客户端可以通过发送请求头部中的 If-Modified-Since 字段,将上次请求中返回的 Last-Modified 值发送给服务器,从而判断资源是否有更新。而 Expires 是指定资源的具体过期时间,客户端在再次请求资源时,通过与当前时间进行比较,判断资源是否过期。

如果不想让某个资源使用缓存,可以通过设置以下的响应头部字段来实现:

  • Cache-Control: no-store:禁止缓存服务器缓存资源。
  • Cache-Control: no-cache:要求缓存服务器在使用缓存资源之前,先与原始服务器进行验证。

什么是代理

顾名思义,所谓代理就是:本该以我的名义做的事,现在请你去做。

代理前:客户端 -> 服务器

代理后:客户端 -> 代理服务器 -> 服务器

使用代理前

客户端的请求会直接发送到服务器 =》 然后服务器处理完毕响应客户端 =》 客户端检测是否同源 =》 发现不同源,啪,报错。 游戏结束

使用代理后

客户端发送请求到代理服务器 =》 代理服务器 =》 代理服务器转发给目标服务器 =》 目标服务器处理请求并向代理服务器做出响应 =》 代理服务器接收响应 =》 代理服务器再把请求发送给客户端(代理服务器允许跨域请求。因为代理服务器是一个类中间件,具备一个换装功能。可以将不符合同源策略的域名伪装成符合同源策列的域名)=》 客户端检测不出跨域,处理响应。

非受控组件(uncontrolled component)

使用非受控组件,不是为每个状态更新编写数据处理函数,而是将表单数据交给 DOM 节点来处理,可以使用 Ref 来获取数据 在非受控组件中,希望能够赋予表单一个初始值,但是不去控制后续的更新。可以采用defaultValue指定一个默认值

受控组件(controlled component)

React 组件控制着用户输入过程中表单发生的操作并且 state 还是唯一数据源,被 React 以这种方式控制取值的表单输入元素叫做受控组件