「面试复盘」美图一面

113 阅读13分钟

1、vue3的响应式系统,是怎么跟他的父作用做关联的,非对象类型怎么处理的等等

Vue 3 的响应式系统利用了 ES6 中的 Proxy 对象来实现。在 Vue 3 中,每个组件实例都有一个对应的响应式对象,它通过 Proxy 对象来捕获对属性的访问和修改,并触发更新机制来实现响应式。

当在一个组件中定义一个响应式对象时,该对象会与组件实例建立联系,并且会自动和父子作用域建立关联。具体来说,响应式对象会在组件实例创建时进行初始化,并在父子组件关系建立时进行关联。

当一个响应式对象被创建时,它会被转换成一个 Proxy 对象,并使用一个隐藏的属性 __v_raw 来存储原始对象。这个隐藏属性是不能被外部访问的,它只能在内部用于判断一个对象是否已经被转换为 Proxy 对象。当我们访问响应式对象的属性时,Proxy 对象就会拦截这个访问,并触发依赖收集过程。

对于非对象类型,Vue 3 采用了一种叫做“wrapper”的技术来处理。例如,当我们在模板中使用一个基本类型值(如字符串或数值)时,Vue 3 会将其封装在一个简单的对象中,以便能够进行响应式处理。

在 Vue 3 中,除了对象和数组,所有其他原始类型值都会被封装为一个“wrapper”对象。这个对象包含一个内部属性 _value,用于存储原始的值。当我们在模板中使用一个基本类型值时,Vue 3 会使用这个“wrapper”对象来进行响应式处理。当我们修改这个值时,Vue 3 会更新“wrapper”对象的 _value 属性,并通知所有依赖该值的地方进行更新。

2、vue2,vue3的diff算法具体说说

diff 算法是用于在虚拟 DOM 中寻找差异并更新视图的

Vue 2 的 diff 算法采用了双端比较算法,它会对新旧节点进行同层级的比较。在比较过程中,如果发现新旧节点类型不同,则直接删除旧节点,并创建新节点;如果发现新旧节点类型相同,则继续进行同层级的子节点比较,依次递归下去。当所有子节点都比较完之后,如果新节点没有子节点了,但旧节点有子节点,则直接删除旧节点的子节点;如果新节点有子节点,但旧节点没有,则直接添加新节点的子节点。而对于相同节点,则采用“就地复用”的方式进行更新,只更新节点属性和子节点(这里的子节点指的是文本节点)。这样做的好处是,在保证更新速度的同时,还能减少不必要的 DOM 操作,提高性能。

Vue 3 的 diff 算法采用了更为高效的“静态标记”和“动态标记”技术。在首次渲染时,Vue 3 会根据模板生成一颗静态树,同样是采用双端比较算法进行 Diff 操作。但在更新过程中,Vue 3 会在静态树的基础上,再生成一棵动态树,用来表示那些可能发生改变的节点。因为静态节点在更新过程中不会发生变化,因此 Vue 3 可以跳过对静态节点的比较和更新。

在动态节点比较方面,Vue 3 的 diff 算法采用了“递归遍历+双端比较”的策略,这样可以最大程度地减少无效的 DOM 操作。具体来说,在比较两个动态节点时,Vue 3 会先比较它们的 key 值是否相同,如果不同,则直接删除旧节点,并创建新节点;如果相同,则继续进行同层级的子节点比较,依次递归下去。

3、如何理解前端模块化

是指将前端代码划分为相互独立、可复用的模块,以便于开发和维护。它的目标是提高代码的可维护性、可重用性和可扩展性,使前端开发更加高效和可靠。

可以从以下几个方面来考虑:

  1. 封装:模块化将代码封装在独立的模块中,每个模块负责特定的功能。便于后期的理解和维护。
  2. 依赖管理:通过模块化,可以明确地定义模块之间的依赖关系。模块可以通过导入其他模块来使用其功能,可以避免命名冲突和全局污染问题。同时,模块的依赖关系也使得代码复用更加方便,可以提高开发效率。
  3. 可复用性:模块化使得代码可以被多个项目或团队共享和复用。一个好的模块应该具有独立性和可组合性,可以在不同的项目中使用,提高开发效率和代码质量。
  4. 打包和构建:模块化对于打包和构建工具的支持非常重要。通过打包工具,可以将各个模块打包成一个或多个文件,减少网络请求次数,提高加载速度。同时,构建工具还可以对模块进行压缩、合并和优化,减小文件大小,提升性能。

4、amd、cmd、cjs、esm了解吗

  • AMD(Asynchronous Module Definition):AMD 是 RequireJS 提出的模块化规范,主要用于浏览器端异步加载模块。使用 define 函数来定义模块,并通过回调函数的方式来获取模块的依赖项。但由于其语法冗长,逐渐被其他更简洁的模块化方案所取代。
  • CMD(Common Module Definition):CMD 是 SeaJS 提出的一种模块化规范,和 AMD 相似,也是用于浏览器端异步加载模块。不同的是,CMD 模块的加载方式是按需加载。使用 define 函数来定义模块,但在获取模块的依赖项时采用延迟执行(按需加载)的方式。
  • CommonJS(CJS):CommonJS 是一种服务器端的模块化规范,主要用于 Node.js 环境下的模块管理。它的特点是模块同步加载,使用 require 来导入模块,使用 exports  或  module.exports 来导出模块。主要用于服务器端(如 Node.js)
  • ESM(ECMAScript Modules):ESM 是 JavaScript 的官方模块化规范,自 ES6 开始推广。使用 import 来导入模块,使用 export 来导出模块。既可以用于浏览器端又可以用于服务器端,支持静态引用和编译优化,可以进行静态分析、摇树优化等。

5、esm被导出后,引用的地方去修改这个值,会影响原值吗?

默认导出

对于default这种默认导出方式来说,导出的是变量的值,而不是变量变量本身。相当于模块把变量赋值给一个特殊的内部变量,此后始终导出这个特殊变量。

导出的地方

let numA = 3
setTimeout(() => {
 numA = 99
})
export default {
  objA: {
    nameA: 'esmA',
    idsA: [1, 2, 3, 4, 5]
  },
  numA
}

引入的地方

// 默认导出
import * as esmA from './esmA.mjs'

console.log(esmA, '初始化的值=====');

setTimeout(() => {
  console.log(esmA, '异步打印的esmA') 
})

我们会发现打印出的numA的值都是为 3。

image.png

接下来我们尝试在引用的地方修改导入的值。

import * as esmA from './esmA.mjs'

console.log(esmA, '初始化的值=====');

esmA.default.numA++
esmA.default.objA.nameA = '修改objA.nameA'

console.log(esmA, '修改后的值=====');

image.png

结论:会影响原值

命名导出

导出的地方

let numA = 3

setTimeout(() => {
  numA = 99
})
let objA = {
  nameA: 'esmA',
  idsA: [1,2,3,4,5]
}

export {
  objA,
  numA
}

引入的地方

// 命名导出
import { objA, numA } from './esmA.mjs'

console.log(objA, numA, '初始化的值=====');

setTimeout(() => {
  console.log(numA) // 99
})

我们会发现先打印 3,再打印 99。因为setTimout中的console.log执行时,numA已经被赋值为 99,而且命名导出的是变量本身,类似于 C 语言中的指针。

image.png

接下来我们尝试在引用的地方修改导入的值。

import { objA, numA } from './esmA.mjs'

console.log(objA, numA, '初始化的值=====');

numA++ //这一行会报错 TypeError: Assignment to constant variable.
objA.nameA = '修改objA.nameA'

console.log(objA, numA, '修改后的值=====');

命名导出的变量,修改后报错的信息是与修改常量的报错信息是一致的,也就是说导入的变量的类似用了const声明。

把报错的numA++ 这一行注释之后,只更改对象中的属性:

image.png

结论:会影响原值,但只能修改引用值中的属性。

在 ESM 中,当我们导入一个变量时,实际上是导入了该变量的引用。这意味着,如果导出的变量在导入模块中发生了改变,导入的变量也会随之改变。

而在 CommonJS 中,导入的是导出模块的值的拷贝,而不是引用。这意味着,即使导出模块中的值发生了改变,导入模块中导入的变量不会受到影响。

简而言之,ESM 导入的是值的引用,而 CJS 导入的是值的拷贝

6、esm中,a模块中导入b模块,b模块中导入a模块,会出现死循环吗?

结论:不管是esm还是cjs都不会死循环

ES Module导出的是一份值的引用,CommonJS则是一份值的拷贝。也就是说,CommonJS是把暴露的对象拷贝一份,放在新的一块内存中,每次直接在新的内存中取值,所以对变量修改没有办法同步;而ES Module则是指向同一块内存,模块实际导出的是这块内存的地址,每当用到时根据地址找到对应的内存空间,这样就实现了所谓的“动态绑定”。

  • CommonJS借助模块缓存,遇到require函数会先检查是否有缓存,已经有的则不会进入执行,在模块缓存中还记录着导出的变量的拷贝值;
  • ES Module借助模块地图,已经进入过的模块标注为获取中,遇到import语句会去检查这个地图,已经标注为获取中的则不会进入,地图中的每一个节点是一个模块记录,上面有导出变量的内存地址,导入时会做一个连接——即指向同一块内存。

7、自己实现一个深拷贝,如何处理循环引用、window情况。

weakMap

let obj = {
  a: 1,
  b: {
    c: 2
  }
}

let foo = {
  aa: window,
  bb: obj,
  cc: obj,
  dd: foo
}

问:怎么手写一个函数深拷贝foo这个对象。

8、weakMap为什么不会内存泄漏

WeakMap 是 JavaScript 中的一种数据结构,它和普通的 Map 类似,也是用来存储键值对的。但是与普通的 Map 不同的是,WeakMap 中的键必须是对象,而且是弱引用,即当键不再被引用时,它所对应的值也会被自动释放,这样可以避免内存泄漏。

具体来讲,当一个对象作为 WeakMap 的键时,这个键对应的内存地址会被记录下来,但并不会增加这个对象的引用计数,也就是说,这个对象仍然可以被垃圾回收器回收。如果在程序运行过程中,这个对象不再被其他部分引用,那么垃圾回收器就会回收它所占用的内存,同时也会自动删除 WeakMap 中对应的键值对。

相比之下,如果使用普通的 Map,当一个对象作为键时,这个对象的引用计数会增加,即使这个对象已经不再需要了,也无法被垃圾回收器回收,直到显式地从 Map 中删除这个键值对。如果这个操作被忘记了,就会导致内存泄漏。

因此,WeakMap 能够避免内存泄漏,是因为它的键是弱引用的,不会增加对象的引用计数,也不会阻止垃圾回收器对这个对象进行回收。但是需要注意的是,由于 WeakMap 中的键是弱引用的,因此无法像普通的 Map 那样遍历所有的键,也无法判断 WeakMap 中是否存在某个键。同时,WeakMap 也不能使用 for...of 或 forEach 等迭代方法来遍历键值对。

9、大文件缓存:如何缓存一个几十M、几百M的数据

借助ServiceWorker(具体查阅其他资料)

10、借助ServiceWorker进行大文件缓存,具体用worker干了什么

(具体查阅其他资料)

11、async await 相比promise的链式调用的方式有什么优势呢。

  1. 更直观和易读:async/await 语法使异步代码看起来更像是同步代码,更直观和易于理解。它使用了类似于同步代码的结构,通过 async 声明异步函数,使用 await 关键字等待异步操作完成。
  2. 错误处理更简洁:在传统的 Promise 链式调用中,错误处理通常通过 .catch() 方法或多个 .then() 方法中的回调函数来处理。而在 async/await 中,你可以使用 try/catch 块来捕获和处理异常,使错误处理更加简洁和集中。
  3. 更好的代码可读性和维护性:async/await 使得异步代码的流程更加线性和自然,减少了嵌套和回调地狱的问题。这样可以提高代码的可读性和维护性,降低理解和调试异步代码的难度。
  4. 更灵活的错误处理:async/await 允许你在每个异步操作之间执行自定义的错误处理逻辑,而不需要将错误处理逻辑集中在一个地方。你可以根据不同的情况对错误进行不同的处理,提供更灵活的错误处理能力。
  5. 支持同步风格的编程:使用 async/await 可以方便地处理需要按照顺序执行的异步操作,而无需使用回调函数或者 Promise 链式调用。这使得编写复杂的同步风格代码变得更加容易。

12、vue3的组合式api相比vue2的选项式api,解决了什么问题

  1. 代码复用性:Vue 2 的选项式 API 在处理逻辑复用时存在一些限制。通常情况下,我们需要使用 mixins 或高阶组件来实现逻辑的复用,但这往往会导致代码的维护和理解变得困难。而组合式 API 提供了 setup 函数,允许我们将逻辑组织为可复用的函数,从而更好地实现代码的复用。
  2. 组件逻辑的组织:Vue 2 的选项式 API 中,组件的逻辑往往是根据不同的选项(如 datamethodscomputed 等)进行分割的,这可能导致相关的逻辑在不同的选项中分散。而组合式 API 允许我们将相关的逻辑放在同一个函数中,使得组件的逻辑更加集中和清晰。
  3. 更好的类型推导和编辑器支持:由于 Vue 2 的选项式 API 是基于对象的,对于类型系统和编辑器支持并不友好。而组合式 API 利用 TypeScript 的类型推导和编辑器支持,提供了更好的类型检查和自动补全,使我们能够更早地发现错误,并提供更好的开发体验。
  4. 解决了命名冲突和命名空间污染的问题:在 Vue 2 中,不同的选项(如 datamethodscomputed 等)中可能存在命名冲突或命名空间污染的问题。而组合式 API 允许我们使用函数来定义逻辑,避免了命名冲突和命名空间污染的问题。